Skip to content

Commit 7914cb1

Browse files
authored
chore: convert boundary implementation to a class (#16284)
* WIP class boundaries * WIP * WIP * WIP * unused * unused * unused
1 parent 49cba86 commit 7914cb1

File tree

4 files changed

+149
-92
lines changed

4 files changed

+149
-92
lines changed
Lines changed: 143 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/** @import { Effect, TemplateNode, } from '#client' */
2-
3-
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
2+
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
43
import { component_context, set_component_context } from '../../context.js';
54
import { invoke_error_boundary } from '../../error-handling.js';
65
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@@ -21,116 +20,170 @@ import {
2120
import { queue_micro_task } from '../task.js';
2221

2322
/**
24-
* @param {Effect} boundary
25-
* @param {() => void} fn
23+
* @typedef {{
24+
* onerror?: (error: unknown, reset: () => void) => void;
25+
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
26+
* }} BoundaryProps
2627
*/
27-
function with_boundary(boundary, fn) {
28-
var previous_effect = active_effect;
29-
var previous_reaction = active_reaction;
30-
var previous_ctx = component_context;
31-
32-
set_active_effect(boundary);
33-
set_active_reaction(boundary);
34-
set_component_context(boundary.ctx);
35-
36-
try {
37-
fn();
38-
} finally {
39-
set_active_effect(previous_effect);
40-
set_active_reaction(previous_reaction);
41-
set_component_context(previous_ctx);
42-
}
43-
}
28+
29+
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
4430

4531
/**
4632
* @param {TemplateNode} node
47-
* @param {{
48-
* onerror?: (error: unknown, reset: () => void) => void,
49-
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
50-
* }} props
51-
* @param {((anchor: Node) => void)} boundary_fn
33+
* @param {BoundaryProps} props
34+
* @param {((anchor: Node) => void)} children
5235
* @returns {void}
5336
*/
54-
export function boundary(node, props, boundary_fn) {
55-
var anchor = node;
37+
export function boundary(node, props, children) {
38+
new Boundary(node, props, children);
39+
}
40+
41+
export class Boundary {
42+
/** @type {TemplateNode} */
43+
#anchor;
44+
45+
/** @type {TemplateNode} */
46+
#hydrate_open;
47+
48+
/** @type {BoundaryProps} */
49+
#props;
50+
51+
/** @type {((anchor: Node) => void)} */
52+
#children;
5653

5754
/** @type {Effect} */
58-
var boundary_effect;
59-
60-
block(() => {
61-
var boundary = /** @type {Effect} */ (active_effect);
62-
var hydrate_open = hydrate_node;
63-
var is_creating_fallback = false;
64-
65-
// We re-use the effect's fn property to avoid allocation of an additional field
66-
boundary.fn = (/** @type {unknown}} */ error) => {
67-
var onerror = props.onerror;
68-
let failed = props.failed;
69-
70-
// If we have nothing to capture the error, or if we hit an error while
71-
// rendering the fallback, re-throw for another boundary to handle
72-
if ((!onerror && !failed) || is_creating_fallback) {
73-
throw error;
74-
}
55+
#effect;
7556

76-
var reset = () => {
77-
pause_effect(boundary_effect);
57+
/** @type {Effect | null} */
58+
#main_effect = null;
7859

79-
with_boundary(boundary, () => {
80-
is_creating_fallback = false;
81-
boundary_effect = branch(() => boundary_fn(anchor));
82-
});
83-
};
60+
/** @type {Effect | null} */
61+
#failed_effect = null;
8462

85-
var previous_reaction = active_reaction;
63+
#is_creating_fallback = false;
8664

87-
try {
88-
set_active_reaction(null);
89-
onerror?.(error, reset);
90-
} finally {
91-
set_active_reaction(previous_reaction);
65+
/**
66+
* @param {TemplateNode} node
67+
* @param {BoundaryProps} props
68+
* @param {((anchor: Node) => void)} children
69+
*/
70+
constructor(node, props, children) {
71+
this.#anchor = node;
72+
this.#props = props;
73+
this.#children = children;
74+
75+
this.#hydrate_open = hydrate_node;
76+
77+
this.#effect = block(() => {
78+
/** @type {Effect} */ (active_effect).b = this;
79+
80+
if (hydrating) {
81+
hydrate_next();
9282
}
9383

94-
if (boundary_effect) {
95-
destroy_effect(boundary_effect);
96-
} else if (hydrating) {
97-
set_hydrate_node(hydrate_open);
98-
next();
99-
set_hydrate_node(remove_nodes());
84+
try {
85+
this.#main_effect = branch(() => children(this.#anchor));
86+
} catch (error) {
87+
this.error(error);
10088
}
89+
}, flags);
10190

102-
if (failed) {
103-
// Render the `failed` snippet in a microtask
104-
queue_micro_task(() => {
105-
with_boundary(boundary, () => {
106-
is_creating_fallback = true;
107-
108-
try {
109-
boundary_effect = branch(() => {
110-
failed(
111-
anchor,
112-
() => error,
113-
() => reset
114-
);
115-
});
116-
} catch (error) {
117-
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent));
118-
}
119-
120-
is_creating_fallback = false;
121-
});
91+
if (hydrating) {
92+
this.#anchor = hydrate_node;
93+
}
94+
}
95+
96+
/**
97+
* @param {() => Effect | null} fn
98+
*/
99+
#run(fn) {
100+
var previous_effect = active_effect;
101+
var previous_reaction = active_reaction;
102+
var previous_ctx = component_context;
103+
104+
set_active_effect(this.#effect);
105+
set_active_reaction(this.#effect);
106+
set_component_context(this.#effect.ctx);
107+
108+
try {
109+
return fn();
110+
} finally {
111+
set_active_effect(previous_effect);
112+
set_active_reaction(previous_reaction);
113+
set_component_context(previous_ctx);
114+
}
115+
}
116+
117+
/** @param {unknown} error */
118+
error(error) {
119+
var onerror = this.#props.onerror;
120+
let failed = this.#props.failed;
121+
122+
const reset = () => {
123+
if (this.#failed_effect !== null) {
124+
pause_effect(this.#failed_effect, () => {
125+
this.#failed_effect = null;
122126
});
123127
}
128+
129+
this.#main_effect = this.#run(() => {
130+
this.#is_creating_fallback = false;
131+
return branch(() => this.#children(this.#anchor));
132+
});
124133
};
125134

126-
if (hydrating) {
127-
hydrate_next();
135+
// If we have nothing to capture the error, or if we hit an error while
136+
// rendering the fallback, re-throw for another boundary to handle
137+
if (this.#is_creating_fallback || (!onerror && !failed)) {
138+
throw error;
139+
}
140+
141+
var previous_reaction = active_reaction;
142+
143+
try {
144+
set_active_reaction(null);
145+
onerror?.(error, reset);
146+
} finally {
147+
set_active_reaction(previous_reaction);
128148
}
129149

130-
boundary_effect = branch(() => boundary_fn(anchor));
131-
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
150+
if (this.#main_effect) {
151+
destroy_effect(this.#main_effect);
152+
this.#main_effect = null;
153+
}
132154

133-
if (hydrating) {
134-
anchor = hydrate_node;
155+
if (this.#failed_effect) {
156+
destroy_effect(this.#failed_effect);
157+
this.#failed_effect = null;
158+
}
159+
160+
if (hydrating) {
161+
set_hydrate_node(this.#hydrate_open);
162+
next();
163+
set_hydrate_node(remove_nodes());
164+
}
165+
166+
if (failed) {
167+
queue_micro_task(() => {
168+
this.#failed_effect = this.#run(() => {
169+
this.#is_creating_fallback = true;
170+
171+
try {
172+
return branch(() => {
173+
failed(
174+
this.#anchor,
175+
() => error,
176+
() => reset
177+
);
178+
});
179+
} catch (error) {
180+
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
181+
return null;
182+
} finally {
183+
this.#is_creating_fallback = false;
184+
}
185+
});
186+
});
187+
}
135188
}
136189
}

packages/svelte/src/internal/client/error-handling.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/** @import { Effect } from '#client' */
2+
/** @import { Boundary } from './dom/blocks/boundary.js' */
23
import { DEV } from 'esm-env';
34
import { FILENAME } from '../../constants.js';
45
import { is_firefox } from './dom/operations.js';
@@ -39,8 +40,7 @@ export function invoke_error_boundary(error, effect) {
3940
while (effect !== null) {
4041
if ((effect.f & BOUNDARY_EFFECT) !== 0) {
4142
try {
42-
// @ts-expect-error
43-
effect.fn(error);
43+
/** @type {Boundary} */ (effect.b).error(error);
4444
return;
4545
} catch {}
4646
}

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ function create_effect(type, fn, sync, push = true) {
104104
last: null,
105105
next: null,
106106
parent,
107+
b: parent && parent.b,
107108
prev: null,
108109
teardown: null,
109110
transitions: null,

packages/svelte/src/internal/client/reactivity/types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
TemplateNode,
66
TransitionManager
77
} from '#client';
8+
import type { Boundary } from '../dom/blocks/boundary';
89

910
export interface Signal {
1011
/** Flags bitmask */
@@ -84,6 +85,8 @@ export interface Effect extends Reaction {
8485
last: null | Effect;
8586
/** Parent effect */
8687
parent: Effect | null;
88+
/** The boundary this effect belongs to */
89+
b: Boundary | null;
8790
/** Dev only */
8891
component_function?: any;
8992
/** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */

0 commit comments

Comments
 (0)