Prevent mutating arrays in $effect to cause inifinite loops

pull/16161/head
raythurnvoid 3 months ago
parent 546608636a
commit a012b1d1e0

@ -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<string, Function> | 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<string, Function>} */
const mutating_methods_cache = /** @type {Map<string, Function>} */ (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);
},

Loading…
Cancel
Save