diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 5018887d7f..8b3f817e0d 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -25,6 +25,7 @@ export const EFFECT_HAS_DERIVED = 1 << 21; // Flags used for async export const IS_ASYNC = 1 << 22; export const REACTION_IS_UPDATING = 1 << 23; +export const BOUNDARY_SUSPENDED = 1 << 24; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 31936aa5de..df9082ad0d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; +import { BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, @@ -16,7 +16,8 @@ import { set_active_effect, set_active_reaction, set_component_context, - reset_is_throwing_error + reset_is_throwing_error, + schedule_effect } from '../../runtime.js'; import { hydrate_next, @@ -117,18 +118,8 @@ export function boundary(node, props, children) { pause_effect( effect, () => { - var node = effect.nodes_start; - var end = effect.nodes_end; - offscreen_fragment = document.createDocumentFragment(); - - while (node !== null) { - /** @type {TemplateNode | null} */ - var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - - offscreen_fragment.append(node); - node = next; - } + move_effect(effect, offscreen_fragment); }, false ); @@ -146,7 +137,9 @@ export function boundary(node, props, children) { } if (pending_effect !== null) { - pause_effect(pending_effect); + pause_effect(pending_effect, () => { + pending_effect = null; + }); } anchor.before(/** @type {DocumentFragment} */ (offscreen_fragment)); @@ -159,7 +152,9 @@ export function boundary(node, props, children) { function reset() { if (failed_effect !== null) { - pause_effect(failed_effect); + pause_effect(failed_effect, () => { + failed_effect = null; + }); } main_effect = with_boundary(boundary, () => { @@ -176,16 +171,32 @@ export function boundary(node, props, children) { // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input) => { if (input === ASYNC_INCREMENT) { - if (async_count++ === 0) { - queue_boundary_micro_task(suspend); - } + async_count++; + + // TODO post-init, show the pending snippet after a timeout return; } if (input === ASYNC_DECREMENT) { if (--async_count === 0) { - queue_boundary_micro_task(unsuspend); + boundary.f ^= BOUNDARY_SUSPENDED; + + if (pending_effect) { + pause_effect(pending_effect, () => { + pending_effect = null; + }); + } + + if (offscreen_fragment) { + anchor.before(offscreen_fragment); + offscreen_fragment = null; + } + + if (main_effect !== null) { + // TODO do we also need to `resume_effect` here? + schedule_effect(main_effect); + } } return; @@ -260,6 +271,17 @@ export function boundary(node, props, children) { }); } else { main_effect = branch(() => children(anchor)); + + if (async_count > 0) { + if (pending) { + offscreen_fragment = document.createDocumentFragment(); + move_effect(main_effect, offscreen_fragment); + + pending_effect = branch(() => pending(anchor)); + } else { + // TODO trigger pending boundary on parent + } + } } reset_is_throwing_error(); @@ -270,6 +292,24 @@ export function boundary(node, props, children) { } } +/** + * + * @param {Effect} effect + * @param {DocumentFragment} fragment + */ +function move_effect(effect, fragment) { + var node = effect.nodes_start; + var end = effect.nodes_end; + + while (node !== null) { + /** @type {TemplateNode | null} */ + var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + fragment.append(node); + node = next; + } +} + export function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index c2448c9ee5..9b7047eaeb 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -30,7 +30,10 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + IS_ASYNC, + BOUNDARY_EFFECT, + BOUNDARY_SUSPENDED } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -254,6 +257,22 @@ function mark_reactions(signal, status) { continue; } + // if we're about to trip an async derived, mark the boundary as + // suspended _before_ we actually process effects + if ((flags & IS_ASYNC) !== 0) { + let boundary = /** @type {Derived} */ (reaction).parent; + + while (boundary !== null && (boundary.f & BOUNDARY_EFFECT) === 0) { + boundary = boundary.parent; + } + + if (boundary === null) { + // TODO this is presumably an error — throw here? + } else { + boundary.f |= BOUNDARY_SUSPENDED; + } + } + set_signal_status(reaction, status); // If the signal a) was previously clean or b) is an unowned derived, then mark it diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a29802dbb9..e19567d733 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -28,7 +28,8 @@ import { BOUNDARY_EFFECT, REACTION_IS_UPDATING, IS_ASYNC, - TEMPLATE_EFFECT + TEMPLATE_EFFECT, + BOUNDARY_SUSPENDED } from './constants.js'; import { flush_idle_tasks, @@ -843,15 +844,16 @@ function process_effects(effect, collected_effects) { ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0); if ((flags & RENDER_EFFECT) !== 0) { - if (is_branch) { - current_effect.f ^= CLEAN; + if ((flags & BOUNDARY_EFFECT) !== 0) { + suspended = (flags & BOUNDARY_SUSPENDED) !== 0; + } else if (is_branch) { + if (!suspended) { + current_effect.f ^= CLEAN; + } } else if (!skip_suspended) { try { if (check_dirtiness(current_effect)) { update_effect(current_effect); - if ((flags & IS_ASYNC) !== 0 && !suspended) { - suspended = true; - } } } catch (error) { handle_error(error, current_effect, null, current_effect.ctx); @@ -876,9 +878,16 @@ function process_effects(effect, collected_effects) { if (effect === parent) { break main_loop; } - if (suspended && (parent.f & BOUNDARY_EFFECT) !== 0 && is_pending_boundary(parent)) { - suspended = false; + + if ((parent.f & BOUNDARY_EFFECT) !== 0) { + let boundary = parent.parent; + while (boundary !== null && (boundary.f & BOUNDARY_EFFECT) === 0) { + boundary = boundary.parent; + } + + suspended = boundary === null ? false : (boundary.f & BOUNDARY_SUSPENDED) !== 0; } + var parent_sibling = parent.next; if (parent_sibling !== null) { current_effect = parent_sibling;