chore: convert boundary implementation to a class (#16284)

* WIP class boundaries

* WIP

* WIP

* WIP

* unused

* unused

* unused
pull/16285/head
Rich Harris 3 months ago committed by GitHub
parent 49cba86e9b
commit 7914cb1835
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,6 +1,5 @@
/** @import { Effect, TemplateNode, } from '#client' */ /** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js'; import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js'; import { invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@ -21,116 +20,170 @@ import {
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
/** /**
* @param {Effect} boundary * @typedef {{
* @param {() => void} fn * 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 flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_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);
}
}
/** /**
* @param {TemplateNode} node * @param {TemplateNode} node
* @param {{ * @param {BoundaryProps} props
* onerror?: (error: unknown, reset: () => void) => void, * @param {((anchor: Node) => void)} children
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
* }} props
* @param {((anchor: Node) => void)} boundary_fn
* @returns {void} * @returns {void}
*/ */
export function boundary(node, props, boundary_fn) { export function boundary(node, props, children) {
var anchor = node; 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} */ /** @type {Effect} */
var boundary_effect; #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;
}
var reset = () => { /** @type {Effect | null} */
pause_effect(boundary_effect); #main_effect = null;
with_boundary(boundary, () => { /** @type {Effect | null} */
is_creating_fallback = false; #failed_effect = null;
boundary_effect = branch(() => boundary_fn(anchor));
});
};
var previous_reaction = active_reaction; #is_creating_fallback = false;
try { /**
set_active_reaction(null); * @param {TemplateNode} node
onerror?.(error, reset); * @param {BoundaryProps} props
} finally { * @param {((anchor: Node) => void)} children
set_active_reaction(previous_reaction); */
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) { try {
destroy_effect(boundary_effect); this.#main_effect = branch(() => children(this.#anchor));
} else if (hydrating) { } catch (error) {
set_hydrate_node(hydrate_open); this.error(error);
next();
set_hydrate_node(remove_nodes());
} }
}, flags);
if (failed) { if (hydrating) {
// Render the `failed` snippet in a microtask this.#anchor = hydrate_node;
queue_micro_task(() => { }
with_boundary(boundary, () => { }
is_creating_fallback = true;
/**
try { * @param {() => Effect | null} fn
boundary_effect = branch(() => { */
failed( #run(fn) {
anchor, var previous_effect = active_effect;
() => error, var previous_reaction = active_reaction;
() => reset var previous_ctx = component_context;
);
}); set_active_effect(this.#effect);
} catch (error) { set_active_reaction(this.#effect);
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent)); set_component_context(this.#effect.ctx);
}
try {
is_creating_fallback = false; 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) { // If we have nothing to capture the error, or if we hit an error while
hydrate_next(); // 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)); if (this.#main_effect) {
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); destroy_effect(this.#main_effect);
this.#main_effect = null;
}
if (hydrating) { if (this.#failed_effect) {
anchor = hydrate_node; 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;
}
});
});
}
} }
} }

@ -1,4 +1,5 @@
/** @import { Effect } from '#client' */ /** @import { Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js'; import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js'; import { is_firefox } from './dom/operations.js';
@ -39,8 +40,7 @@ export function invoke_error_boundary(error, effect) {
while (effect !== null) { while (effect !== null) {
if ((effect.f & BOUNDARY_EFFECT) !== 0) { if ((effect.f & BOUNDARY_EFFECT) !== 0) {
try { try {
// @ts-expect-error /** @type {Boundary} */ (effect.b).error(error);
effect.fn(error);
return; return;
} catch {} } catch {}
} }

@ -104,6 +104,7 @@ function create_effect(type, fn, sync, push = true) {
last: null, last: null,
next: null, next: null,
parent, parent,
b: parent && parent.b,
prev: null, prev: null,
teardown: null, teardown: null,
transitions: null, transitions: null,

@ -5,6 +5,7 @@ import type {
TemplateNode, TemplateNode,
TransitionManager TransitionManager
} from '#client'; } from '#client';
import type { Boundary } from '../dom/blocks/boundary';
export interface Signal { export interface Signal {
/** Flags bitmask */ /** Flags bitmask */
@ -84,6 +85,8 @@ export interface Effect extends Reaction {
last: null | Effect; last: null | Effect;
/** Parent effect */ /** Parent effect */
parent: Effect | null; parent: Effect | null;
/** The boundary this effect belongs to */
b: Boundary | null;
/** Dev only */ /** Dev only */
component_function?: any; component_function?: any;
/** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */ /** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */

Loading…
Cancel
Save