From a012b1d1e0b8e627ea88f170bb1ab151dad81620 Mon Sep 17 00:00:00 2001 From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> Date: Sat, 14 Jun 2025 18:30:36 +0100 Subject: [PATCH] Prevent mutating arrays in $effect to cause inifinite loops --- packages/svelte/src/internal/client/proxy.js | 59 +++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4870506699..ab32c32e83 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,12 +1,13 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js'; +import { get, active_effect, active_reaction, set_active_reaction, untrack } from './runtime.js'; import { array_prototype, get_descriptor, get_prototype_of, is_array, - object_prototype + object_prototype, + define_property } from '../shared/utils.js'; import { state as source, set } from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; @@ -18,6 +19,19 @@ import { tracing_mode_flag } from '../flags/index.js'; // TODO move all regexes into shared module? const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; +// Array methods that mutate the array, according to the ECMAScript specification +const MUTATING_ARRAY_METHODS = new Set([ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse', + 'copyWithin', + 'fill' +]); + /** * @template T * @param {T} value @@ -43,6 +57,9 @@ export function proxy(value) { var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; var reaction = active_reaction; + /** @type {Map | null} */ + var proxied_array_mutating_methods_cache = is_proxied_array ? new Map() : null; + /** * @template T * @param {() => T} fn @@ -176,6 +193,44 @@ export function proxy(value) { return v === UNINITIALIZED ? undefined : v; } + // if this is a proxied array and the property is a mutating method, return a + // wrapper that executes the native method inside `untrack`, preventing the + // current reaction from accidentally depending on `length` (or other + // internals) that the algorithm reads. + if (is_proxied_array && typeof prop === 'string' && MUTATING_ARRAY_METHODS.has(prop)) { + /** @type {Map} */ + const mutating_methods_cache = /** @type {Map} */ (proxied_array_mutating_methods_cache); + + var cached_method = mutating_methods_cache.get(prop); + + if (cached_method === undefined) { + /** + * wrapper executes the native mutating method inside `untrack` so that + * any implicit `length` reads are ignored. + * @this any + * @param {...any} args + */ + cached_method = function (...args) { + // preserve correct `this` binding and forward result. + // eslint-disable-next-line prefer-spread + return untrack(() => /** @type {any} */ (array_prototype)[prop].apply(this, args)); + }; + + // give the wrapper a meaningful name for better debugging + try { + define_property(cached_method, 'name', { + value: `proxied_array_untracked_${/** @type {string} */ (prop)}` + }); + } catch { + // property might be non-configurable in some engines + } + + mutating_methods_cache.set(prop, cached_method); + } + + return cached_method; + } + return Reflect.get(target, prop, receiver); },