From 74c483c69de5b59995e0c8454a108f548a188c75 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 14 Jan 2025 23:46:51 +0000 Subject: [PATCH] wip --- .../2-analyze/visitors/SvelteBoundary.js | 2 +- .../client/visitors/SvelteBoundary.js | 5 +- packages/svelte/src/index-client.js | 2 + .../internal/client/dom/blocks/boundary.js | 154 +++++++++++++++--- packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/effects.js | 32 ++-- .../svelte/src/internal/client/runtime.js | 2 +- 7 files changed, 160 insertions(+), 39 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index d50cb80cb8..35af96ba12 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -const valid = ['onerror', 'failed']; +const valid = ['onerror', 'failed', 'pending']; /** * @param {AST.SvelteBoundary} node diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 325485d4c0..48402ccc75 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -39,7 +39,10 @@ export function SvelteBoundary(node, context) { // Capture the `failed` implicit snippet prop for (const child of node.fragment.nodes) { - if (child.type === 'SnippetBlock' && child.expression.name === 'failed') { + if ( + child.type === 'SnippetBlock' && + (child.expression.name === 'failed' || child.expression.name === 'pending') + ) { // we need to delay the visit of the snippets in case they access a ConstTag that is declared // after the snippets so that the visitor for the const tag can be updated snippets_visits.push(() => { diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 587d766233..1b15ec9fce 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -191,3 +191,5 @@ export { } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; + +export { suspend, unsuspend } from './internal/client/dom/blocks/boundary.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 7f4f000dce..ba983c4c4b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,7 +1,13 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; -import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; +import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT, INERT } from '../../constants.js'; +import { + block, + branch, + destroy_effect, + pause_effect, + resume_effect +} from '../../reactivity/effects.js'; import { active_effect, active_reaction, @@ -20,8 +26,12 @@ import { remove_nodes, set_hydrate_node } from '../hydration.js'; +import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; +const SUSPEND_INCREMENT = Symbol(); +const SUSPEND_DECREMENT = Symbol(); + /** * @param {Effect} boundary * @param {() => void} fn @@ -49,6 +59,7 @@ function with_boundary(boundary, fn) { * @param {{ * onerror?: (error: unknown, reset: () => void) => void, * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void + * pending?: (anchor: Node) => void * }} props * @param {((anchor: Node) => void)} boundary_fn * @returns {void} @@ -58,14 +69,95 @@ export function boundary(node, props, boundary_fn) { /** @type {Effect} */ var boundary_effect; + /** @type {Effect | null} */ + var suspended_effect = null; + /** @type {DocumentFragment | null} */ + var suspended_fragment = null; + var suspend_count = 0; block(() => { var boundary = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; var is_creating_fallback = false; - // We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {unknown}} */ error) => { + const render_snippet = (/** @type { () => void } */ snippet_fn) => { + // Render the snippet in a microtask + queue_micro_task(() => { + with_boundary(boundary, () => { + is_creating_fallback = true; + + try { + boundary_effect = branch(() => { + snippet_fn(); + }); + } catch (error) { + handle_error(error, boundary, null, boundary.ctx); + } + + reset_is_throwing_error(); + is_creating_fallback = false; + }); + }); + }; + + // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field + boundary.fn = (/** @type {unknown} */ input) => { + let pending = props.pending; + + if (input === SUSPEND_INCREMENT) { + if (!pending) { + return false; + } + suspend_count++; + + if (suspended_effect === null) { + var effect = boundary_effect; + suspended_effect = boundary_effect; + + pause_effect(suspended_effect, () => { + /** @type {TemplateNode | null} */ + var node = effect.nodes_start; + var end = effect.nodes_end; + suspended_fragment = document.createDocumentFragment(); + + while (node !== null) { + /** @type {TemplateNode | null} */ + var sibling = + node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + node.remove(); + suspended_fragment.append(node); + node = sibling; + } + }, false); + + render_snippet(() => { + pending(anchor); + }); + } + return true; + } + + if (input === SUSPEND_DECREMENT) { + if (!pending) { + return false; + } + suspend_count--; + + if (suspend_count === 0 && suspended_effect !== null) { + if (boundary_effect) { + destroy_effect(boundary_effect); + } + boundary_effect = suspended_effect; + suspended_effect = null; + anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); + resume_effect(boundary_effect); + } + + return true; + } + + var error = input; var onerror = props.onerror; let failed = props.failed; @@ -96,26 +188,12 @@ export function boundary(node, props, boundary_fn) { } if (failed) { - // Render the `failed` snippet in a microtask - queue_micro_task(() => { - with_boundary(boundary, () => { - is_creating_fallback = true; - - try { - boundary_effect = branch(() => { - failed( - anchor, - () => error, - () => reset - ); - }); - } catch (error) { - handle_error(error, boundary, null, boundary.ctx); - } - - reset_is_throwing_error(); - is_creating_fallback = false; - }); + render_snippet(() => { + failed( + anchor, + () => error, + () => reset + ); }); } }; @@ -132,3 +210,31 @@ export function boundary(node, props, boundary_fn) { anchor = hydrate_node; } } + +export function suspend() { + var current = active_effect; + + while (current !== null) { + if ((current.f & BOUNDARY_EFFECT) !== 0) { + // @ts-ignore + if (current.fn(SUSPEND_INCREMENT)) { + return; + } + } + current = current.parent; + } +} + +export function unsuspend() { + var current = active_effect; + + while (current !== null) { + if ((current.f & BOUNDARY_EFFECT) !== 0) { + // @ts-ignore + if (current.fn(SUSPEND_DECREMENT)) { + return; + } + } + current = current.parent; + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 2bf58c51f7..20ded180b0 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary } from './dom/blocks/boundary.js'; +export { boundary, suspend } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 149cbd2d38..abcb558c7f 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -528,15 +528,20 @@ export function unlink_effect(effect) { * A paused effect does not update, and the DOM subtree becomes inert. * @param {Effect} effect * @param {() => void} [callback] + * @param {boolean} [destroy] */ -export function pause_effect(effect, callback) { +export function pause_effect(effect, callback, destroy = true) { /** @type {TransitionManager[]} */ var transitions = []; - pause_children(effect, transitions, true); + pause_children(effect, transitions, true, destroy); run_out_transitions(transitions, () => { - destroy_effect(effect); + if (destroy) { + destroy_effect(effect); + } else { + execute_effect_teardown(effect); + } if (callback) callback(); }); } @@ -561,8 +566,9 @@ export function run_out_transitions(transitions, fn) { * @param {Effect} effect * @param {TransitionManager[]} transitions * @param {boolean} local + * @param {boolean} [destroy] */ -export function pause_children(effect, transitions, local) { +export function pause_children(effect, transitions, local, destroy = true) { if ((effect.f & INERT) !== 0) return; effect.f ^= INERT; @@ -582,7 +588,7 @@ export function pause_children(effect, transitions, local) { // TODO we don't need to call pause_children recursively with a linked list in place // it's slightly more involved though as we have to account for `transparent` changing // through the tree. - pause_children(child, transitions, transparent ? local : false); + pause_children(child, transitions, transparent ? local : false, destroy); child = sibling; } } @@ -602,17 +608,21 @@ export function resume_effect(effect) { */ function resume_children(effect, local) { if ((effect.f & INERT) === 0) return; + effect.f ^= INERT; + + // Ensure the effect is marked as clean again so that any dirty child + // effects can schedule themselves for execution + if ((effect.f & CLEAN) === 0) { + effect.f ^= CLEAN; + } // If a dependency of this effect changed while it was paused, - // apply the change now + // schedule the effect to update if (check_dirtiness(effect)) { - update_effect(effect); + set_signal_status(effect, DIRTY); + schedule_effect(effect); } - // Ensure we toggle the flag after possibly updating the effect so that - // each block logic can correctly operate on inert items - effect.f ^= INERT; - var child = effect.first; while (child !== null) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index eca5ee94f9..55a8ccf32d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -776,7 +776,7 @@ export function schedule_effect(signal) { var flags = effect.f; if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - if ((flags & CLEAN) === 0) return; + if ((flags & CLEAN) === 0) return effect.f ^= CLEAN; } }