WIP class boundaries

pull/16284/head
Rich Harris 3 months ago
parent 49cba86e9b
commit 0045414425

@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode, } from '#client' */ /** @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 { 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';
@ -18,23 +18,102 @@ import {
remove_nodes, remove_nodes,
set_hydrate_node set_hydrate_node
} from '../hydration.js'; } from '../hydration.js';
import { get_next_sibling } from '../operations.js';
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 flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
* @param {((anchor: Node) => void)} children
* @returns {void}
*/
export function boundary(node, props, children) {
new Boundary(node, props, children);
}
export class Boundary {
/** @type {Boundary | null} */
parent;
/** @type {TemplateNode} */
#anchor;
/** @type {TemplateNode} */
#hydrate_open;
/** @type {BoundaryProps} */
#props;
/** @type {((anchor: Node) => void)} */
#children;
/** @type {Effect} */
#effect;
/** @type {Effect | null} */
#main_effect = null;
/** @type {Effect | null} */
#failed_effect = null;
#is_creating_fallback = false;
/**
* @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.parent = /** @type {Effect} */ (active_effect).b;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
if (hydrating) {
hydrate_next();
}
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
}, flags);
if (hydrating) {
this.#anchor = hydrate_node;
}
}
/**
* @param {() => Effect | null} fn
*/
#run(fn) {
var previous_effect = active_effect; var previous_effect = active_effect;
var previous_reaction = active_reaction; var previous_reaction = active_reaction;
var previous_ctx = component_context; var previous_ctx = component_context;
set_active_effect(boundary); set_active_effect(this.#effect);
set_active_reaction(boundary); set_active_reaction(this.#effect);
set_component_context(boundary.ctx); set_component_context(this.#effect.ctx);
try { try {
fn(); return fn();
} finally { } finally {
set_active_effect(previous_effect); set_active_effect(previous_effect);
set_active_reaction(previous_reaction); set_active_reaction(previous_reaction);
@ -42,46 +121,30 @@ function with_boundary(boundary, fn) {
} }
} }
/** /** @param {unknown} error */
* @param {TemplateNode} node error(error) {
* @param {{ var onerror = this.#props.onerror;
* onerror?: (error: unknown, reset: () => void) => void, let failed = this.#props.failed;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
* }} props
* @param {((anchor: Node) => void)} boundary_fn
* @returns {void}
*/
export function boundary(node, props, boundary_fn) {
var anchor = node;
/** @type {Effect} */ const reset = () => {
var boundary_effect; if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
block(() => { this.#failed_effect = null;
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 this.#main_effect = this.#run(() => {
boundary.fn = (/** @type {unknown}} */ error) => { this.#is_creating_fallback = false;
var onerror = props.onerror; return branch(() => this.#children(this.#anchor));
let failed = props.failed; });
};
// If we have nothing to capture the error, or if we hit an error while // 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 // rendering the fallback, re-throw for another boundary to handle
if ((!onerror && !failed) || is_creating_fallback) { if (this.#is_creating_fallback || (!onerror && !failed)) {
throw error; throw error;
} }
var reset = () => {
pause_effect(boundary_effect);
with_boundary(boundary, () => {
is_creating_fallback = false;
boundary_effect = branch(() => boundary_fn(anchor));
});
};
var previous_reaction = active_reaction; var previous_reaction = active_reaction;
try { try {
@ -91,46 +154,61 @@ export function boundary(node, props, boundary_fn) {
set_active_reaction(previous_reaction); set_active_reaction(previous_reaction);
} }
if (boundary_effect) { if (this.#main_effect) {
destroy_effect(boundary_effect); destroy_effect(this.#main_effect);
} else if (hydrating) { this.#main_effect = null;
set_hydrate_node(hydrate_open); }
if (this.#failed_effect) {
destroy_effect(this.#failed_effect);
this.#failed_effect = null;
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
next(); next();
set_hydrate_node(remove_nodes()); set_hydrate_node(remove_nodes());
} }
if (failed) { if (failed) {
// Render the `failed` snippet in a microtask
queue_micro_task(() => { queue_micro_task(() => {
with_boundary(boundary, () => { this.#failed_effect = this.#run(() => {
is_creating_fallback = true; this.#is_creating_fallback = true;
try { try {
boundary_effect = branch(() => { return branch(() => {
failed( failed(
anchor, this.#anchor,
() => error, () => error,
() => reset () => reset
); );
}); });
} catch (error) { } catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent)); invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
return null;
} finally {
this.#is_creating_fallback = false;
} }
is_creating_fallback = false;
}); });
}); });
} }
}; }
if (hydrating) {
hydrate_next();
} }
boundary_effect = branch(() => boundary_fn(anchor)); /**
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); *
* @param {Effect} effect
* @param {DocumentFragment} fragment
*/
function move_effect(effect, fragment) {
var node = effect.nodes_start;
var end = effect.nodes_end;
if (hydrating) { while (node !== null) {
anchor = hydrate_node; /** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
fragment.append(node);
node = next;
} }
} }

Loading…
Cancel
Save