Skip to content

Commit f0b01f7

Browse files
stuff from upstream
1 parent de48a77 commit f0b01f7

File tree

6 files changed

+199
-28
lines changed

6 files changed

+199
-28
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/** @import { TemplateNode, Value } from '#client' */
22
import { flatten } from '../../reactivity/async.js';
33
import { get } from '../../runtime.js';
4-
import { get_pending_boundary } from './boundary.js';
4+
import { get_boundary } from './boundary.js';
55

66
/**
77
* @param {TemplateNode} node
88
* @param {Array<() => Promise<any>>} expressions
99
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
1010
*/
1111
export function async(node, expressions, fn) {
12-
var boundary = get_pending_boundary();
12+
var boundary = get_boundary();
1313

1414
boundary.update_pending_count(1);
1515

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

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,34 @@ export function boundary(node, props, children) {
5454
}
5555

5656
export class Boundary {
57-
pending = false;
58-
5957
/** @type {Boundary | null} */
6058
parent;
6159

60+
/**
61+
* Whether this boundary is inside a boundary (including this one) that's showing a pending snippet.
62+
* @type {boolean}
63+
*/
64+
get pending() {
65+
if (this.has_pending_snippet()) {
66+
return this.#pending;
67+
}
68+
69+
// intentionally not throwing here, as the answer to "am I in a pending snippet" is false when
70+
// there's no pending snippet at all
71+
return this.parent?.pending ?? false;
72+
}
73+
74+
set pending(value) {
75+
if (this.has_pending_snippet()) {
76+
this.#pending = value;
77+
} else if (this.parent) {
78+
this.parent.pending = value;
79+
} else if (value) {
80+
e.await_outside_boundary();
81+
}
82+
// if we're trying to set it to `false` and yeeting that into the void, it's fine
83+
}
84+
6285
/** @type {TemplateNode} */
6386
#anchor;
6487

@@ -86,7 +109,28 @@ export class Boundary {
86109
/** @type {DocumentFragment | null} */
87110
#offscreen_fragment = null;
88111

112+
/**
113+
* Whether this boundary is inside a boundary (including this one) that's showing a pending snippet.
114+
* Derived from {@link props.pending} and {@link cascading_pending_count}.
115+
*/
116+
#pending = false;
117+
118+
/**
119+
* The number of pending async deriveds/expressions within this boundary, not counting any parent or child boundaries.
120+
* This controls `$effect.pending` for this boundary.
121+
*
122+
* Don't ever set this directly; use {@link update_pending_count} instead.
123+
*/
89124
#pending_count = 0;
125+
126+
/**
127+
* Like {@link #pending_count}, but treats boundaries with no `pending` snippet as porous.
128+
* This controls the pending snippet for this boundary.
129+
*
130+
* Don't ever set this directly; use {@link update_pending_count} instead.
131+
*/
132+
#cascading_pending_count = 0;
133+
90134
#is_creating_fallback = false;
91135

92136
/**
@@ -154,7 +198,7 @@ export class Boundary {
154198
return branch(() => this.#children(this.#anchor));
155199
});
156200

157-
if (this.#pending_count > 0) {
201+
if (this.#cascading_pending_count > 0) {
158202
this.#show_pending_snippet();
159203
} else {
160204
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
@@ -171,7 +215,7 @@ export class Boundary {
171215
this.error(error);
172216
}
173217

174-
if (this.#pending_count > 0) {
218+
if (this.#cascading_pending_count > 0) {
175219
this.#show_pending_snippet();
176220
} else {
177221
this.pending = false;
@@ -225,11 +269,11 @@ export class Boundary {
225269
}
226270
}
227271

228-
/** @param {1 | -1} d */
229-
#update_pending_count(d) {
230-
this.#pending_count += d;
272+
/** @param {number} d */
273+
#update_cascading_pending_count(d) {
274+
this.#cascading_pending_count = Math.max(this.#cascading_pending_count + d, 0);
231275

232-
if (this.#pending_count === 0) {
276+
if (this.#cascading_pending_count === 0) {
233277
this.pending = false;
234278

235279
if (this.#pending_effect) {
@@ -245,12 +289,19 @@ export class Boundary {
245289
}
246290
}
247291

248-
/** @param {1 | -1} d */
249-
update_pending_count(d) {
292+
/**
293+
* @param {number} d
294+
* @param {boolean} safe
295+
*/
296+
update_pending_count(d, safe = false) {
297+
this.#pending_count = Math.max(this.#pending_count + d, 0);
298+
250299
if (this.has_pending_snippet()) {
251-
this.#update_pending_count(d);
300+
this.#update_cascading_pending_count(d);
252301
} else if (this.parent) {
253-
this.parent.#update_pending_count(d);
302+
this.parent.update_pending_count(d, safe);
303+
} else if (this.parent === null && !safe) {
304+
e.await_outside_boundary();
254305
}
255306

256307
effect_pending_updates.add(this.#effect_pending_update);
@@ -302,22 +353,26 @@ export class Boundary {
302353
e.svelte_boundary_reset_onerror();
303354
}
304355

305-
this.#pending_count = 0;
356+
// this ensures we modify the cascading_pending_count of the correct parent
357+
// by the number we're decreasing this boundary by
358+
this.update_pending_count(-this.#pending_count, true);
306359

307360
if (this.#failed_effect !== null) {
308361
pause_effect(this.#failed_effect, () => {
309362
this.#failed_effect = null;
310363
});
311364
}
312365

313-
this.pending = true;
366+
// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
367+
// but it would be really weird to show the parent's boundary on a child reset.
368+
this.pending = this.has_pending_snippet();
314369

315370
this.#main_effect = this.#run(() => {
316371
this.#is_creating_fallback = false;
317372
return branch(() => this.#children(this.#anchor));
318373
});
319374

320-
if (this.#pending_count > 0) {
375+
if (this.#cascading_pending_count > 0) {
321376
this.#show_pending_snippet();
322377
} else {
323378
this.pending = false;
@@ -386,13 +441,9 @@ function move_effect(effect, fragment) {
386441
}
387442
}
388443

389-
export function get_pending_boundary() {
444+
export function get_boundary() {
390445
var boundary = /** @type {Effect} */ (active_effect).b;
391446

392-
while (boundary !== null && !boundary.has_pending_snippet()) {
393-
boundary = boundary.parent;
394-
}
395-
396447
if (boundary === null) {
397448
e.await_outside_boundary();
398449
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { DESTROYED } from '#client/constants';
44
import { DEV } from 'esm-env';
55
import { component_context, is_runes, set_component_context } from '../context.js';
6-
import { get_pending_boundary } from '../dom/blocks/boundary.js';
76
import { invoke_error_boundary } from '../error-handling.js';
87
import {
98
active_effect,
@@ -39,7 +38,6 @@ export function flatten(sync, async, fn) {
3938
var parent = /** @type {Effect} */ (active_effect);
4039

4140
var restore = capture();
42-
var boundary = get_pending_boundary();
4341

4442
Promise.all(async.map((expression) => async_derived(expression)))
4543
.then((result) => {
@@ -60,7 +58,7 @@ export function flatten(sync, async, fn) {
6058
unset_context();
6159
})
6260
.catch((error) => {
63-
boundary.error(error);
61+
invoke_error_boundary(error, parent);
6462
});
6563
}
6664

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ import {
1010
INERT,
1111
RENDER_EFFECT,
1212
ROOT_EFFECT,
13-
USER_EFFECT,
1413
MAYBE_DIRTY
1514
} from '#client/constants';
1615
import { async_mode_flag } from '../../flags/index.js';
1716
import { deferred, define_property } from '../../shared/utils.js';
18-
import { get_pending_boundary } from '../dom/blocks/boundary.js';
17+
import { get_boundary } from '../dom/blocks/boundary.js';
1918
import {
2019
active_effect,
2120
is_dirty,
@@ -665,7 +664,7 @@ export function schedule_effect(signal) {
665664
}
666665

667666
export function suspend() {
668-
var boundary = get_pending_boundary();
667+
var boundary = get_boundary();
669668
var batch = /** @type {Batch} */ (current_batch);
670669
var pending = boundary.pending;
671670

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [increment, shift] = target.querySelectorAll('button');
7+
8+
assert.htmlEqual(target.innerHTML, `<p>loading...</p>`);
9+
10+
shift.click();
11+
shift.click();
12+
shift.click();
13+
14+
await tick();
15+
assert.htmlEqual(
16+
target.innerHTML,
17+
`
18+
<button>increment</button>
19+
<button>shift</button>
20+
<p>0</p>
21+
<p>0</p>
22+
<p>0</p>
23+
<p>inner pending: 0</p>
24+
<p>outer pending: 0</p>
25+
`
26+
);
27+
28+
increment.click();
29+
await tick();
30+
assert.htmlEqual(
31+
target.innerHTML,
32+
`
33+
<button>increment</button>
34+
<button>shift</button>
35+
<p>0</p>
36+
<p>0</p>
37+
<p>0</p>
38+
<p>inner pending: 3</p>
39+
<p>outer pending: 0</p>
40+
`
41+
);
42+
43+
shift.click();
44+
await tick();
45+
assert.htmlEqual(
46+
target.innerHTML,
47+
`
48+
<button>increment</button>
49+
<button>shift</button>
50+
<p>0</p>
51+
<p>0</p>
52+
<p>0</p>
53+
<p>inner pending: 2</p>
54+
<p>outer pending: 0</p>
55+
`
56+
);
57+
58+
shift.click();
59+
await tick();
60+
assert.htmlEqual(
61+
target.innerHTML,
62+
`
63+
<button>increment</button>
64+
<button>shift</button>
65+
<p>0</p>
66+
<p>0</p>
67+
<p>0</p>
68+
<p>inner pending: 1</p>
69+
<p>outer pending: 0</p>
70+
`
71+
);
72+
73+
shift.click();
74+
await tick();
75+
assert.htmlEqual(
76+
target.innerHTML,
77+
`
78+
<button>increment</button>
79+
<button>shift</button>
80+
<p>1</p>
81+
<p>1</p>
82+
<p>1</p>
83+
<p>inner pending: 0</p>
84+
<p>outer pending: 0</p>
85+
`
86+
);
87+
}
88+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script>
2+
let value = $state(0);
3+
let deferreds = [];
4+
5+
function push(value) {
6+
const deferred = Promise.withResolvers();
7+
deferreds.push({ value, deferred });
8+
return deferred.promise;
9+
}
10+
11+
function shift() {
12+
const d = deferreds.shift();
13+
d?.deferred.resolve(d.value);
14+
}
15+
</script>
16+
17+
<button onclick={() => value++}>increment</button>
18+
<button onclick={() => shift()}>shift</button>
19+
20+
<svelte:boundary>
21+
22+
<svelte:boundary>
23+
<p>{await push(value)}</p>
24+
<p>{await push(value)}</p>
25+
<p>{await push(value)}</p>
26+
<p>inner pending: {$effect.pending()}</p>
27+
</svelte:boundary>
28+
<p>outer pending: {$effect.pending()}</p>
29+
30+
{#snippet pending()}
31+
<p>loading...</p>
32+
{/snippet}
33+
</svelte:boundary>
34+
35+

0 commit comments

Comments
 (0)