diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index d7a53da1d1..03cec4fd2f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,5 @@ /** @import { Effect, TemplateNode, } from '#client' */ - -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants'; +import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants'; import { component_context, set_component_context } from '../../context.js'; import { invoke_error_boundary } from '../../error-handling.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; @@ -21,116 +20,170 @@ import { import { queue_micro_task } from '../task.js'; /** - * @param {Effect} boundary - * @param {() => void} fn + * @typedef {{ + * onerror?: (error: unknown, reset: () => void) => void; + * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; + * }} BoundaryProps */ -function with_boundary(boundary, fn) { - var previous_effect = active_effect; - var previous_reaction = active_reaction; - var previous_ctx = component_context; - - set_active_effect(boundary); - set_active_reaction(boundary); - set_component_context(boundary.ctx); - - try { - fn(); - } finally { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_ctx); - } -} + +var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; /** * @param {TemplateNode} node - * @param {{ - * onerror?: (error: unknown, reset: () => void) => void, - * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void - * }} props - * @param {((anchor: Node) => void)} boundary_fn + * @param {BoundaryProps} props + * @param {((anchor: Node) => void)} children * @returns {void} */ -export function boundary(node, props, boundary_fn) { - var anchor = node; +export function boundary(node, props, children) { + new Boundary(node, props, children); +} + +export class Boundary { + /** @type {TemplateNode} */ + #anchor; + + /** @type {TemplateNode} */ + #hydrate_open; + + /** @type {BoundaryProps} */ + #props; + + /** @type {((anchor: Node) => void)} */ + #children; /** @type {Effect} */ - var boundary_effect; - - 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) => { - var onerror = props.onerror; - let failed = props.failed; - - // If we have nothing to capture the error, or if we hit an error while - // rendering the fallback, re-throw for another boundary to handle - if ((!onerror && !failed) || is_creating_fallback) { - throw error; - } + #effect; - var reset = () => { - pause_effect(boundary_effect); + /** @type {Effect | null} */ + #main_effect = null; - with_boundary(boundary, () => { - is_creating_fallback = false; - boundary_effect = branch(() => boundary_fn(anchor)); - }); - }; + /** @type {Effect | null} */ + #failed_effect = null; - var previous_reaction = active_reaction; + #is_creating_fallback = false; - try { - set_active_reaction(null); - onerror?.(error, reset); - } finally { - set_active_reaction(previous_reaction); + /** + * @param {TemplateNode} node + * @param {BoundaryProps} props + * @param {((anchor: Node) => void)} children + */ + constructor(node, props, children) { + this.#anchor = node; + this.#props = props; + this.#children = children; + + this.#hydrate_open = hydrate_node; + + this.#effect = block(() => { + /** @type {Effect} */ (active_effect).b = this; + + if (hydrating) { + hydrate_next(); } - if (boundary_effect) { - destroy_effect(boundary_effect); - } else if (hydrating) { - set_hydrate_node(hydrate_open); - next(); - set_hydrate_node(remove_nodes()); + try { + this.#main_effect = branch(() => children(this.#anchor)); + } catch (error) { + this.error(error); } + }, flags); - 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) { - invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent)); - } - - is_creating_fallback = false; - }); + if (hydrating) { + this.#anchor = hydrate_node; + } + } + + /** + * @param {() => Effect | null} fn + */ + #run(fn) { + var previous_effect = active_effect; + var previous_reaction = active_reaction; + var previous_ctx = component_context; + + set_active_effect(this.#effect); + set_active_reaction(this.#effect); + set_component_context(this.#effect.ctx); + + try { + return fn(); + } finally { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_ctx); + } + } + + /** @param {unknown} error */ + error(error) { + var onerror = this.#props.onerror; + let failed = this.#props.failed; + + const reset = () => { + if (this.#failed_effect !== null) { + pause_effect(this.#failed_effect, () => { + this.#failed_effect = null; }); } + + this.#main_effect = this.#run(() => { + this.#is_creating_fallback = false; + return branch(() => this.#children(this.#anchor)); + }); }; - if (hydrating) { - hydrate_next(); + // If we have nothing to capture the error, or if we hit an error while + // rendering the fallback, re-throw for another boundary to handle + if (this.#is_creating_fallback || (!onerror && !failed)) { + throw error; + } + + var previous_reaction = active_reaction; + + try { + set_active_reaction(null); + onerror?.(error, reset); + } finally { + set_active_reaction(previous_reaction); } - boundary_effect = branch(() => boundary_fn(anchor)); - }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); + if (this.#main_effect) { + destroy_effect(this.#main_effect); + this.#main_effect = null; + } - if (hydrating) { - anchor = hydrate_node; + if (this.#failed_effect) { + destroy_effect(this.#failed_effect); + this.#failed_effect = null; + } + + if (hydrating) { + set_hydrate_node(this.#hydrate_open); + next(); + set_hydrate_node(remove_nodes()); + } + + if (failed) { + queue_micro_task(() => { + this.#failed_effect = this.#run(() => { + this.#is_creating_fallback = true; + + try { + return branch(() => { + failed( + this.#anchor, + () => error, + () => reset + ); + }); + } catch (error) { + invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent)); + return null; + } finally { + this.#is_creating_fallback = false; + } + }); + }); + } } } diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 99f3ed6cd4..378f7408ef 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -1,4 +1,5 @@ /** @import { Effect } from '#client' */ +/** @import { Boundary } from './dom/blocks/boundary.js' */ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; @@ -39,8 +40,7 @@ export function invoke_error_boundary(error, effect) { while (effect !== null) { if ((effect.f & BOUNDARY_EFFECT) !== 0) { try { - // @ts-expect-error - effect.fn(error); + /** @type {Boundary} */ (effect.b).error(error); return; } catch {} } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 702c410ce8..7570064c37 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -104,6 +104,7 @@ function create_effect(type, fn, sync, push = true) { last: null, next: null, parent, + b: parent && parent.b, prev: null, teardown: null, transitions: null, diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 80c4155705..90f922df68 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -5,6 +5,7 @@ import type { TemplateNode, TransitionManager } from '#client'; +import type { Boundary } from '../dom/blocks/boundary'; export interface Signal { /** Flags bitmask */ @@ -84,6 +85,8 @@ export interface Effect extends Reaction { last: null | Effect; /** Parent effect */ parent: Effect | null; + /** The boundary this effect belongs to */ + b: Boundary | null; /** Dev only */ component_function?: any; /** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */