@pyreon/core 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -54
- package/lib/_chunks/h-CYSD6aBx.js +48 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +59 -56
- package/lib/jsx-dev-runtime.js +3 -97
- package/lib/jsx-runtime.js +1 -45
- package/lib/types/index.d.ts +28 -1
- package/package.json +2 -2
- package/src/component.ts +33 -2
- package/src/context.ts +69 -13
- package/src/error-boundary.ts +7 -1
- package/src/index.ts +1 -0
- package/lib/analysis/jsx-dev-runtime.js.html +0 -5406
- package/lib/analysis/jsx-runtime.js.html +0 -5406
package/lib/jsx-dev-runtime.js
CHANGED
|
@@ -1,98 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Marker for fragment nodes — renders children without a wrapper element.
|
|
4
|
-
*
|
|
5
|
-
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
6
|
-
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
7
|
-
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
8
|
-
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
9
|
-
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
10
|
-
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
11
|
-
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
12
|
-
* fell through to `renderElement` and crashed SSG with
|
|
13
|
-
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
14
|
-
* `Symbol.for()` keys by string in a global registry shared across all
|
|
15
|
-
* bundle evaluations — same identity everywhere.
|
|
16
|
-
*/
|
|
17
|
-
const Fragment = Symbol.for("Pyreon.Fragment");
|
|
18
|
-
/**
|
|
19
|
-
* Hyperscript function — the compiled output of JSX.
|
|
20
|
-
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
21
|
-
*
|
|
22
|
-
* Generic on P so TypeScript validates props match the component's signature
|
|
23
|
-
* at the call site, then stores the result in the loosely-typed VNode.
|
|
24
|
-
*/
|
|
25
|
-
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
26
|
-
const EMPTY_PROPS = {};
|
|
27
|
-
function h(type, props, ...children) {
|
|
28
|
-
return {
|
|
29
|
-
type,
|
|
30
|
-
props: props ?? EMPTY_PROPS,
|
|
31
|
-
children: normalizeChildren(children),
|
|
32
|
-
key: props?.key ?? null
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function normalizeChildren(children) {
|
|
36
|
-
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
37
|
-
return children;
|
|
38
|
-
}
|
|
39
|
-
function flattenChildren(children) {
|
|
40
|
-
const result = [];
|
|
41
|
-
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
42
|
-
else result.push(child);
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
1
|
+
import { n as Fragment } from "./_chunks/h-CYSD6aBx.js";
|
|
2
|
+
import { jsx, jsxs } from "./jsx-runtime.js";
|
|
45
3
|
|
|
46
|
-
|
|
47
|
-
//#region src/jsx-runtime.ts
|
|
48
|
-
/**
|
|
49
|
-
* JSX automatic runtime.
|
|
50
|
-
*
|
|
51
|
-
* When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
|
|
52
|
-
* rewrites JSX to imports from this file automatically:
|
|
53
|
-
* <div class="x" /> → jsx("div", { class: "x" })
|
|
54
|
-
*
|
|
55
|
-
* The triple-slash reference above makes this file self-declare its DOM-lib
|
|
56
|
-
* dependency. Without it, any consumer whose tsconfig has `lib: ["ESNext"]`
|
|
57
|
-
* (no DOM) — e.g. backend-only packages like @pyreon/cli — fails to typecheck
|
|
58
|
-
* once `@pyreon/core` becomes resolvable from their dependency graph (e.g. via
|
|
59
|
-
* a transitive devDep), because tsc auto-resolves jsxImportSource and pulls
|
|
60
|
-
* jsx-runtime.ts into the consumer's compilation unit.
|
|
61
|
-
*/
|
|
62
|
-
function jsx(type, props, key) {
|
|
63
|
-
const descriptors = Object.getOwnPropertyDescriptors(props);
|
|
64
|
-
let hasGetter = false;
|
|
65
|
-
for (const k in descriptors) if (descriptors[k].get) {
|
|
66
|
-
hasGetter = true;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
const children = props.children;
|
|
70
|
-
if (!hasGetter) {
|
|
71
|
-
const { children: _ignored, ...rest } = props;
|
|
72
|
-
const propsWithKey = key != null ? {
|
|
73
|
-
...rest,
|
|
74
|
-
key
|
|
75
|
-
} : rest;
|
|
76
|
-
if (typeof type === "function") return h(type, children !== void 0 ? {
|
|
77
|
-
...propsWithKey,
|
|
78
|
-
children
|
|
79
|
-
} : propsWithKey);
|
|
80
|
-
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
81
|
-
}
|
|
82
|
-
const propsWithKey = {};
|
|
83
|
-
for (const k in descriptors) {
|
|
84
|
-
if (k === "children") continue;
|
|
85
|
-
Object.defineProperty(propsWithKey, k, descriptors[k]);
|
|
86
|
-
}
|
|
87
|
-
if (key != null) propsWithKey.key = key;
|
|
88
|
-
if (typeof type === "function") {
|
|
89
|
-
if (children !== void 0) propsWithKey.children = children;
|
|
90
|
-
return h(type, propsWithKey);
|
|
91
|
-
}
|
|
92
|
-
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
93
|
-
}
|
|
94
|
-
const jsxs = jsx;
|
|
95
|
-
|
|
96
|
-
//#endregion
|
|
97
|
-
export { Fragment, jsx as jsxDEV, jsxs };
|
|
98
|
-
//# sourceMappingURL=jsx-dev-runtime.js.map
|
|
4
|
+
export { Fragment, jsx as jsxDEV, jsxs };
|
package/lib/jsx-runtime.js
CHANGED
|
@@ -1,49 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Marker for fragment nodes — renders children without a wrapper element.
|
|
4
|
-
*
|
|
5
|
-
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
6
|
-
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
7
|
-
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
8
|
-
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
9
|
-
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
10
|
-
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
11
|
-
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
12
|
-
* fell through to `renderElement` and crashed SSG with
|
|
13
|
-
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
14
|
-
* `Symbol.for()` keys by string in a global registry shared across all
|
|
15
|
-
* bundle evaluations — same identity everywhere.
|
|
16
|
-
*/
|
|
17
|
-
const Fragment = Symbol.for("Pyreon.Fragment");
|
|
18
|
-
/**
|
|
19
|
-
* Hyperscript function — the compiled output of JSX.
|
|
20
|
-
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
21
|
-
*
|
|
22
|
-
* Generic on P so TypeScript validates props match the component's signature
|
|
23
|
-
* at the call site, then stores the result in the loosely-typed VNode.
|
|
24
|
-
*/
|
|
25
|
-
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
26
|
-
const EMPTY_PROPS = {};
|
|
27
|
-
function h(type, props, ...children) {
|
|
28
|
-
return {
|
|
29
|
-
type,
|
|
30
|
-
props: props ?? EMPTY_PROPS,
|
|
31
|
-
children: normalizeChildren(children),
|
|
32
|
-
key: props?.key ?? null
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function normalizeChildren(children) {
|
|
36
|
-
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
37
|
-
return children;
|
|
38
|
-
}
|
|
39
|
-
function flattenChildren(children) {
|
|
40
|
-
const result = [];
|
|
41
|
-
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
42
|
-
else result.push(child);
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
1
|
+
import { n as Fragment, r as h } from "./_chunks/h-CYSD6aBx.js";
|
|
45
2
|
|
|
46
|
-
//#endregion
|
|
47
3
|
//#region src/jsx-runtime.ts
|
|
48
4
|
/**
|
|
49
5
|
* JSX automatic runtime.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -247,7 +247,34 @@ declare function createReactiveContext<T>(defaultValue: T): ReactiveContext<T>;
|
|
|
247
247
|
*/
|
|
248
248
|
declare function setContextStackProvider(fn: () => Map<symbol, unknown>[]): void;
|
|
249
249
|
declare function pushContext(values: Map<symbol, unknown>): void;
|
|
250
|
+
/**
|
|
251
|
+
* Pop the LAST frame from the context stack.
|
|
252
|
+
*
|
|
253
|
+
* NOTE: position-based pop. Safe ONLY when the caller can guarantee that the
|
|
254
|
+
* top of the stack is the frame they want to remove (the strict LIFO contract).
|
|
255
|
+
* The `provide()` helper does NOT use this — it uses identity-based removal
|
|
256
|
+
* via `removeContextFrame` because reactive boundaries can push snapshot
|
|
257
|
+
* frames between a component's `provide(ctx, value)` and its eventual
|
|
258
|
+
* unmount, making the top-of-stack unsafe to assume.
|
|
259
|
+
*/
|
|
250
260
|
declare function popContext(): void;
|
|
261
|
+
/**
|
|
262
|
+
* Remove a SPECIFIC frame from the context stack by reference identity.
|
|
263
|
+
*
|
|
264
|
+
* Internal — used by `provide()` and `withContext()` to safely clean up
|
|
265
|
+
* their pushed frame on unmount even when other frames have been pushed
|
|
266
|
+
* between push and pop (e.g. a reactive boundary's `restoreContextStack`
|
|
267
|
+
* pushing snapshot frames during the descendant's lifecycle). The
|
|
268
|
+
* symmetric position-based `popContext()` would pop the wrong frame in
|
|
269
|
+
* that case and orphan the descendant's provider frame on the live stack
|
|
270
|
+
* — the root cause of the 321k-entry context-stack leak under repeated
|
|
271
|
+
* reactive remounts.
|
|
272
|
+
*
|
|
273
|
+
* Uses `lastIndexOf` (LIFO match) — picks the most-recently-pushed frame
|
|
274
|
+
* with that exact reference, so `provide(ctx, a); provide(ctx, b)` followed
|
|
275
|
+
* by two unmounts removes them in reverse order.
|
|
276
|
+
*/
|
|
277
|
+
declare function removeContextFrame(frame: Map<symbol, unknown>): void;
|
|
251
278
|
/**
|
|
252
279
|
* Read the nearest provided value for a context.
|
|
253
280
|
* Falls back to `context.defaultValue` if none found.
|
|
@@ -1416,5 +1443,5 @@ declare function registerErrorHandler(handler: ErrorHandler): () => void;
|
|
|
1416
1443
|
*/
|
|
1417
1444
|
declare function reportError(ctx: ErrorContext): void;
|
|
1418
1445
|
//#endregion
|
|
1419
|
-
export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, Defer, type DeferProps, Dynamic, type DynamicProps, EMPTY_PROPS, ErrorBoundary, type ErrorContext, type ErrorHandler, type ExtractProps, For, type ForProps, ForSymbol, type FormAttributes, Fragment, type HigherOrderComponent, type ImgAttributes, type InputAttributes, type LazyComponent, type LifecycleHooks, Match, type MatchProps, MatchSymbol, NATIVE_COMPAT_MARKER, type NativeItem, Portal, type PortalProps, PortalSymbol, type Props, type PyreonHTMLAttributes, REACTIVE_PROP, type ReactiveContext, type ReactiveTraceEntry, type Ref, type RefCallback, type RefProp, type SelectAttributes, Show, type ShowProps, type StyleValue, Suspense, type SvgAttributes, Switch, type SwitchProps, type TargetedEvent, type TextareaAttributes, type VNode, type VNodeChild, type VNodeChildAccessor, type VNodeChildAtom, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
|
|
1446
|
+
export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, Defer, type DeferProps, Dynamic, type DynamicProps, EMPTY_PROPS, ErrorBoundary, type ErrorContext, type ErrorHandler, type ExtractProps, For, type ForProps, ForSymbol, type FormAttributes, Fragment, type HigherOrderComponent, type ImgAttributes, type InputAttributes, type LazyComponent, type LifecycleHooks, Match, type MatchProps, MatchSymbol, NATIVE_COMPAT_MARKER, type NativeItem, Portal, type PortalProps, PortalSymbol, type Props, type PyreonHTMLAttributes, REACTIVE_PROP, type ReactiveContext, type ReactiveTraceEntry, type Ref, type RefCallback, type RefProp, type SelectAttributes, Show, type ShowProps, type StyleValue, Suspense, type SvgAttributes, Switch, type SwitchProps, type TargetedEvent, type TextareaAttributes, type VNode, type VNodeChild, type VNodeChildAccessor, type VNodeChildAtom, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, removeContextFrame, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
|
|
1420
1447
|
//# sourceMappingURL=index2.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Core component model and lifecycle for Pyreon",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/core#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"prepublishOnly": "bun run build"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@pyreon/reactivity": "^0.
|
|
56
|
+
"@pyreon/reactivity": "^0.23.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@pyreon/manifest": "0.13.1"
|
package/src/component.ts
CHANGED
|
@@ -46,6 +46,22 @@ export function propagateError(err: unknown, hooks: LifecycleHooks): boolean {
|
|
|
46
46
|
// Module-level stack of active ErrorBoundary handlers (innermost last).
|
|
47
47
|
// ErrorBoundary pushes during its own setup (before children mount) so that
|
|
48
48
|
// any child mountComponent error can dispatch up to the nearest boundary.
|
|
49
|
+
//
|
|
50
|
+
// Mutation contract: removal is IDENTITY-based (`lastIndexOf + splice`), not
|
|
51
|
+
// position-based (`pop`). Sibling boundaries unmount in an order that's
|
|
52
|
+
// driven by the renderer (keyed `<For>` reconciliation, `<Show>` flips,
|
|
53
|
+
// route nav), NOT in strict LIFO push order. A position-based `pop()` would
|
|
54
|
+
// remove the wrong frame whenever the unmount order diverges from the push
|
|
55
|
+
// order — the first boundary's `onUnmount` would pop the last boundary's
|
|
56
|
+
// handler, orphaning the first boundary's handler on the stack and removing
|
|
57
|
+
// the surviving boundary's handler from it. Subsequent errors would then
|
|
58
|
+
// route to the orphan (whose owning boundary's signal is already disposed,
|
|
59
|
+
// so the error vanishes silently) and the surviving boundary's children's
|
|
60
|
+
// errors would fall through to whichever boundary happens to sit at
|
|
61
|
+
// `stack[length-1]`. Same root-cause shape as the `popContext()` bug
|
|
62
|
+
// fixed in #725 for `provide()` — see
|
|
63
|
+
// `.claude/rules/anti-patterns.md` "Position-based pop for stack frames
|
|
64
|
+
// that may be pushed by reactive boundaries".
|
|
49
65
|
|
|
50
66
|
const _errorBoundaryStack: ((err: unknown) => boolean)[] = []
|
|
51
67
|
|
|
@@ -53,8 +69,23 @@ export function pushErrorBoundary(handler: (err: unknown) => boolean): void {
|
|
|
53
69
|
_errorBoundaryStack.push(handler)
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Remove a SPECIFIC handler from the error-boundary stack by reference
|
|
74
|
+
* identity. Each `ErrorBoundary` registers `onUnmount(() => popErrorBoundary(handler))`
|
|
75
|
+
* with its OWN handler — so unmount in any order (LIFO, FIFO, middle-out)
|
|
76
|
+
* correctly removes the right handler.
|
|
77
|
+
*/
|
|
78
|
+
export function popErrorBoundary(handler?: (err: unknown) => boolean): void {
|
|
79
|
+
if (handler === undefined) {
|
|
80
|
+
// Back-compat: legacy callers that don't pass a handler get the old
|
|
81
|
+
// pop-last behaviour. Internal `ErrorBoundary` setup always passes
|
|
82
|
+
// its handler now; any external direct callers (tests, advanced
|
|
83
|
+
// consumers) keep working with no-arg form.
|
|
84
|
+
_errorBoundaryStack.pop()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
const idx = _errorBoundaryStack.lastIndexOf(handler)
|
|
88
|
+
if (idx !== -1) _errorBoundaryStack.splice(idx, 1)
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
/**
|
package/src/context.ts
CHANGED
|
@@ -73,12 +73,44 @@ export function pushContext(values: Map<symbol, unknown>) {
|
|
|
73
73
|
getStack().push(values)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Pop the LAST frame from the context stack.
|
|
78
|
+
*
|
|
79
|
+
* NOTE: position-based pop. Safe ONLY when the caller can guarantee that the
|
|
80
|
+
* top of the stack is the frame they want to remove (the strict LIFO contract).
|
|
81
|
+
* The `provide()` helper does NOT use this — it uses identity-based removal
|
|
82
|
+
* via `removeContextFrame` because reactive boundaries can push snapshot
|
|
83
|
+
* frames between a component's `provide(ctx, value)` and its eventual
|
|
84
|
+
* unmount, making the top-of-stack unsafe to assume.
|
|
85
|
+
*/
|
|
76
86
|
export function popContext() {
|
|
77
87
|
const stack = getStack()
|
|
78
88
|
if (stack.length === 0) return
|
|
79
89
|
stack.pop()
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Remove a SPECIFIC frame from the context stack by reference identity.
|
|
94
|
+
*
|
|
95
|
+
* Internal — used by `provide()` and `withContext()` to safely clean up
|
|
96
|
+
* their pushed frame on unmount even when other frames have been pushed
|
|
97
|
+
* between push and pop (e.g. a reactive boundary's `restoreContextStack`
|
|
98
|
+
* pushing snapshot frames during the descendant's lifecycle). The
|
|
99
|
+
* symmetric position-based `popContext()` would pop the wrong frame in
|
|
100
|
+
* that case and orphan the descendant's provider frame on the live stack
|
|
101
|
+
* — the root cause of the 321k-entry context-stack leak under repeated
|
|
102
|
+
* reactive remounts.
|
|
103
|
+
*
|
|
104
|
+
* Uses `lastIndexOf` (LIFO match) — picks the most-recently-pushed frame
|
|
105
|
+
* with that exact reference, so `provide(ctx, a); provide(ctx, b)` followed
|
|
106
|
+
* by two unmounts removes them in reverse order.
|
|
107
|
+
*/
|
|
108
|
+
export function removeContextFrame(frame: Map<symbol, unknown>): void {
|
|
109
|
+
const stack = getStack()
|
|
110
|
+
const idx = stack.lastIndexOf(frame)
|
|
111
|
+
if (idx !== -1) stack.splice(idx, 1)
|
|
112
|
+
}
|
|
113
|
+
|
|
82
114
|
/**
|
|
83
115
|
* Read the nearest provided value for a context.
|
|
84
116
|
* Falls back to `context.defaultValue` if none found.
|
|
@@ -111,8 +143,17 @@ export function useContext<T>(context: Context<T>): T {
|
|
|
111
143
|
* }
|
|
112
144
|
*/
|
|
113
145
|
export function provide<T>(context: Context<T>, value: T): void {
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
const frame = new Map<symbol, unknown>([[context.id, value]])
|
|
147
|
+
pushContext(frame)
|
|
148
|
+
// Identity-based removal — the top of the stack is NOT guaranteed to be
|
|
149
|
+
// this frame at unmount time. Reactive boundaries (`mountReactive`'s
|
|
150
|
+
// effect snapshot-restore + the inner `restoreContextStack` call) push
|
|
151
|
+
// additional snapshot frames during a descendant's lifecycle. A
|
|
152
|
+
// position-based `popContext()` would pop the snapshot frame instead
|
|
153
|
+
// of this provider's frame and orphan the provider on the live stack.
|
|
154
|
+
// See `.claude/rules/anti-patterns.md` "Context-stack frame identity"
|
|
155
|
+
// for the full bug class.
|
|
156
|
+
onUnmount(() => removeContextFrame(frame))
|
|
116
157
|
}
|
|
117
158
|
|
|
118
159
|
/**
|
|
@@ -125,7 +166,11 @@ export function withContext<T>(context: Context<T>, value: T, fn: () => void) {
|
|
|
125
166
|
try {
|
|
126
167
|
fn()
|
|
127
168
|
} finally {
|
|
128
|
-
|
|
169
|
+
// Same identity-based-removal rationale as `provide()` — `fn()` may
|
|
170
|
+
// synchronously trigger a `mountReactive` re-run whose snapshot-restore
|
|
171
|
+
// window leaves the top-of-stack pointing at a snapshot push, not our
|
|
172
|
+
// frame.
|
|
173
|
+
removeContextFrame(frame)
|
|
129
174
|
}
|
|
130
175
|
}
|
|
131
176
|
|
|
@@ -162,7 +207,6 @@ export function captureContextStack(): ContextSnapshot {
|
|
|
162
207
|
*/
|
|
163
208
|
export function restoreContextStack<T>(snapshot: ContextSnapshot, fn: () => T): T {
|
|
164
209
|
const stack = getStack()
|
|
165
|
-
const insertIndex = stack.length
|
|
166
210
|
|
|
167
211
|
// Push captured snapshot frames at the END of the current stack.
|
|
168
212
|
for (const frame of snapshot) {
|
|
@@ -172,15 +216,27 @@ export function restoreContextStack<T>(snapshot: ContextSnapshot, fn: () => T):
|
|
|
172
216
|
try {
|
|
173
217
|
return fn()
|
|
174
218
|
} finally {
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// `
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// snapshot
|
|
183
|
-
|
|
219
|
+
// Remove our pushed snapshot frames by REFERENCE IDENTITY (not by
|
|
220
|
+
// position). `fn()` may legitimately remove frames at indices BEFORE
|
|
221
|
+
// our push window — most commonly via `provide()` registering
|
|
222
|
+
// `onUnmount(removeContextFrame(frame))` and a descendant unmount
|
|
223
|
+
// firing inside this restore window. A position-based `splice` would
|
|
224
|
+
// either pull the wrong frames or no-op when the live stack has
|
|
225
|
+
// shrunk below the original `insertIndex + snapshot.length` —
|
|
226
|
+
// orphaning the snapshot pushes on the live stack and producing the
|
|
227
|
+
// 321k-frame leak reported under repeated reactive remounts.
|
|
228
|
+
//
|
|
229
|
+
// Iterate in reverse so multi-occurrence frames (the same Map ref
|
|
230
|
+
// pushed by multiple nested restores) are removed in LIFO push order.
|
|
231
|
+
// `lastIndexOf` is O(N); N is small in practice (single-digit nesting),
|
|
232
|
+
// and the alternative `findLastIndex(f => f === frame)` is the same
|
|
233
|
+
// cost.
|
|
234
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
235
|
+
const frame = snapshot[i]
|
|
236
|
+
if (!frame) continue
|
|
237
|
+
const idx = stack.lastIndexOf(frame)
|
|
238
|
+
if (idx !== -1) stack.splice(idx, 1)
|
|
239
|
+
}
|
|
184
240
|
}
|
|
185
241
|
}
|
|
186
242
|
|
package/src/error-boundary.ts
CHANGED
|
@@ -68,7 +68,13 @@ export function ErrorBoundary(props: {
|
|
|
68
68
|
|
|
69
69
|
// Push synchronously — before children are mounted — so child errors see this boundary
|
|
70
70
|
pushErrorBoundary(handler)
|
|
71
|
-
|
|
71
|
+
// Identity-based pop: pass our own handler reference. Sibling boundaries
|
|
72
|
+
// can unmount in any order driven by the renderer (keyed `<For>` removal
|
|
73
|
+
// of a non-last item, `<Show>` flipping on the FIRST of N siblings, route
|
|
74
|
+
// nav, etc.) — without passing the handler reference, the position-based
|
|
75
|
+
// `pop()` would remove the WRONG boundary's handler. Same bug class as
|
|
76
|
+
// #725 (`popContext()` orphaning provider frames under reactive remount).
|
|
77
|
+
onUnmount(() => popErrorBoundary(handler))
|
|
72
78
|
|
|
73
79
|
return (): VNodeChildAtom => {
|
|
74
80
|
const err = error()
|