From d3fdf028a654b1dc63753cf23f36c465e3975dc9 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 18 Dec 2025 18:58:27 -0800 Subject: [PATCH 1/5] add leaking UNINITIALIZED tests (#17271) --- .../Child.svelte | 18 +++ .../_config.js | 25 ++++ .../main.svelte | 11 ++ packages/svelte/tests/signals/test.ts | 115 +++++++++++++++++- 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/Child.svelte new file mode 100644 index 0000000000..9b705f3064 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/Child.svelte @@ -0,0 +1,18 @@ + + +

bar: {bar}

+

baz: {baz}

+{#if qux} +

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js new file mode 100644 index 0000000000..ff77b1c512 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Ensure async derived remains reactive with associated effect and boundary with guard (#17271) +// +// Accounts for both UNINITIALIZED leaking from get and the baz derived remaining NaN +// due to having both an $effect and a boundary with an if in the same template +export default test({ + html: `

Loading...

`, + + skip_no_async: true, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

bar: 1

+

baz: 69

+

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte new file mode 100644 index 0000000000..9687e51400 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte @@ -0,0 +1,11 @@ + + + + {#snippet pending()} +

Loading...

+ {/snippet} + + +
diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 23c4bb42f9..b3bf3df5fe 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1,5 +1,5 @@ import { describe, assert, it } from 'vitest'; -import { flushSync } from '../../src/index-client'; +import { flushSync, fork } from '../../src/index-client'; import * as $ from '../../src/internal/client/runtime'; import { push, pop } from '../../src/internal/client/context'; import { @@ -9,15 +9,24 @@ import { user_effect, user_pre_effect } from '../../src/internal/client/reactivity/effects'; -import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; +import { + state, + set, + update, + update_pre, + internal_set, + source +} from '../../src/internal/client/reactivity/sources'; import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; -import { DESTROYED } from '../../src/internal/client/constants'; +import { DESTROYED, MAYBE_DIRTY } from '../../src/internal/client/constants'; +import { UNINITIALIZED } from '../../src/constants'; import { noop } from 'svelte/internal/client'; import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; +import { Batch } from '../../src/internal/client/reactivity/batch'; /** * @param runes runes mode @@ -1493,4 +1502,104 @@ describe('signals', () => { assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); }; }); + + // prevent UNINITIALIZED from leaking in get() (#17271) + it('derived with deps evaluated in fork should not return UNINITIALIZED', () => { + enable_async_mode_flag(); + + try { + const count = state(5); + let d: Derived; + + const f = fork(() => { + effect_root(() => { + render_effect(() => { + d = derived(() => $.get(count) * 2); + $.get(d); + }); + })(); + }); + + assert.equal(d!.v, /** @type {any} */ UNINITIALIZED); + assert.equal((d!.f & MAYBE_DIRTY) !== 0, true); + assert.equal($.get(d!), 10); + + f.discard(); + } finally { + disable_async_mode_flag(); + } + }); + + it('should return undefined when reading UNINITIALIZED from batch.previous via apply()', () => { + enable_async_mode_flag(); + + try { + const s = source(/** @type {any} */ UNINITIALIZED); + const dummy = state(0); + + const forkA = fork(() => { + effect_root(() => { + render_effect(() => { + internal_set(s, 'resolved'); + }); + })(); + }); + + let readValue: any; + + const forkB = fork(() => { + effect_root(() => { + render_effect(() => { + set(dummy, 1); + readValue = $.get(s); + }); + })(); + }); + + assert.equal(readValue, undefined); + assert.notEqual(readValue, UNINITIALIZED); + + forkA.discard(); + forkB.discard(); + } finally { + disable_async_mode_flag(); + } + }); + + it('should return undefined when reading UNINITIALIZED source captured in concurrent batch', () => { + enable_async_mode_flag(); + + try { + const s = source('initial'); + const dummy = state(0); + + // Create a pending batch + const f1 = fork(() => { + effect_root(() => { + render_effect(() => { + set(dummy, 1); + }); + })(); + }); + + let readValue: any; + const f2 = fork(() => { + effect_root(() => { + render_effect(() => { + // Puts UNINITIALIZED in batch_values via capture() + internal_set(s, /** @type {any} */ UNINITIALIZED); + readValue = $.get(s); + }); + })(); + }); + + assert.equal(readValue, undefined); + assert.notEqual(readValue, UNINITIALIZED); + + f1.discard(); + f2.discard(); + } finally { + disable_async_mode_flag(); + } + }); }); From 75d6e72e9974739892fdf01e14a70ccbe528dbe2 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 18 Dec 2025 18:58:27 -0800 Subject: [PATCH 2/5] fix: use current value for UNINITIALIZED sources in Batch.apply --- packages/svelte/src/internal/client/reactivity/batch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6f941c7ff2..37951e9d74 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -21,6 +21,7 @@ import { MANAGED_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; +import { UNINITIALIZED } from '../../../constants.js'; import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, @@ -320,7 +321,7 @@ export class Batch { // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` if ((source.f & ERROR_VALUE) === 0) { this.current.set(source, source.v); - batch_values?.set(source, source.v); + batch_values?.set(source, source.v === UNINITIALIZED ? undefined : source.v); } } @@ -546,7 +547,7 @@ export class Batch { for (const [source, previous] of batch.previous) { if (!batch_values.has(source)) { - batch_values.set(source, previous); + batch_values.set(source, previous === UNINITIALIZED ? undefined : previous); } } } From 2264941da32c784e0acfae1abf94ea04121b6ba4 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 18 Dec 2025 20:31:43 -0800 Subject: [PATCH 3/5] fix: prevent UNINITIALIZED from leaking in get() --- packages/svelte/src/internal/client/runtime.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 64c8409b8f..5fba2277df 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -163,6 +163,10 @@ export function is_dirty(reaction) { } if (flags & DERIVED) { + // UNINITIALIZED deriveds need computation + if (/** @type {Derived} */ (reaction).v === UNINITIALIZED) { + return true; + } reaction.f &= ~WAS_MARKED; } @@ -642,6 +646,10 @@ export function get(signal) { throw signal.v; } + if (signal.v === UNINITIALIZED) { + return /** @type {V} */ (undefined); + } + return signal.v; } From 38e851c9968f848a5dc49192ec916e76ee5ca463 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 18 Dec 2025 22:43:37 -0800 Subject: [PATCH 4/5] add changeset --- .changeset/loud-rabbits-call.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/loud-rabbits-call.md diff --git a/.changeset/loud-rabbits-call.md b/.changeset/loud-rabbits-call.md new file mode 100644 index 0000000000..564601f1af --- /dev/null +++ b/.changeset/loud-rabbits-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure unresolved async deriveds return undefined instead of internal symbol From 8250b929826af7c38fa5d02d79ed713f2ec2a573 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Fri, 19 Dec 2025 00:30:31 -0800 Subject: [PATCH 5/5] fix signals test type errors --- packages/svelte/tests/signals/test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index b3bf3df5fe..b0e70a03b0 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1520,7 +1520,7 @@ describe('signals', () => { })(); }); - assert.equal(d!.v, /** @type {any} */ UNINITIALIZED); + assert.equal(d!.v, UNINITIALIZED as unknown); assert.equal((d!.f & MAYBE_DIRTY) !== 0, true); assert.equal($.get(d!), 10); @@ -1534,7 +1534,7 @@ describe('signals', () => { enable_async_mode_flag(); try { - const s = source(/** @type {any} */ UNINITIALIZED); + const s = source(UNINITIALIZED as unknown); const dummy = state(0); const forkA = fork(() => { @@ -1587,7 +1587,7 @@ describe('signals', () => { effect_root(() => { render_effect(() => { // Puts UNINITIALIZED in batch_values via capture() - internal_set(s, /** @type {any} */ UNINITIALIZED); + internal_set(s, UNINITIALIZED as unknown); readValue = $.get(s); }); })();