Skip to content

Commit 116e9c8

Browse files
committed
Prevent mutating arrays in $effect to cause inifinite loops
1 parent 58b4b84 commit 116e9c8

File tree

1 file changed

+57
-2
lines changed
  • packages/svelte/src/internal/client

1 file changed

+57
-2
lines changed

packages/svelte/src/internal/client/proxy.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/** @import { Source } from '#client' */
22
import { DEV } from 'esm-env';
3-
import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
3+
import { get, active_effect, active_reaction, set_active_reaction, untrack } from './runtime.js';
44
import {
55
array_prototype,
66
get_descriptor,
77
get_prototype_of,
88
is_array,
9-
object_prototype
9+
object_prototype,
10+
define_property
1011
} from '../shared/utils.js';
1112
import { state as source, set } from './reactivity/sources.js';
1213
import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
@@ -18,6 +19,19 @@ import { tracing_mode_flag } from '../flags/index.js';
1819
// TODO move all regexes into shared module?
1920
const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
2021

22+
// Array methods that mutate the array, according to the ECMAScript specification
23+
const MUTATING_ARRAY_METHODS = new Set([
24+
'push',
25+
'pop',
26+
'shift',
27+
'unshift',
28+
'splice',
29+
'sort',
30+
'reverse',
31+
'copyWithin',
32+
'fill'
33+
]);
34+
2135
/**
2236
* @template T
2337
* @param {T} value
@@ -43,6 +57,9 @@ export function proxy(value) {
4357
var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
4458
var reaction = active_reaction;
4559

60+
/** @type {Map<string, Function> | null} */
61+
var proxied_array_mutating_methods_cache = is_proxied_array ? new Map() : null;
62+
4663
/**
4764
* @template T
4865
* @param {() => T} fn
@@ -176,6 +193,44 @@ export function proxy(value) {
176193
return v === UNINITIALIZED ? undefined : v;
177194
}
178195

196+
// if this is a proxied array and the property is a mutating method, return a
197+
// wrapper that executes the native method inside `untrack`, preventing the
198+
// current reaction from accidentally depending on `length` (or other
199+
// internals) that the algorithm reads.
200+
if (is_proxied_array && typeof prop === 'string' && MUTATING_ARRAY_METHODS.has(prop)) {
201+
/** @type {Map<string, Function>} */
202+
const mutating_methods_cache = /** @type {Map<string, Function>} */ (proxied_array_mutating_methods_cache);
203+
204+
var cached_method = mutating_methods_cache.get(prop);
205+
206+
if (cached_method === undefined) {
207+
/**
208+
* wrapper executes the native mutating method inside `untrack` so that
209+
* any implicit `length` reads are ignored.
210+
* @this any
211+
* @param {...any} args
212+
*/
213+
cached_method = function (...args) {
214+
// preserve correct `this` binding and forward result.
215+
// eslint-disable-next-line prefer-spread
216+
return untrack(() => /** @type {any} */ (array_prototype)[prop].apply(this, args));
217+
};
218+
219+
// give the wrapper a meaningful name for better debugging
220+
try {
221+
define_property(cached_method, 'name', {
222+
value: `proxied_array_untracked_${/** @type {string} */ (prop)}`
223+
});
224+
} catch {
225+
// property might be non-configurable in some engines
226+
}
227+
228+
mutating_methods_cache.set(prop, cached_method);
229+
}
230+
231+
return cached_method;
232+
}
233+
179234
return Reflect.get(target, prop, receiver);
180235
},
181236

0 commit comments

Comments
 (0)