Skip to content

Commit c56ee71

Browse files
committed
add showPendingAfter and showPendingFor
1 parent c9d6195 commit c56ee71

File tree

4 files changed

+125
-16
lines changed

4 files changed

+125
-16
lines changed

packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { Context } from '../types' */
33
import * as e from '../../../errors.js';
44

5-
const valid = ['onerror', 'failed', 'pending'];
5+
const valid = ['onerror', 'failed', 'pending', 'showPendingAfter', 'showPendingFor'];
66

77
/**
88
* @param {AST.SvelteBoundary} node

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

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
BOUNDARY_EFFECT,
55
BOUNDARY_SUSPENDED,
66
EFFECT_PRESERVED,
7+
EFFECT_RAN,
78
EFFECT_TRANSPARENT,
89
RENDER_EFFECT
910
} from '../../constants.js';
@@ -33,6 +34,8 @@ import { queue_boundary_micro_task } from '../task.js';
3334
import * as e from '../../../shared/errors.js';
3435
import { DEV } from 'esm-env';
3536
import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js';
37+
import { raf } from '../../timing.js';
38+
import { loop } from '../../loop.js';
3639

3740
const ASYNC_INCREMENT = Symbol();
3841
const ASYNC_DECREMENT = Symbol();
@@ -69,16 +72,20 @@ var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
6972
/**
7073
* @param {TemplateNode} node
7174
* @param {{
72-
* onerror?: (error: unknown, reset: () => void) => void,
73-
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
74-
* pending?: (anchor: Node) => void
75+
* onerror?: (error: unknown, reset: () => void) => void;
76+
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
77+
* pending?: (anchor: Node) => void;
78+
* showPendingAfter?: number;
79+
* showPendingFor?: number;
7580
* }} props
7681
* @param {((anchor: Node) => void)} children
7782
* @returns {void}
7883
*/
7984
export function boundary(node, props, children) {
8085
var anchor = node;
8186

87+
var parent_boundary = find_boundary(active_effect);
88+
8289
block(() => {
8390
/** @type {Effect | null} */
8491
var main_effect = null;
@@ -106,6 +113,8 @@ export function boundary(node, props, children) {
106113
/** @type {Effect[]} */
107114
var effects = [];
108115

116+
var keep_pending_snippet = false;
117+
109118
/**
110119
* @param {() => void} snippet_fn
111120
* @returns {Effect | null}
@@ -145,6 +154,10 @@ export function boundary(node, props, children) {
145154
}
146155

147156
function unsuspend() {
157+
if (keep_pending_snippet || async_count > 0) {
158+
return;
159+
}
160+
148161
if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) {
149162
boundary.f ^= BOUNDARY_SUSPENDED;
150163
}
@@ -184,19 +197,70 @@ export function boundary(node, props, children) {
184197
}
185198
}
186199

200+
/**
201+
* @param {boolean} initial
202+
*/
203+
function show_pending_snippet(initial) {
204+
const pending = props.pending;
205+
206+
if (pending !== undefined) {
207+
// TODO can this be false?
208+
if (main_effect !== null) {
209+
offscreen_fragment = document.createDocumentFragment();
210+
move_effect(main_effect, offscreen_fragment);
211+
}
212+
213+
if (pending_effect === null) {
214+
pending_effect = branch(() => pending(anchor));
215+
}
216+
217+
// TODO do we want to differentiate between initial render and updates here?
218+
if (!initial) {
219+
keep_pending_snippet = true;
220+
221+
var end = raf.now() + (props.showPendingFor ?? 300);
222+
223+
loop((now) => {
224+
if (now >= end) {
225+
keep_pending_snippet = false;
226+
unsuspend();
227+
return false;
228+
}
229+
230+
return true;
231+
});
232+
}
233+
} else if (parent_boundary) {
234+
throw new Error('TODO show pending snippet on parent');
235+
} else {
236+
throw new Error('no pending snippet to show');
237+
}
238+
}
239+
187240
// @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field
188241
boundary.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => {
189242
if (input === ASYNC_INCREMENT) {
243+
// post-init, show the pending snippet after a timeout
244+
if ((boundary.f & BOUNDARY_SUSPENDED) === 0 && (boundary.f & EFFECT_RAN) !== 0) {
245+
var start = raf.now();
246+
var end = start + (props.showPendingAfter ?? 500);
247+
248+
loop((now) => {
249+
if (async_count === 0) return false;
250+
if (now < end) return true;
251+
252+
show_pending_snippet(false);
253+
});
254+
}
255+
190256
boundary.f |= BOUNDARY_SUSPENDED;
191257
async_count++;
192258

193-
// TODO post-init, show the pending snippet after a timeout
194-
195259
return;
196260
}
197261

198262
if (input === ASYNC_DECREMENT) {
199-
if (--async_count === 0) {
263+
if (--async_count === 0 && !keep_pending_snippet) {
200264
unsuspend();
201265

202266
if (main_effect !== null) {
@@ -307,15 +371,7 @@ export function boundary(node, props, children) {
307371

308372
if (async_count > 0) {
309373
boundary.f |= BOUNDARY_SUSPENDED;
310-
311-
if (pending) {
312-
offscreen_fragment = document.createDocumentFragment();
313-
move_effect(main_effect, offscreen_fragment);
314-
315-
pending_effect = branch(() => pending(anchor));
316-
} else {
317-
// TODO trigger pending boundary on parent
318-
}
374+
show_pending_snippet(true);
319375
}
320376
}
321377

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { flushSync, tick } from 'svelte';
2+
import { deferred } from '../../../../src/internal/shared/utils.js';
3+
import { test } from '../../test';
4+
5+
/** @type {ReturnType<typeof deferred>} */
6+
let d;
7+
8+
export default test({
9+
html: `<p>pending</p>`,
10+
11+
get props() {
12+
d = deferred();
13+
14+
return {
15+
promise: d.promise
16+
};
17+
},
18+
19+
async test({ assert, target, component, raf }) {
20+
d.resolve('hello');
21+
await Promise.resolve();
22+
await Promise.resolve();
23+
await tick();
24+
flushSync();
25+
assert.htmlEqual(target.innerHTML, '<h1>hello</h1>');
26+
27+
component.promise = (d = deferred()).promise;
28+
await tick();
29+
assert.htmlEqual(target.innerHTML, '<h1>hello</h1>');
30+
31+
raf.tick(500);
32+
assert.htmlEqual(target.innerHTML, '<p>pending</p>');
33+
34+
d.resolve('wheee');
35+
await tick();
36+
raf.tick(600);
37+
assert.htmlEqual(target.innerHTML, '<p>pending</p>');
38+
39+
raf.tick(800);
40+
assert.htmlEqual(target.innerHTML, '<h1>wheee</h1>');
41+
}
42+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let { promise } = $props();
3+
</script>
4+
5+
<svelte:boundary>
6+
<h1>{await promise}</h1>
7+
8+
{#snippet pending()}
9+
<p>pending</p>
10+
{/snippet}
11+
</svelte:boundary>

0 commit comments

Comments
 (0)