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
boundary-batch-nullpointer-fix
Simon Holthausen 3 weeks ago
parent 2e02868ef1
commit e14561c829

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: associate batch with boundary

@ -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, () => {

@ -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);

@ -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) {

@ -0,0 +1,7 @@
<script lang="ts">
import { resolve } from './main.svelte';
const bar = await new Promise((r) => resolve.push(() => r('bar')));
</script>
<p>bar: {bar}</p>

@ -0,0 +1,10 @@
<script lang="ts">
import { resolve } from './main.svelte';
import Bar from './Bar.svelte';
const foo = await new Promise((r) => resolve.push(() => r('foo')));
</script>
<p>foo: {foo}</p>
<Bar/>

@ -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,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>foo: foo</p>
<p>bar: bar</p>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
export let resolve = [];
</script>
<script>
import Foo from './Foo.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>
show
</button>
<button onclick={() => resolve.shift()()}>
resolve
</button>
<svelte:boundary>
{#if show}
<Foo/>
{/if}
{#if $effect.pending()}
<p>pending...</p>
{/if}
{#snippet pending()}
<p>initializing...</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,8 @@
<script lang="ts">
import { resolve } from './main.svelte';
const foo = $derived(await new Promise((r) => resolve.push(() => r('foo'))));
const bar = $derived(await new Promise((r) => resolve.push(() => r('bar'))));
</script>
<p>{foo} {bar}</p>

@ -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,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>pending...</p>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>resolve</button>
<p>foo bar</p>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
export let resolve = [];
</script>
<script>
import Foo from './Foo.svelte';
let show = $state(false);
</script>
<button onclick={() => show = true}>
show
</button>
<button onclick={() => resolve.shift()()}>
resolve
</button>
<svelte:boundary>
{#if show}
<Foo/>
{/if}
{#if $effect.pending()}
<p>pending...</p>
{/if}
{#snippet pending()}
<p>initializing...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save