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(); + } + }); });