Skip to content

Commit e14561c

Browse files
committed
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 sveltejs/kit#14124
1 parent 2e02868 commit e14561c

File tree

11 files changed

+190
-2
lines changed

11 files changed

+190
-2
lines changed

.changeset/spicy-ears-join.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: associate batch with boundary

packages/svelte/src/internal/client/dom/blocks/boundary.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ export class Boundary {
5959
/** @type {Boundary | null} */
6060
parent;
6161

62+
/**
63+
* The associated batch to this boundary while the boundary pending; set by the one interacting with the boundary when entering pending state.
64+
* Will be `null` once the boundary is no longer pending.
65+
*
66+
* Needed because `current_batch` isn't guaranteed to exist: E.g. when component A has top level await, then renders component B
67+
* which also has top level await, `current_batch` can be null when a flush from component A happens before
68+
* suspend() in component B is called. We hence save it on the boundary instead.
69+
*
70+
* @type {Batch | null}
71+
*/
72+
batch = null;
73+
6274
/** @type {TemplateNode} */
6375
#anchor;
6476

@@ -231,6 +243,7 @@ export class Boundary {
231243

232244
if (this.#pending_count === 0) {
233245
this.pending = false;
246+
this.batch = null;
234247

235248
if (this.#pending_effect) {
236249
pause_effect(this.#pending_effect, () => {

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ export function schedule_effect(signal) {
664664

665665
export function suspend() {
666666
var boundary = get_pending_boundary();
667-
var batch = /** @type {Batch} */ (current_batch);
667+
var batch = (boundary.batch ??= /** @type {Batch} */ (current_batch));
668668
var pending = boundary.pending;
669669

670670
boundary.update_pending_count(1);

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export function async_derived(fn, location) {
131131

132132
prev = promise;
133133

134-
var batch = /** @type {Batch} */ (current_batch);
134+
var batch = (boundary.batch ??= /** @type {Batch} */ (current_batch));
135135
var pending = boundary.pending;
136136

137137
if (should_suspend) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts">
2+
import { resolve } from './main.svelte';
3+
4+
const bar = await new Promise((r) => resolve.push(() => r('bar')));
5+
</script>
6+
7+
<p>bar: {bar}</p>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
import { resolve } from './main.svelte';
3+
import Bar from './Bar.svelte';
4+
5+
const foo = await new Promise((r) => resolve.push(() => r('foo')));
6+
</script>
7+
8+
<p>foo: {foo}</p>
9+
10+
<Bar/>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [show, resolve] = target.querySelectorAll('button');
7+
8+
show.click();
9+
await tick();
10+
assert.htmlEqual(
11+
target.innerHTML,
12+
`
13+
<button>show</button>
14+
<button>resolve</button>
15+
<p>pending...</p>
16+
`
17+
);
18+
19+
resolve.click();
20+
await tick();
21+
assert.htmlEqual(
22+
target.innerHTML,
23+
`
24+
<button>show</button>
25+
<button>resolve</button>
26+
<p>pending...</p>
27+
`
28+
);
29+
30+
resolve.click();
31+
await tick();
32+
assert.htmlEqual(
33+
target.innerHTML,
34+
`
35+
<button>show</button>
36+
<button>resolve</button>
37+
<p>foo: foo</p>
38+
<p>bar: bar</p>
39+
`
40+
);
41+
}
42+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script module>
2+
export let resolve = [];
3+
</script>
4+
5+
<script>
6+
import Foo from './Foo.svelte';
7+
8+
let show = $state(false);
9+
</script>
10+
11+
<button onclick={() => show = true}>
12+
show
13+
</button>
14+
15+
<button onclick={() => resolve.shift()()}>
16+
resolve
17+
</button>
18+
19+
<svelte:boundary>
20+
{#if show}
21+
<Foo/>
22+
{/if}
23+
24+
{#if $effect.pending()}
25+
<p>pending...</p>
26+
{/if}
27+
28+
{#snippet pending()}
29+
<p>initializing...</p>
30+
{/snippet}
31+
</svelte:boundary>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
import { resolve } from './main.svelte';
3+
4+
const foo = $derived(await new Promise((r) => resolve.push(() => r('foo'))));
5+
const bar = $derived(await new Promise((r) => resolve.push(() => r('bar'))));
6+
</script>
7+
8+
<p>{foo} {bar}</p>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [show, resolve] = target.querySelectorAll('button');
7+
8+
show.click();
9+
await tick();
10+
assert.htmlEqual(
11+
target.innerHTML,
12+
`
13+
<button>show</button>
14+
<button>resolve</button>
15+
<p>pending...</p>
16+
`
17+
);
18+
19+
resolve.click();
20+
await tick();
21+
assert.htmlEqual(
22+
target.innerHTML,
23+
`
24+
<button>show</button>
25+
<button>resolve</button>
26+
<p>pending...</p>
27+
`
28+
);
29+
30+
resolve.click();
31+
await tick();
32+
assert.htmlEqual(
33+
target.innerHTML,
34+
`
35+
<button>show</button>
36+
<button>resolve</button>
37+
<p>foo bar</p>
38+
`
39+
);
40+
}
41+
});

0 commit comments

Comments
 (0)