From e14561c8293fcfd27456df84327cc4f4700aacfb Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Aug 2025 16:03:27 +0200 Subject: [PATCH] fix: associate batch with boundary This associates the current batch with the boundary when entering pending mode. That way other async work associated to that boundary also can associate itself with that batch, even if e.g. due to flushing it is no longer the current batch. This solves a null pointer exception that can occur when the batch is flushed before the next top level await or async derived gets a hold of the current batch, which is null then. Fixes #16596 Fixes https://github.com/sveltejs/kit/issues/14124 --- .changeset/spicy-ears-join.md | 5 +++ .../internal/client/dom/blocks/boundary.js | 13 ++++++ .../src/internal/client/reactivity/batch.js | 2 +- .../internal/client/reactivity/deriveds.js | 2 +- .../samples/async-nested-top-level/Bar.svelte | 7 ++++ .../samples/async-nested-top-level/Foo.svelte | 10 +++++ .../samples/async-nested-top-level/_config.js | 42 +++++++++++++++++++ .../async-nested-top-level/main.svelte | 31 ++++++++++++++ .../async-top-level-deriveds/Foo.svelte | 8 ++++ .../async-top-level-deriveds/_config.js | 41 ++++++++++++++++++ .../async-top-level-deriveds/main.svelte | 31 ++++++++++++++ 11 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 .changeset/spicy-ears-join.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte diff --git a/.changeset/spicy-ears-join.md b/.changeset/spicy-ears-join.md new file mode 100644 index 0000000000..ee29e77afe --- /dev/null +++ b/.changeset/spicy-ears-join.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: associate batch with boundary diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 1866931ef2..b7fd9a856c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -59,6 +59,18 @@ export class Boundary { /** @type {Boundary | null} */ parent; + /** + * The associated batch to this boundary while the boundary pending; set by the one interacting with the boundary when entering pending state. + * Will be `null` once the boundary is no longer pending. + * + * Needed because `current_batch` isn't guaranteed to exist: E.g. when component A has top level await, then renders component B + * which also has top level await, `current_batch` can be null when a flush from component A happens before + * suspend() in component B is called. We hence save it on the boundary instead. + * + * @type {Batch | null} + */ + batch = null; + /** @type {TemplateNode} */ #anchor; @@ -231,6 +243,7 @@ export class Boundary { if (this.#pending_count === 0) { this.pending = false; + this.batch = null; if (this.#pending_effect) { pause_effect(this.#pending_effect, () => { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 123bc95d16..b3a3bef731 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -664,7 +664,7 @@ export function schedule_effect(signal) { export function suspend() { var boundary = get_pending_boundary(); - var batch = /** @type {Batch} */ (current_batch); + var batch = (boundary.batch ??= /** @type {Batch} */ (current_batch)); var pending = boundary.pending; boundary.update_pending_count(1); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 7f730e365e..0ac8582933 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -131,7 +131,7 @@ export function async_derived(fn, location) { prev = promise; - var batch = /** @type {Batch} */ (current_batch); + var batch = (boundary.batch ??= /** @type {Batch} */ (current_batch)); var pending = boundary.pending; if (should_suspend) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte new file mode 100644 index 0000000000..f1ac9ab760 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Bar.svelte @@ -0,0 +1,7 @@ + + +

bar: {bar}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte new file mode 100644 index 0000000000..e2029a3033 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/Foo.svelte @@ -0,0 +1,10 @@ + + +

foo: {foo}

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js new file mode 100644 index 0000000000..ca7965bf79 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [show, resolve] = target.querySelectorAll('button'); + + show.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo: foo

+

bar: bar

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte new file mode 100644 index 0000000000..bd0efaa4f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-top-level/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + {#if show} + + {/if} + + {#if $effect.pending()} +

pending...

+ {/if} + + {#snippet pending()} +

initializing...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte new file mode 100644 index 0000000000..e8a7c84137 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/Foo.svelte @@ -0,0 +1,8 @@ + + +

{foo} {bar}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js new file mode 100644 index 0000000000..2c7ffd3952 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [show, resolve] = target.querySelectorAll('button'); + + show.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

pending...

+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo bar

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte new file mode 100644 index 0000000000..bd0efaa4f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-deriveds/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + {#if show} + + {/if} + + {#if $effect.pending()} +

pending...

+ {/if} + + {#snippet pending()} +

initializing...

+ {/snippet} +