From c745945e9ec92f6e2809b11f4e68594cee6dd36f Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:45:41 -0700 Subject: [PATCH] fix: avoid recursion error when tagging circular references --- .changeset/six-shirts-scream.md | 5 +++ .../svelte/src/internal/client/dev/tracing.js | 7 +++-- packages/svelte/src/internal/client/proxy.js | 31 ++++++++++++++----- .../src/internal/client/reactivity/sources.js | 2 +- 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 .changeset/six-shirts-scream.md diff --git a/.changeset/six-shirts-scream.md b/.changeset/six-shirts-scream.md new file mode 100644 index 0000000000..ac4d183474 --- /dev/null +++ b/.changeset/six-shirts-scream.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid recursion error when tagging circular references diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 673a710fac..06b0055cb9 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -179,7 +179,7 @@ export function get_stack(label) { */ export function tag(source, label) { source.label = label; - tag_proxy(source.v, label); + tag_proxy(source.v, label, source); return source; } @@ -187,10 +187,11 @@ export function tag(source, label) { /** * @param {unknown} value * @param {string} label + * @param {Value} source */ -export function tag_proxy(value, label) { +export function tag_proxy(value, label, source) { // @ts-expect-error - value?.[PROXY_PATH_SYMBOL]?.(label); + value?.[PROXY_PATH_SYMBOL]?.(label, source); return value; } diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 3ae4b87ed5..e39b8d6106 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -34,9 +34,13 @@ const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; /** * @template T * @param {T} value + * @param {WeakSet} [owned_sources] * @returns {T} */ -export function proxy(value) { +export function proxy(value, owned_sources) { + if (DEV) { + owned_sources ??= new WeakSet(); + } // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { return value; @@ -94,8 +98,15 @@ export function proxy(value) { /** Used in dev for $inspect.trace() */ var path = ''; - /** @param {string} new_path */ - function update_path(new_path) { + /** + * @param {string} new_path + * @param {Source} updater the source causing the path update + */ + function update_path(new_path, updater) { + // there's a circular reference somewhere, don't update the path to avoid recursion + if (owned_sources?.has(updater)) { + return; + } path = new_path; tag(version, `${path} version`); @@ -127,6 +138,7 @@ export function proxy(value) { sources.set(prop, s); if (DEV && typeof prop === 'string') { tag(s, get_label(path, prop)); + owned_sources?.add(s); } return s; }); @@ -148,6 +160,7 @@ export function proxy(value) { if (DEV) { tag(s, get_label(path, prop)); + owned_sources?.add(s); } } } else { @@ -173,11 +186,12 @@ export function proxy(value) { // create a source, but only if it's an own property and not a prototype property if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { s = with_parent(() => { - var p = proxy(exists ? target[prop] : UNINITIALIZED); + var p = proxy(exists ? target[prop] : UNINITIALIZED, DEV ? owned_sources : undefined); var s = source(p, stack); if (DEV) { tag(s, get_label(path, prop)); + owned_sources?.add(s); } return s; @@ -231,11 +245,12 @@ export function proxy(value) { ) { if (s === undefined) { s = with_parent(() => { - var p = has ? proxy(target[prop]) : UNINITIALIZED; + var p = has ? proxy(target[prop], DEV ? owned_sources : undefined) : UNINITIALIZED; var s = source(p, stack); if (DEV) { tag(s, get_label(path, prop)); + owned_sources?.add(s); } return s; @@ -272,6 +287,7 @@ export function proxy(value) { if (DEV) { tag(other_s, get_label(path, i)); + owned_sources?.add(other_s); } } } @@ -284,18 +300,19 @@ export function proxy(value) { if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { s = with_parent(() => source(undefined, stack)); - set(s, proxy(value)); + set(s, proxy(value, DEV ? owned_sources : undefined)); sources.set(prop, s); if (DEV) { tag(s, get_label(path, prop)); + owned_sources?.add(s); } } } else { has = s.v !== UNINITIALIZED; - var p = with_parent(() => proxy(value)); + var p = with_parent(() => proxy(value, DEV ? owned_sources : undefined)); set(s, p); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 7fb3135708..45582d4f9f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -156,7 +156,7 @@ export function set(source, value, should_proxy = false) { let new_value = should_proxy ? proxy(value) : value; if (DEV) { - tag_proxy(new_value, /** @type {string} */ (source.label)); + tag_proxy(new_value, /** @type {string} */ (source.label), source); } return internal_set(source, new_value);