add showPendingAfter and showPendingFor

aa-coordination
Rich Harris 7 months ago
parent c9d61951c6
commit c56ee71653

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
const valid = ['onerror', 'failed', 'pending']; const valid = ['onerror', 'failed', 'pending', 'showPendingAfter', 'showPendingFor'];
/** /**
* @param {AST.SvelteBoundary} node * @param {AST.SvelteBoundary} node

@ -4,6 +4,7 @@ import {
BOUNDARY_EFFECT, BOUNDARY_EFFECT,
BOUNDARY_SUSPENDED, BOUNDARY_SUSPENDED,
EFFECT_PRESERVED, EFFECT_PRESERVED,
EFFECT_RAN,
EFFECT_TRANSPARENT, EFFECT_TRANSPARENT,
RENDER_EFFECT RENDER_EFFECT
} from '../../constants.js'; } from '../../constants.js';
@ -33,6 +34,8 @@ import { queue_boundary_micro_task } from '../task.js';
import * as e from '../../../shared/errors.js'; import * as e from '../../../shared/errors.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js';
import { raf } from '../../timing.js';
import { loop } from '../../loop.js';
const ASYNC_INCREMENT = Symbol(); const ASYNC_INCREMENT = Symbol();
const ASYNC_DECREMENT = Symbol(); const ASYNC_DECREMENT = Symbol();
@ -69,9 +72,11 @@ var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
/** /**
* @param {TemplateNode} node * @param {TemplateNode} node
* @param {{ * @param {{
* onerror?: (error: unknown, reset: () => void) => void, * onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* pending?: (anchor: Node) => void * pending?: (anchor: Node) => void;
* showPendingAfter?: number;
* showPendingFor?: number;
* }} props * }} props
* @param {((anchor: Node) => void)} children * @param {((anchor: Node) => void)} children
* @returns {void} * @returns {void}
@ -79,6 +84,8 @@ var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
export function boundary(node, props, children) { export function boundary(node, props, children) {
var anchor = node; var anchor = node;
var parent_boundary = find_boundary(active_effect);
block(() => { block(() => {
/** @type {Effect | null} */ /** @type {Effect | null} */
var main_effect = null; var main_effect = null;
@ -106,6 +113,8 @@ export function boundary(node, props, children) {
/** @type {Effect[]} */ /** @type {Effect[]} */
var effects = []; var effects = [];
var keep_pending_snippet = false;
/** /**
* @param {() => void} snippet_fn * @param {() => void} snippet_fn
* @returns {Effect | null} * @returns {Effect | null}
@ -145,6 +154,10 @@ export function boundary(node, props, children) {
} }
function unsuspend() { function unsuspend() {
if (keep_pending_snippet || async_count > 0) {
return;
}
if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) { if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) {
boundary.f ^= BOUNDARY_SUSPENDED; boundary.f ^= BOUNDARY_SUSPENDED;
} }
@ -184,19 +197,70 @@ export function boundary(node, props, children) {
} }
} }
/**
* @param {boolean} initial
*/
function show_pending_snippet(initial) {
const pending = props.pending;
if (pending !== undefined) {
// TODO can this be false?
if (main_effect !== null) {
offscreen_fragment = document.createDocumentFragment();
move_effect(main_effect, offscreen_fragment);
}
if (pending_effect === null) {
pending_effect = branch(() => pending(anchor));
}
// TODO do we want to differentiate between initial render and updates here?
if (!initial) {
keep_pending_snippet = true;
var end = raf.now() + (props.showPendingFor ?? 300);
loop((now) => {
if (now >= end) {
keep_pending_snippet = false;
unsuspend();
return false;
}
return true;
});
}
} else if (parent_boundary) {
throw new Error('TODO show pending snippet on parent');
} else {
throw new Error('no pending snippet to show');
}
}
// @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field
boundary.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { boundary.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => {
if (input === ASYNC_INCREMENT) { if (input === ASYNC_INCREMENT) {
// post-init, show the pending snippet after a timeout
if ((boundary.f & BOUNDARY_SUSPENDED) === 0 && (boundary.f & EFFECT_RAN) !== 0) {
var start = raf.now();
var end = start + (props.showPendingAfter ?? 500);
loop((now) => {
if (async_count === 0) return false;
if (now < end) return true;
show_pending_snippet(false);
});
}
boundary.f |= BOUNDARY_SUSPENDED; boundary.f |= BOUNDARY_SUSPENDED;
async_count++; async_count++;
// TODO post-init, show the pending snippet after a timeout
return; return;
} }
if (input === ASYNC_DECREMENT) { if (input === ASYNC_DECREMENT) {
if (--async_count === 0) { if (--async_count === 0 && !keep_pending_snippet) {
unsuspend(); unsuspend();
if (main_effect !== null) { if (main_effect !== null) {
@ -307,15 +371,7 @@ export function boundary(node, props, children) {
if (async_count > 0) { if (async_count > 0) {
boundary.f |= BOUNDARY_SUSPENDED; boundary.f |= BOUNDARY_SUSPENDED;
show_pending_snippet(true);
if (pending) {
offscreen_fragment = document.createDocumentFragment();
move_effect(main_effect, offscreen_fragment);
pending_effect = branch(() => pending(anchor));
} else {
// TODO trigger pending boundary on parent
}
} }
} }

@ -0,0 +1,42 @@
import { flushSync, tick } from 'svelte';
import { deferred } from '../../../../src/internal/shared/utils.js';
import { test } from '../../test';
/** @type {ReturnType<typeof deferred>} */
let d;
export default test({
html: `<p>pending</p>`,
get props() {
d = deferred();
return {
promise: d.promise
};
},
async test({ assert, target, component, raf }) {
d.resolve('hello');
await Promise.resolve();
await Promise.resolve();
await tick();
flushSync();
assert.htmlEqual(target.innerHTML, '<h1>hello</h1>');
component.promise = (d = deferred()).promise;
await tick();
assert.htmlEqual(target.innerHTML, '<h1>hello</h1>');
raf.tick(500);
assert.htmlEqual(target.innerHTML, '<p>pending</p>');
d.resolve('wheee');
await tick();
raf.tick(600);
assert.htmlEqual(target.innerHTML, '<p>pending</p>');
raf.tick(800);
assert.htmlEqual(target.innerHTML, '<h1>wheee</h1>');
}
});

@ -0,0 +1,11 @@
<script>
let { promise } = $props();
</script>
<svelte:boundary>
<h1>{await promise}</h1>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save