Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 143 additions & 90 deletions packages/svelte/src/internal/client/dom/blocks/boundary.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is intended to match the signature of the Boundary constructor you can just type it /** @type {typeof Boundary} */ to avoid having all of the duplicate JSDoc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not working for me?

image

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;
}
});
});
}
}
}
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/error-handling.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {}
}
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/reactivity/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/svelte/src/internal/client/reactivity/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
TemplateNode,
TransitionManager
} from '#client';
import type { Boundary } from '../dom/blocks/boundary';

export interface Signal {
/** Flags bitmask */
Expand Down Expand Up @@ -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 */
Expand Down