effectable 0.1.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/LICENSE +21 -0
- package/README.md +157 -0
- package/build/LICENSE +21 -0
- package/build/bootstrap/bootstrap.d.ts +32 -0
- package/build/bootstrap/bootstrap.d.ts.map +1 -0
- package/build/bootstrap/bootstrap.js +179 -0
- package/build/bootstrap/bootstrap.js.map +1 -0
- package/build/bootstrap/index.d.ts +9 -0
- package/build/bootstrap/index.d.ts.map +1 -0
- package/build/bootstrap/index.js +14 -0
- package/build/bootstrap/index.js.map +1 -0
- package/build/bootstrap/types.d.ts +105 -0
- package/build/bootstrap/types.d.ts.map +1 -0
- package/build/bootstrap/types.js +26 -0
- package/build/bootstrap/types.js.map +1 -0
- package/build/component/Component.d.ts +126 -0
- package/build/component/Component.d.ts.map +1 -0
- package/build/component/Component.js +123 -0
- package/build/component/Component.js.map +1 -0
- package/build/component/GraphRuntime.d.ts +457 -0
- package/build/component/GraphRuntime.d.ts.map +1 -0
- package/build/component/GraphRuntime.js +1168 -0
- package/build/component/GraphRuntime.js.map +1 -0
- package/build/component/constants.d.ts +1 -0
- package/build/component/constants.d.ts.map +1 -0
- package/build/component/constants.js +2 -0
- package/build/component/constants.js.map +1 -0
- package/build/component/context.d.ts +199 -0
- package/build/component/context.d.ts.map +1 -0
- package/build/component/context.js +262 -0
- package/build/component/context.js.map +1 -0
- package/build/component/graphRuntime.constants.d.ts +13 -0
- package/build/component/graphRuntime.constants.d.ts.map +1 -0
- package/build/component/graphRuntime.constants.js +15 -0
- package/build/component/graphRuntime.constants.js.map +1 -0
- package/build/component/h.d.ts +48 -0
- package/build/component/h.d.ts.map +1 -0
- package/build/component/h.js +72 -0
- package/build/component/h.js.map +1 -0
- package/build/component/index.d.ts +19 -0
- package/build/component/index.d.ts.map +1 -0
- package/build/component/index.js +39 -0
- package/build/component/index.js.map +1 -0
- package/build/component/lifecycle.d.ts +179 -0
- package/build/component/lifecycle.d.ts.map +1 -0
- package/build/component/lifecycle.js +367 -0
- package/build/component/lifecycle.js.map +1 -0
- package/build/component/refs.d.ts +105 -0
- package/build/component/refs.d.ts.map +1 -0
- package/build/component/refs.js +128 -0
- package/build/component/refs.js.map +1 -0
- package/build/component/types.d.ts +169 -0
- package/build/component/types.d.ts.map +1 -0
- package/build/component/types.js +40 -0
- package/build/component/types.js.map +1 -0
- package/build/connect/connect.d.ts +31 -0
- package/build/connect/connect.d.ts.map +1 -0
- package/build/connect/connect.js +500 -0
- package/build/connect/connect.js.map +1 -0
- package/build/connect/index.d.ts +8 -0
- package/build/connect/index.d.ts.map +1 -0
- package/build/connect/index.js +11 -0
- package/build/connect/index.js.map +1 -0
- package/build/connect/types.d.ts +90 -0
- package/build/connect/types.d.ts.map +1 -0
- package/build/connect/types.js +8 -0
- package/build/connect/types.js.map +1 -0
- package/build/index.d.ts +26 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +60 -0
- package/build/index.js.map +1 -0
- package/build/runtime/BusDecorators.d.ts +113 -0
- package/build/runtime/BusDecorators.d.ts.map +1 -0
- package/build/runtime/BusDecorators.js +302 -0
- package/build/runtime/BusDecorators.js.map +1 -0
- package/build/runtime/CommandBus.d.ts +43 -0
- package/build/runtime/CommandBus.d.ts.map +1 -0
- package/build/runtime/CommandBus.js +65 -0
- package/build/runtime/CommandBus.js.map +1 -0
- package/build/runtime/EventBus.d.ts +50 -0
- package/build/runtime/EventBus.d.ts.map +1 -0
- package/build/runtime/EventBus.js +90 -0
- package/build/runtime/EventBus.js.map +1 -0
- package/build/runtime/HandleRegistry.d.ts +143 -0
- package/build/runtime/HandleRegistry.d.ts.map +1 -0
- package/build/runtime/HandleRegistry.js +244 -0
- package/build/runtime/HandleRegistry.js.map +1 -0
- package/build/runtime/QueryBus.d.ts +43 -0
- package/build/runtime/QueryBus.d.ts.map +1 -0
- package/build/runtime/QueryBus.js +65 -0
- package/build/runtime/QueryBus.js.map +1 -0
- package/build/runtime/index.d.ts +13 -0
- package/build/runtime/index.d.ts.map +1 -0
- package/build/runtime/index.js +32 -0
- package/build/runtime/index.js.map +1 -0
- package/build/runtime/types.d.ts +79 -0
- package/build/runtime/types.d.ts.map +1 -0
- package/build/runtime/types.js +8 -0
- package/build/runtime/types.js.map +1 -0
- package/build/store/createStore.d.ts +36 -0
- package/build/store/createStore.d.ts.map +1 -0
- package/build/store/createStore.js +131 -0
- package/build/store/createStore.js.map +1 -0
- package/build/store/index.d.ts +14 -0
- package/build/store/index.d.ts.map +1 -0
- package/build/store/index.js +39 -0
- package/build/store/index.js.map +1 -0
- package/build/store/middleware.d.ts +38 -0
- package/build/store/middleware.d.ts.map +1 -0
- package/build/store/middleware.js +99 -0
- package/build/store/middleware.js.map +1 -0
- package/build/store/selector.d.ts +62 -0
- package/build/store/selector.d.ts.map +1 -0
- package/build/store/selector.js +226 -0
- package/build/store/selector.js.map +1 -0
- package/build/store/semanticStateTree.d.ts +91 -0
- package/build/store/semanticStateTree.d.ts.map +1 -0
- package/build/store/semanticStateTree.js +263 -0
- package/build/store/semanticStateTree.js.map +1 -0
- package/build/store/types.d.ts +259 -0
- package/build/store/types.d.ts.map +1 -0
- package/build/store/types.js +28 -0
- package/build/store/types.js.map +1 -0
- package/package.json +109 -0
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GraphRuntime — runtime engine for materialization, reconcile, and lifecycle of a component tree.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Materialize a VirtualServiceNode tree into real component instances (Fiber tree).
|
|
7
|
+
* - Fiber-like reconcile: diff current vs next trees by key + type, assign effectTags.
|
|
8
|
+
* - Drive lifecycle via LifecycleEngine: startup in topological order (children before parent),
|
|
9
|
+
* shutdown in reverse order (parent before children).
|
|
10
|
+
* - Inject contexts (@UseContext) and bind refs on mount.
|
|
11
|
+
* - Pass updated props into existing instances during reconcile.
|
|
12
|
+
*
|
|
13
|
+
* Current limitations:
|
|
14
|
+
* - Work loop is synchronous (no priority lanes — next increment).
|
|
15
|
+
* - Component.setState() and connect selector updates schedule automatic subtree reconcile
|
|
16
|
+
* via a dirty-fiber queue with microtask coalescing; manual reconcile remains a force-update API.
|
|
17
|
+
* - ContextProvider is handled as a special case in buildScope.
|
|
18
|
+
*
|
|
19
|
+
* @module Effectable/component/GraphRuntime
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.GraphRuntime = void 0;
|
|
23
|
+
exports.makeFiberEffectTag = makeFiberEffectTag;
|
|
24
|
+
const types_1 = require("./types");
|
|
25
|
+
const lifecycle_1 = require("./lifecycle");
|
|
26
|
+
const context_1 = require("./context");
|
|
27
|
+
const BusDecorators_1 = require("../runtime/BusDecorators");
|
|
28
|
+
const graphRuntime_constants_1 = require("./graphRuntime.constants");
|
|
29
|
+
/**
|
|
30
|
+
* Checks whether a value is thenable (Promise-like).
|
|
31
|
+
* Key to GraphRuntime sync fast-path: `materialize`/`reconcileFiber`/`destroyFiber`
|
|
32
|
+
* return union `T | Promise<T>`; the caller routes via this helper
|
|
33
|
+
* (116x / 266x on pure sync trees).
|
|
34
|
+
*
|
|
35
|
+
* @param {unknown} value - value under test
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
function isThenable(value) {
|
|
39
|
+
return (value !== null &&
|
|
40
|
+
typeof value === 'object' &&
|
|
41
|
+
typeof value.then === 'function');
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// GraphRuntime
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Runtime engine for a declarative component tree.
|
|
48
|
+
*
|
|
49
|
+
* Usage:
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const runtime = await GraphRuntime.mount(h(AppRoot));
|
|
52
|
+
* await runtime.reconcile(h(AppRoot, { newProp: 1 }));
|
|
53
|
+
* await runtime.unmount();
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
class GraphRuntime {
|
|
57
|
+
/** Current root fiber tree (current tree). */
|
|
58
|
+
currentRoot = null;
|
|
59
|
+
/** Whether unmount has completed. */
|
|
60
|
+
unmounted = false;
|
|
61
|
+
/**
|
|
62
|
+
* Entry counter for {@link continueStableReconcileAsync} (test/debug probe).
|
|
63
|
+
* Not reset automatically — compare before/after around reconcile.
|
|
64
|
+
*/
|
|
65
|
+
stableAsyncContinueCount = 0;
|
|
66
|
+
/**
|
|
67
|
+
* Runtime buses for auto-wiring decorators on nodes (optional, set in {@link GraphRuntime.mount}).
|
|
68
|
+
*/
|
|
69
|
+
effectableRuntimeBuses = null;
|
|
70
|
+
/**
|
|
71
|
+
* Depth-indexed pool of Map objects for keyedCurrentMap in reconcileChildren.
|
|
72
|
+
* Index is recursion depth, which provides re-entrancy safety for nested calls.
|
|
73
|
+
* Map.clear() instead of new Map() yields 5.1x speedup.
|
|
74
|
+
*/
|
|
75
|
+
keyedMapPool = [];
|
|
76
|
+
/**
|
|
77
|
+
* Current reconcileChildren recursion depth.
|
|
78
|
+
* Incremented before async work, decremented in finally.
|
|
79
|
+
*/
|
|
80
|
+
reconcileDepth = 0;
|
|
81
|
+
/**
|
|
82
|
+
* Set of fiber nodes whose `compose()` subtrees need rebuild.
|
|
83
|
+
* Holds only a “minimal cover”: if an ancestor is already in the set, a descendant is not added.
|
|
84
|
+
*/
|
|
85
|
+
dirtyFibers = new Set();
|
|
86
|
+
/** `true` — a microtask flush is already queued. */
|
|
87
|
+
flushScheduled = false;
|
|
88
|
+
/** `true` — automatic dirty-fiber flush is in progress (re-entrancy guard). */
|
|
89
|
+
flushing = false;
|
|
90
|
+
/**
|
|
91
|
+
* Promise of the current/scheduled dirty-flush (microtask).
|
|
92
|
+
* Manual `reconcile` awaits it to avoid overlapping an in-flight flush.
|
|
93
|
+
*/
|
|
94
|
+
activeFlush = null;
|
|
95
|
+
/**
|
|
96
|
+
* Number of consecutive dirty-flush passes in the current microtask chain.
|
|
97
|
+
* Reset when the queue is empty after a pass or on manual `reconcile`.
|
|
98
|
+
*/
|
|
99
|
+
dirtyFlushPassCount = 0;
|
|
100
|
+
/**
|
|
101
|
+
* Optional hook invoked when automatic reconcile (dirty-fiber flush) fails.
|
|
102
|
+
* Set via the fourth argument of {@link GraphRuntime.mount}.
|
|
103
|
+
*/
|
|
104
|
+
onAutoReconcileError = null;
|
|
105
|
+
/**
|
|
106
|
+
* Instances are created only via {@link GraphRuntime.mount}; direct `new GraphRuntime()` is unavailable externally.
|
|
107
|
+
*/
|
|
108
|
+
constructor() { }
|
|
109
|
+
/**
|
|
110
|
+
* Auto-wires runtime-bus decorators onto the instance before {@link LifecycleEngine.runStartup}.
|
|
111
|
+
*
|
|
112
|
+
* @template P
|
|
113
|
+
* @param {Component<unknown, P>} instance - node instance
|
|
114
|
+
* @param {RuntimeFiber<P>} fiber - node fiber (stores disposer)
|
|
115
|
+
* @returns {void}
|
|
116
|
+
*/
|
|
117
|
+
attachEffectableRuntimeBusWiring(instance, fiber) {
|
|
118
|
+
if (this.effectableRuntimeBuses === null) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const disposer = (0, BusDecorators_1.wireRuntimeBusesIfDecorated)(instance, this.effectableRuntimeBuses);
|
|
122
|
+
if (disposer !== null) {
|
|
123
|
+
fiber.effectableRuntimeBusDisposer = disposer;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Removes runtime-bus registrations created by {@link attachEffectableRuntimeBusWiring}.
|
|
128
|
+
*
|
|
129
|
+
* @param {RuntimeFiber<unknown>} fiber - node fiber
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
disposeEffectableRuntimeBusWiring(fiber) {
|
|
133
|
+
const disposer = fiber.effectableRuntimeBusDisposer;
|
|
134
|
+
if (typeof disposer === 'function') {
|
|
135
|
+
disposer();
|
|
136
|
+
}
|
|
137
|
+
delete fiber.effectableRuntimeBusDisposer;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Injects the scheduler hook onto the component instance after successful startup.
|
|
141
|
+
* The hook is called from {@link Component.setState} and enqueues the fiber for automatic reconcile.
|
|
142
|
+
*
|
|
143
|
+
* If `setState` ran during startup (pre-mount buffer), schedules reconcile immediately.
|
|
144
|
+
*
|
|
145
|
+
* @param {Component<unknown, unknown>} instance - mounted instance
|
|
146
|
+
* @param {RuntimeFiber<unknown>} fiber - instance fiber (captured in the closure)
|
|
147
|
+
* @returns {void}
|
|
148
|
+
*/
|
|
149
|
+
injectUpdateHook(instance, fiber) {
|
|
150
|
+
instance[types_1.SCHEDULE_UPDATE_HOOK] = () => {
|
|
151
|
+
this.scheduleUpdate(fiber);
|
|
152
|
+
};
|
|
153
|
+
if (fiber.pendingScheduleUpdate === true) {
|
|
154
|
+
fiber.pendingScheduleUpdate = false;
|
|
155
|
+
this.scheduleUpdate(fiber);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Pre-mount buffer: `setState` during `onMount` cannot yet schedule reconcile
|
|
160
|
+
* (the live hook is injected after startup). Marks the fiber; {@link injectUpdateHook}
|
|
161
|
+
* after startup will call {@link scheduleUpdate}
|
|
162
|
+
* (deferred until the mount pass completes).
|
|
163
|
+
*
|
|
164
|
+
* @param {Component<unknown, unknown>} instance - instance before/during startup
|
|
165
|
+
* @param {RuntimeFiber<unknown>} fiber - instance fiber
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
injectPreMountUpdateHook(instance, fiber) {
|
|
169
|
+
fiber.pendingScheduleUpdate = false;
|
|
170
|
+
instance[types_1.SCHEDULE_UPDATE_HOOK] = () => {
|
|
171
|
+
fiber.pendingScheduleUpdate = true;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Removes the scheduler hook from the instance before unmount.
|
|
176
|
+
* After removal, `setState()` no longer triggers automatic reconcile.
|
|
177
|
+
*
|
|
178
|
+
* @param {Component<unknown, unknown>} instance - instance being unmounted
|
|
179
|
+
* @returns {void}
|
|
180
|
+
*/
|
|
181
|
+
clearUpdateHook(instance) {
|
|
182
|
+
delete instance[types_1.SCHEDULE_UPDATE_HOOK];
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Adds a fiber to the dirty queue with ancestor deduplication and schedules a microtask flush.
|
|
186
|
+
*
|
|
187
|
+
* Deduplication:
|
|
188
|
+
* - If an ancestor of the fiber is already queued → skip (ancestor covers the subtree).
|
|
189
|
+
* - If descendants of the fiber are queued → remove them (fiber covers their subtrees).
|
|
190
|
+
*
|
|
191
|
+
* @param {RuntimeFiber<unknown>} fiber - fiber whose subtree needs rebuild
|
|
192
|
+
* @returns {void}
|
|
193
|
+
*/
|
|
194
|
+
scheduleUpdate(fiber) {
|
|
195
|
+
if (this.unmounted) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// If any ancestor is already queued — this fiber will be rebuilt as part of the ancestor
|
|
199
|
+
let ancestor = fiber.parentFiber;
|
|
200
|
+
while (ancestor !== null) {
|
|
201
|
+
if (this.dirtyFibers.has(ancestor)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
ancestor = ancestor.parentFiber;
|
|
205
|
+
}
|
|
206
|
+
// Remove descendants now covered by this fiber
|
|
207
|
+
if (this.dirtyFibers.size > 0) {
|
|
208
|
+
for (const existing of this.dirtyFibers) {
|
|
209
|
+
let p = existing.parentFiber;
|
|
210
|
+
while (p !== null) {
|
|
211
|
+
if (p === fiber) {
|
|
212
|
+
this.dirtyFibers.delete(existing);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
p = p.parentFiber;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Add fiber to the dirty queue
|
|
220
|
+
this.dirtyFibers.add(fiber);
|
|
221
|
+
// Schedule microtask dirty-flush
|
|
222
|
+
this.scheduleDirtyFlushMicrotask();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Queues one dirty-flush microtask and publishes {@link activeFlush} for await from `reconcile`.
|
|
226
|
+
*
|
|
227
|
+
* @returns {void}
|
|
228
|
+
*/
|
|
229
|
+
scheduleDirtyFlushMicrotask() {
|
|
230
|
+
if (this.flushScheduled || this.unmounted) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.flushScheduled = true;
|
|
234
|
+
const flushWork = new Promise((resolve) => {
|
|
235
|
+
queueMicrotask(() => {
|
|
236
|
+
this.flushDirtyFibers()
|
|
237
|
+
.catch((err) => {
|
|
238
|
+
if (this.onAutoReconcileError !== null) {
|
|
239
|
+
this.onAutoReconcileError(err);
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
.finally(() => {
|
|
243
|
+
resolve();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
const trackedFlush = flushWork.finally(() => {
|
|
248
|
+
if (this.activeFlush === trackedFlush) {
|
|
249
|
+
this.activeFlush = null;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
this.activeFlush = trackedFlush;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Flushes accumulated dirty fibers.
|
|
256
|
+
* Called from the microtask queued by {@link scheduleUpdate}.
|
|
257
|
+
* Guarded by the `flushing` flag against re-entrancy.
|
|
258
|
+
* The chain of repeat passes is capped by {@link GRAPH_RUNTIME_MAX_DIRTY_FLUSH_PASSES}.
|
|
259
|
+
*
|
|
260
|
+
* @returns {Promise<void>}
|
|
261
|
+
*/
|
|
262
|
+
async flushDirtyFibers() {
|
|
263
|
+
this.flushScheduled = false;
|
|
264
|
+
if (this.unmounted || this.flushing) {
|
|
265
|
+
this.dirtyFibers.clear();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.dirtyFlushPassCount += 1;
|
|
269
|
+
this.flushing = true;
|
|
270
|
+
const snapshot = Array.from(this.dirtyFibers);
|
|
271
|
+
this.dirtyFibers.clear();
|
|
272
|
+
try {
|
|
273
|
+
for (const fiber of snapshot) {
|
|
274
|
+
if (this.unmounted) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
const res = this.reconcileDirtyFiber(fiber);
|
|
278
|
+
if (isThenable(res)) {
|
|
279
|
+
await res;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
this.flushing = false;
|
|
285
|
+
}
|
|
286
|
+
// If new dirty fibers appeared during flush — schedule the next pass
|
|
287
|
+
if (this.dirtyFibers.size > 0 && !this.unmounted) {
|
|
288
|
+
if (this.dirtyFlushPassCount >= graphRuntime_constants_1.GRAPH_RUNTIME_MAX_DIRTY_FLUSH_PASSES) {
|
|
289
|
+
this.dirtyFibers.clear();
|
|
290
|
+
this.dirtyFlushPassCount = 0;
|
|
291
|
+
throw new Error(`GraphRuntime: dirty flush exceeded ${String(graphRuntime_constants_1.GRAPH_RUNTIME_MAX_DIRTY_FLUSH_PASSES)} passes (anti-loop)`);
|
|
292
|
+
}
|
|
293
|
+
this.scheduleDirtyFlushMicrotask();
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.dirtyFlushPassCount = 0;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Rebuilds the `compose()` subtree of one dirty fiber without updating its props.
|
|
301
|
+
*
|
|
302
|
+
* Used for automatic reconcile after `setState()`: the instance already updated `state`,
|
|
303
|
+
* we only need to call `compose()` again and diff children.
|
|
304
|
+
*
|
|
305
|
+
* @param {RuntimeFiber<unknown>} fiber - fiber whose subtree needs rebuild
|
|
306
|
+
* @returns {void | Promise<void>}
|
|
307
|
+
*/
|
|
308
|
+
reconcileDirtyFiber(fiber) {
|
|
309
|
+
if (this.unmounted) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const instance = fiber.instance;
|
|
313
|
+
if (instance === null) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
const childScope = this.buildChildScope(instance, fiber.scope);
|
|
318
|
+
const nextChildVnodes = this.getChildVnodes(instance, fiber.vnode.children);
|
|
319
|
+
const childrenRes = this.reconcileChildren(fiber.children, nextChildVnodes, fiber, childScope);
|
|
320
|
+
if (isThenable(childrenRes)) {
|
|
321
|
+
return childrenRes.then((nextChildren) => {
|
|
322
|
+
fiber.children = nextChildren;
|
|
323
|
+
}, (error) => {
|
|
324
|
+
const cleanupResult = this.runFiberFailedCleanup(fiber);
|
|
325
|
+
if (isThenable(cleanupResult)) {
|
|
326
|
+
return cleanupResult.then(() => {
|
|
327
|
+
throw error;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
fiber.children = childrenRes;
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
const cleanupResult = this.runFiberFailedCleanup(fiber);
|
|
337
|
+
if (isThenable(cleanupResult)) {
|
|
338
|
+
return cleanupResult.then(() => {
|
|
339
|
+
throw error;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Public API
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
/**
|
|
349
|
+
* Mounts a component tree and returns a running GraphRuntime.
|
|
350
|
+
* Builds the fiber tree, injects contexts, runs lifecycle in order:
|
|
351
|
+
* child nodes first, then the parent.
|
|
352
|
+
*
|
|
353
|
+
* @param {VirtualServiceNode<P>} root - root virtual node
|
|
354
|
+
* @param {ContextScope} initialScope - initial context scope (empty by default)
|
|
355
|
+
* @returns {Promise<GraphRuntime>} running runtime
|
|
356
|
+
* @throws {Error} if any node's startup failed
|
|
357
|
+
*/
|
|
358
|
+
static async mount(root, initialScope = context_1.EMPTY_CONTEXT_SCOPE, runtimeBuses, onAutoReconcileError) {
|
|
359
|
+
const rt = new GraphRuntime();
|
|
360
|
+
rt.effectableRuntimeBuses = typeof runtimeBuses === 'undefined' ? null : runtimeBuses;
|
|
361
|
+
rt.onAutoReconcileError = typeof onAutoReconcileError === 'function' ? onAutoReconcileError : null;
|
|
362
|
+
const res = rt.materialize(root, null, initialScope);
|
|
363
|
+
rt.currentRoot = isThenable(res) ? await res : res;
|
|
364
|
+
return rt;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Reconciles against a new tree.
|
|
368
|
+
* Builds a work-in-progress tree, computes effectTags, applies changes:
|
|
369
|
+
* - PLACE: create and mount a new node
|
|
370
|
+
* - UPDATE: update props on an existing instance, call onUpdate
|
|
371
|
+
* - DELETE: unmount and destroy a node
|
|
372
|
+
*
|
|
373
|
+
* @param {VirtualServiceNode<P>} nextTree - new virtual tree
|
|
374
|
+
* @returns {Promise<void>}
|
|
375
|
+
* @throws {Error} if the runtime is already unmounted
|
|
376
|
+
*/
|
|
377
|
+
async reconcile(nextTree) {
|
|
378
|
+
if (this.unmounted) {
|
|
379
|
+
throw new Error('[Effectable] GraphRuntime: reconcile attempted after unmount.');
|
|
380
|
+
}
|
|
381
|
+
if (this.currentRoot === null) {
|
|
382
|
+
throw new Error('[Effectable] GraphRuntime: currentRoot is not initialized.');
|
|
383
|
+
}
|
|
384
|
+
// Await the full dirty-flush chain (including re-schedule) — otherwise manual
|
|
385
|
+
// reconcile overlaps the snapshot auto-flush.
|
|
386
|
+
while (this.activeFlush !== null) {
|
|
387
|
+
await this.activeFlush;
|
|
388
|
+
}
|
|
389
|
+
if (this.unmounted) {
|
|
390
|
+
throw new Error('[Effectable] GraphRuntime: reconcile attempted after unmount.');
|
|
391
|
+
}
|
|
392
|
+
// Manual reconcile covers the whole tree from the root: cancel pending auto-flush
|
|
393
|
+
// to avoid double-mounting components from concurrent reconcile paths.
|
|
394
|
+
this.dirtyFibers.clear();
|
|
395
|
+
this.flushScheduled = false;
|
|
396
|
+
this.dirtyFlushPassCount = 0;
|
|
397
|
+
const res = this.reconcileFiber(this.currentRoot, nextTree, null, this.currentRoot.scope);
|
|
398
|
+
this.currentRoot = isThenable(res) ? await res : res;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Fully unmounts the component tree.
|
|
402
|
+
* Calls onUnmount for each node in reverse order (children before parent) and
|
|
403
|
+
* moves stages to destroyed via LifecycleEngine.
|
|
404
|
+
*
|
|
405
|
+
* @returns {Promise<void>}
|
|
406
|
+
*/
|
|
407
|
+
async unmount() {
|
|
408
|
+
if (this.unmounted) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.unmounted = true;
|
|
412
|
+
if (this.currentRoot !== null) {
|
|
413
|
+
const d = this.destroyFiber(this.currentRoot);
|
|
414
|
+
if (isThenable(d)) {
|
|
415
|
+
await d;
|
|
416
|
+
}
|
|
417
|
+
this.currentRoot = null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Returns the root component instance (for testing and introspection).
|
|
422
|
+
*
|
|
423
|
+
* @returns {Component<unknown, unknown> | null}
|
|
424
|
+
*/
|
|
425
|
+
getRootInstance() {
|
|
426
|
+
if (this.currentRoot === null) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
return this.currentRoot.instance;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Whether the runtime is active (unmount has not been called).
|
|
433
|
+
*
|
|
434
|
+
* @returns {boolean}
|
|
435
|
+
*/
|
|
436
|
+
isActive() {
|
|
437
|
+
return !this.unmounted;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Readonly snapshot of the root fiber tree for test/debug introspection.
|
|
441
|
+
* Does not export mutable RuntimeFiber; returns null after unmount.
|
|
442
|
+
*
|
|
443
|
+
* @returns {FiberInspectNode | null} root snapshot or null
|
|
444
|
+
*/
|
|
445
|
+
inspectRootFiber() {
|
|
446
|
+
if (this.currentRoot === null) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
return this.toFiberInspectNode(this.currentRoot);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Entity tests only: nulls the root fiber `instance`
|
|
453
|
+
* to exercise the UPDATE guard when `fiber.instance === null`.
|
|
454
|
+
* Do not use in the production control plane.
|
|
455
|
+
*
|
|
456
|
+
* @returns {void}
|
|
457
|
+
*/
|
|
458
|
+
nullRootInstanceForTests() {
|
|
459
|
+
if (this.currentRoot === null) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.currentRoot.instance = null;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Number of {@link continueStableReconcileAsync} calls since runtime creation.
|
|
466
|
+
* Test/debug probe; not a production API.
|
|
467
|
+
*
|
|
468
|
+
* @returns {number} accumulated counter
|
|
469
|
+
*/
|
|
470
|
+
getStableAsyncContinueCount() {
|
|
471
|
+
return this.stableAsyncContinueCount;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Builds a deep readonly {@link FiberInspectNode} from a RuntimeFiber.
|
|
475
|
+
*
|
|
476
|
+
* @param {RuntimeFiber} fiber - source fiber
|
|
477
|
+
* @returns {FiberInspectNode} node snapshot
|
|
478
|
+
*/
|
|
479
|
+
toFiberInspectNode(fiber) {
|
|
480
|
+
const children = [];
|
|
481
|
+
for (let i = 0; i < fiber.children.length; i++) {
|
|
482
|
+
children.push(this.toFiberInspectNode(fiber.children[i]));
|
|
483
|
+
}
|
|
484
|
+
const keyRaw = fiber.vnode.key;
|
|
485
|
+
const key = keyRaw === undefined ? null : keyRaw;
|
|
486
|
+
return {
|
|
487
|
+
effectTag: fiber.effectTag,
|
|
488
|
+
hasInstance: fiber.instance !== null,
|
|
489
|
+
key,
|
|
490
|
+
childCount: fiber.children.length,
|
|
491
|
+
children,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// Materialize
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
/**
|
|
498
|
+
* Creates a RuntimeFiber for a virtual node: instantiates the component,
|
|
499
|
+
* injects contexts, builds the child scope (for ContextProvider),
|
|
500
|
+
* recursively materializes children, binds ref, runs lifecycle.
|
|
501
|
+
*
|
|
502
|
+
* Returns {@link RuntimeFiber} synchronously if the whole subtree is sync
|
|
503
|
+
* (up to 116x speedup for a tree of 16 sync children); otherwise a Promise.
|
|
504
|
+
* `await` works correctly with either union branch.
|
|
505
|
+
*
|
|
506
|
+
* @param {VirtualServiceNode<P>} vnode - virtual node
|
|
507
|
+
* @param {RuntimeFiber | null} parentFiber - parent fiber
|
|
508
|
+
* @param {ContextScope} parentScope - parent scope
|
|
509
|
+
* @returns {RuntimeFiber<P> | Promise<RuntimeFiber<P>>}
|
|
510
|
+
*/
|
|
511
|
+
materialize(vnode, parentFiber, parentScope) {
|
|
512
|
+
const engine = new lifecycle_1.LifecycleEngine();
|
|
513
|
+
// Constructor is stored as ComponentConstructor<unknown> for covariance,
|
|
514
|
+
// but invoked with concrete props P. The cast is safe: P extends unknown.
|
|
515
|
+
const instance = new vnode.type(vnode.props);
|
|
516
|
+
// Cache hookFlags once for fast-exit in runStartup/runShutdown (1.15x)
|
|
517
|
+
engine.initHookFlags(instance);
|
|
518
|
+
// Inject @UseContext fields before startup
|
|
519
|
+
(0, context_1.injectContextFields)(instance, parentScope);
|
|
520
|
+
// Build scope for child nodes: ContextProvider extends the scope
|
|
521
|
+
const childScope = this.buildChildScope(instance, parentScope);
|
|
522
|
+
const fiber = {
|
|
523
|
+
vnode,
|
|
524
|
+
instance,
|
|
525
|
+
lifecycleStatus: 'registered',
|
|
526
|
+
children: [],
|
|
527
|
+
parentFiber: parentFiber,
|
|
528
|
+
alternate: null,
|
|
529
|
+
pendingProps: null,
|
|
530
|
+
effectTag: types_1.FIBER_EFFECT_TAG.PLACE,
|
|
531
|
+
engine,
|
|
532
|
+
scope: parentScope,
|
|
533
|
+
};
|
|
534
|
+
// Recursively materialize children before running the parent's lifecycle
|
|
535
|
+
const childVnodes = this.getChildVnodes(instance, vnode.children);
|
|
536
|
+
const childFibers = [];
|
|
537
|
+
for (let i = 0; i < childVnodes.length; i++) {
|
|
538
|
+
const childVnode = childVnodes[i];
|
|
539
|
+
const childRes = this.materialize(childVnode, fiber, childScope);
|
|
540
|
+
if (isThenable(childRes)) {
|
|
541
|
+
// Hit an async child — continue the materialization tail in the async continuation.
|
|
542
|
+
return this.continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childFibers, childRes, i);
|
|
543
|
+
}
|
|
544
|
+
childFibers.push(childRes);
|
|
545
|
+
}
|
|
546
|
+
fiber.children = childFibers;
|
|
547
|
+
// Bind ref to the instance
|
|
548
|
+
if (vnode.ref !== undefined) {
|
|
549
|
+
vnode.ref.current = instance;
|
|
550
|
+
}
|
|
551
|
+
this.attachEffectableRuntimeBusWiring(instance, fiber);
|
|
552
|
+
// Run lifecycle after all children are materialized.
|
|
553
|
+
// Pre-mount hook buffers setState from onMount until injectUpdateHook.
|
|
554
|
+
this.injectPreMountUpdateHook(instance, fiber);
|
|
555
|
+
const startupRes = engine.runStartup(instance);
|
|
556
|
+
if (isThenable(startupRes)) {
|
|
557
|
+
return this.finalizeMaterializeAsync(fiber, engine, childFibers, startupRes);
|
|
558
|
+
}
|
|
559
|
+
if (!startupRes.ok) {
|
|
560
|
+
// Unmount already-mounted children when parent startup fails
|
|
561
|
+
const destroyChain = this.destroyChildrenOnError(childFibers);
|
|
562
|
+
if (isThenable(destroyChain)) {
|
|
563
|
+
return destroyChain.then(() => {
|
|
564
|
+
throw startupRes.error;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
throw startupRes.error;
|
|
568
|
+
}
|
|
569
|
+
fiber.lifecycleStatus = engine.getStatus();
|
|
570
|
+
fiber.effectTag = null;
|
|
571
|
+
this.injectUpdateHook(instance, fiber);
|
|
572
|
+
return fiber;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Async continuation of {@link materialize} after a child returned a Promise.
|
|
576
|
+
* Finishes remaining child fibers with `await`, then runs parent startup.
|
|
577
|
+
*
|
|
578
|
+
* @param {RuntimeFiber<P>} fiber - current fiber
|
|
579
|
+
* @param {Component<unknown, P>} instance - component instance
|
|
580
|
+
* @param {LifecycleEngine} engine - lifecycle engine
|
|
581
|
+
* @param {VirtualServiceNode<P>} vnode - virtual node
|
|
582
|
+
* @param {VirtualServiceNode[]} childVnodes - all child vnodes
|
|
583
|
+
* @param {RuntimeFiber<unknown>[]} childFibers - already materialized child fibers
|
|
584
|
+
* @param {Promise<RuntimeFiber<unknown>>} pending - Promise for the current child
|
|
585
|
+
* @param {number} pendingIdx - index of the current child
|
|
586
|
+
* @returns {Promise<RuntimeFiber<P>>}
|
|
587
|
+
*/
|
|
588
|
+
async continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childFibers, pending, pendingIdx) {
|
|
589
|
+
const childScope = this.buildChildScope(instance, fiber.scope);
|
|
590
|
+
childFibers.push(await pending);
|
|
591
|
+
for (let i = pendingIdx + 1; i < childVnodes.length; i++) {
|
|
592
|
+
const childVnode = childVnodes[i];
|
|
593
|
+
const childRes = this.materialize(childVnode, fiber, childScope);
|
|
594
|
+
childFibers.push(isThenable(childRes) ? await childRes : childRes);
|
|
595
|
+
}
|
|
596
|
+
fiber.children = childFibers;
|
|
597
|
+
if (vnode.ref !== undefined) {
|
|
598
|
+
vnode.ref.current = instance;
|
|
599
|
+
}
|
|
600
|
+
this.attachEffectableRuntimeBusWiring(instance, fiber);
|
|
601
|
+
this.injectPreMountUpdateHook(instance, fiber);
|
|
602
|
+
const startupRes = engine.runStartup(instance);
|
|
603
|
+
const resolved = isThenable(startupRes) ? await startupRes : startupRes;
|
|
604
|
+
if (!resolved.ok) {
|
|
605
|
+
for (const c of childFibers) {
|
|
606
|
+
const d = this.destroyFiber(c);
|
|
607
|
+
if (isThenable(d)) {
|
|
608
|
+
await d;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
throw resolved.error;
|
|
612
|
+
}
|
|
613
|
+
fiber.lifecycleStatus = engine.getStatus();
|
|
614
|
+
fiber.effectTag = null;
|
|
615
|
+
this.injectUpdateHook(instance, fiber);
|
|
616
|
+
return fiber;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Async finalization of {@link materialize} when child fibers were gathered synchronously but {@link LifecycleEngine.runStartup} returned a Promise.
|
|
620
|
+
*
|
|
621
|
+
* @template P node props type
|
|
622
|
+
* @param {RuntimeFiber<P>} fiber - fiber of the subtree root node
|
|
623
|
+
* @param {LifecycleEngine} engine - lifecycle engine for this node
|
|
624
|
+
* @param {RuntimeFiber<unknown>[]} childFibers - already mounted child fibers (for rollback on error)
|
|
625
|
+
* @param {PromiseLike<import('./lifecycle').LifecycleTransitionResult>} pendingStartup - Promise of the `runStartup` result
|
|
626
|
+
* @returns {Promise<RuntimeFiber<P>>} ready fiber, or rollback children and rethrow
|
|
627
|
+
*/
|
|
628
|
+
async finalizeMaterializeAsync(fiber, engine, childFibers, pendingStartup) {
|
|
629
|
+
const result = await pendingStartup;
|
|
630
|
+
if (!result.ok) {
|
|
631
|
+
for (const c of childFibers) {
|
|
632
|
+
const d = this.destroyFiber(c);
|
|
633
|
+
if (isThenable(d)) {
|
|
634
|
+
await d;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
throw result.error;
|
|
638
|
+
}
|
|
639
|
+
fiber.lifecycleStatus = engine.getStatus();
|
|
640
|
+
fiber.effectTag = null;
|
|
641
|
+
if (fiber.instance !== null) {
|
|
642
|
+
this.injectUpdateHook(fiber.instance, fiber);
|
|
643
|
+
}
|
|
644
|
+
return fiber;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Unmounts already-mounted children when parent startup fails.
|
|
648
|
+
* Returns sync void if all demounts are sync, otherwise a Promise.
|
|
649
|
+
*
|
|
650
|
+
* @param {RuntimeFiber<unknown>[]} childFibers
|
|
651
|
+
* @returns {void | Promise<void>}
|
|
652
|
+
*/
|
|
653
|
+
destroyChildrenOnError(childFibers) {
|
|
654
|
+
let pending = null;
|
|
655
|
+
let startIdx = 0;
|
|
656
|
+
for (let i = 0; i < childFibers.length; i++) {
|
|
657
|
+
const d = this.destroyFiber(childFibers[i]);
|
|
658
|
+
if (isThenable(d)) {
|
|
659
|
+
pending = d;
|
|
660
|
+
startIdx = i + 1;
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (pending === null) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
return (async () => {
|
|
668
|
+
await pending;
|
|
669
|
+
for (let i = startIdx; i < childFibers.length; i++) {
|
|
670
|
+
const d = this.destroyFiber(childFibers[i]);
|
|
671
|
+
if (isThenable(d)) {
|
|
672
|
+
await d;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
})();
|
|
676
|
+
}
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// Reconcile
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
/**
|
|
681
|
+
* Runs fiber-like reconcile for a single node.
|
|
682
|
+
* If type and key match — UPDATE: update props, reconcile children.
|
|
683
|
+
* If they differ — DELETE the old node, PLACE the new one.
|
|
684
|
+
*
|
|
685
|
+
* @param {RuntimeFiber<P>} current - current fiber
|
|
686
|
+
* @param {VirtualServiceNode<P>} nextVnode - new virtual node
|
|
687
|
+
* @param {RuntimeFiber | null} parentFiber - parent fiber
|
|
688
|
+
* @param {ContextScope} parentScope - parent scope
|
|
689
|
+
* @returns {Promise<RuntimeFiber<P>>}
|
|
690
|
+
*/
|
|
691
|
+
reconcileFiber(current, nextVnode, parentFiber, parentScope) {
|
|
692
|
+
const sameType = current.vnode.type === nextVnode.type;
|
|
693
|
+
const sameKey = (current.vnode.key ?? null) === (nextVnode.key ?? null);
|
|
694
|
+
if (sameType && sameKey) {
|
|
695
|
+
return this.updateFiber(current, nextVnode, parentFiber, parentScope);
|
|
696
|
+
}
|
|
697
|
+
// Type or key changed — destroy the old node, create a new one.
|
|
698
|
+
// Sync fast-path if both destroy and materialize completed synchronously.
|
|
699
|
+
const destroyRes = this.destroyFiber(current);
|
|
700
|
+
if (isThenable(destroyRes)) {
|
|
701
|
+
return destroyRes.then(() => this.materialize(nextVnode, parentFiber, parentScope));
|
|
702
|
+
}
|
|
703
|
+
return this.materialize(nextVnode, parentFiber, parentScope);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Updates an existing fiber: applies new props to the instance,
|
|
707
|
+
* calls onUpdate, recursively diffs children.
|
|
708
|
+
*
|
|
709
|
+
* @param {RuntimeFiber<P>} current - current fiber
|
|
710
|
+
* @param {VirtualServiceNode<P>} nextVnode - new virtual node
|
|
711
|
+
* @param {RuntimeFiber | null} parentFiber - parent fiber
|
|
712
|
+
* @param {ContextScope} parentScope - parent scope
|
|
713
|
+
* @returns {Promise<RuntimeFiber<P>>}
|
|
714
|
+
*/
|
|
715
|
+
updateFiber(current, nextVnode, parentFiber, parentScope) {
|
|
716
|
+
const instance = current.instance;
|
|
717
|
+
if (instance === null) {
|
|
718
|
+
throw new Error('[Effectable] GraphRuntime: UPDATE on fiber with null instance.');
|
|
719
|
+
}
|
|
720
|
+
const prevProps = instance.props;
|
|
721
|
+
const propsReceiver = instance[types_1.RUNTIME_PROPS_RECEIVER];
|
|
722
|
+
if (typeof propsReceiver === 'function') {
|
|
723
|
+
propsReceiver.call(instance, nextVnode.props);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
instance.props = nextVnode.props;
|
|
727
|
+
}
|
|
728
|
+
// Build scope for child nodes (ContextProvider may have updated values)
|
|
729
|
+
const childScope = this.buildChildScope(instance, parentScope);
|
|
730
|
+
// Call onUpdate if props changed
|
|
731
|
+
if (prevProps !== instance.props && current.engine.canUpdate()) {
|
|
732
|
+
try {
|
|
733
|
+
instance.onUpdate(prevProps, instance.props);
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
const cleanupResult = this.runFiberFailedCleanup(current);
|
|
737
|
+
if (isThenable(cleanupResult)) {
|
|
738
|
+
return cleanupResult.then(() => {
|
|
739
|
+
throw error;
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// Update ref
|
|
746
|
+
if (nextVnode.ref !== undefined) {
|
|
747
|
+
nextVnode.ref.current = instance;
|
|
748
|
+
}
|
|
749
|
+
// Reconcile child nodes (sync fast-path if all children are sync).
|
|
750
|
+
let nextChildVnodes;
|
|
751
|
+
try {
|
|
752
|
+
nextChildVnodes = this.getChildVnodes(instance, nextVnode.children);
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
const cleanupResult = this.runFiberFailedCleanup(current);
|
|
756
|
+
if (isThenable(cleanupResult)) {
|
|
757
|
+
return cleanupResult.then(() => {
|
|
758
|
+
throw error;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
const childrenRes = this.reconcileChildren(current.children, nextChildVnodes, current, childScope);
|
|
764
|
+
if (isThenable(childrenRes)) {
|
|
765
|
+
return childrenRes.then((nextChildren) => {
|
|
766
|
+
this.applyFiberUpdate(current, nextVnode, parentFiber, parentScope, nextChildren);
|
|
767
|
+
return current;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
this.applyFiberUpdate(current, nextVnode, parentFiber, parentScope, childrenRes);
|
|
771
|
+
return current;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Fiber cleanup after update/compose error: `runFailedCleanup` + bus dispose.
|
|
775
|
+
* Does not leave the node in `ready`.
|
|
776
|
+
*
|
|
777
|
+
* @param {RuntimeFiber<unknown>} fiber - fiber that failed
|
|
778
|
+
* @returns {void | Promise<void>}
|
|
779
|
+
*/
|
|
780
|
+
runFiberFailedCleanup(fiber) {
|
|
781
|
+
const instance = fiber.instance;
|
|
782
|
+
if (instance === null) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.clearUpdateHook(instance);
|
|
786
|
+
this.dirtyFibers.delete(fiber);
|
|
787
|
+
const cleanupResult = fiber.engine.runFailedCleanup(instance, true);
|
|
788
|
+
if (isThenable(cleanupResult)) {
|
|
789
|
+
return cleanupResult.then(() => {
|
|
790
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
791
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
795
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Applies the reconcile result to the current fiber in-place.
|
|
799
|
+
* In-place mutation instead of spread: 0 heap allocations on UPDATE
|
|
800
|
+
* (3.09x speedup). Safe: RuntimeFiber is private.
|
|
801
|
+
*
|
|
802
|
+
* @param {RuntimeFiber<P>} current - current fiber
|
|
803
|
+
* @param {VirtualServiceNode<P>} nextVnode - new vnode
|
|
804
|
+
* @param {RuntimeFiber<unknown> | null} parentFiber - parent fiber
|
|
805
|
+
* @param {ContextScope} parentScope - parent scope
|
|
806
|
+
* @param {RuntimeFiber<unknown>[]} nextChildren - new child fibers
|
|
807
|
+
* @returns {void}
|
|
808
|
+
*/
|
|
809
|
+
applyFiberUpdate(current, nextVnode, parentFiber, parentScope, nextChildren) {
|
|
810
|
+
current.vnode = nextVnode;
|
|
811
|
+
current.parentFiber = parentFiber;
|
|
812
|
+
current.children = nextChildren;
|
|
813
|
+
current.effectTag = types_1.FIBER_EFFECT_TAG.UPDATE;
|
|
814
|
+
current.scope = parentScope;
|
|
815
|
+
current.lifecycleStatus = current.engine.getStatus();
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Diffs children: matches current and next child nodes by key+type.
|
|
819
|
+
* Nodes without a key are matched by position.
|
|
820
|
+
* Extra current nodes — DELETE; new unpaired nodes — PLACE.
|
|
821
|
+
*
|
|
822
|
+
* Optimizations:
|
|
823
|
+
* - isStableChildren fast-path (9.31x): if children are stable (N≤32, same type+key) — indexed loop without Map.
|
|
824
|
+
* - Skip keyedCurrentMap (6.06x): if there are no keyed children — do not create a Map at all.
|
|
825
|
+
* - Depth-indexed Map pool (5.1x): with keyed children, reuse Map via clear() instead of new Map().
|
|
826
|
+
*
|
|
827
|
+
* @param {RuntimeFiber[]} currentChildren - current child fibers
|
|
828
|
+
* @param {VirtualServiceNode[]} nextVnodes - new child virtual nodes
|
|
829
|
+
* @param {RuntimeFiber} parentFiber - parent fiber
|
|
830
|
+
* @param {ContextScope} childScope - scope for child nodes
|
|
831
|
+
* @returns {Promise<RuntimeFiber[]>}
|
|
832
|
+
*/
|
|
833
|
+
reconcileChildren(currentChildren, nextVnodes, parentFiber, childScope) {
|
|
834
|
+
// === FAST PATH: stable children (N≤32, same type+key per position) ===
|
|
835
|
+
// 9.31x speedup vs full diff for stable trees (typical HFT scenario)
|
|
836
|
+
if (this.isStableChildren(currentChildren, nextVnodes)) {
|
|
837
|
+
const n = nextVnodes.length;
|
|
838
|
+
const stableResult = [];
|
|
839
|
+
for (let i = 0; i < n; i++) {
|
|
840
|
+
const reconciled = this.reconcileFiber(currentChildren[i], nextVnodes[i], parentFiber, childScope);
|
|
841
|
+
if (isThenable(reconciled)) {
|
|
842
|
+
return this.continueStableReconcileAsync(stableResult, reconciled, i, nextVnodes, currentChildren, parentFiber, childScope);
|
|
843
|
+
}
|
|
844
|
+
stableResult.push(reconciled);
|
|
845
|
+
}
|
|
846
|
+
return stableResult;
|
|
847
|
+
}
|
|
848
|
+
// === FULL DIFF PATH — always async (complex logic; sync path not optimized here) ===
|
|
849
|
+
return this.reconcileChildrenFullDiff(currentChildren, nextVnodes, parentFiber, childScope);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Async continuation of the stable fast-path reconcile after an async child.
|
|
853
|
+
*
|
|
854
|
+
* @param {RuntimeFiber<unknown>[]} resultSoFar - already gathered sync children
|
|
855
|
+
* @param {PromiseLike<RuntimeFiber<unknown>>} pending - Promise for the current child
|
|
856
|
+
* @param {number} pendingIdx - index of the pending child
|
|
857
|
+
* @param {VirtualServiceNode[]} nextVnodes - all new vnodes
|
|
858
|
+
* @param {RuntimeFiber<unknown>[]} currentChildren - all current fibers
|
|
859
|
+
* @param {RuntimeFiber<unknown>} parentFiber - parent fiber
|
|
860
|
+
* @param {ContextScope} childScope - children scope
|
|
861
|
+
* @returns {Promise<RuntimeFiber<unknown>[]>}
|
|
862
|
+
*/
|
|
863
|
+
async continueStableReconcileAsync(resultSoFar, pending, pendingIdx, nextVnodes, currentChildren, parentFiber, childScope) {
|
|
864
|
+
this.stableAsyncContinueCount += 1;
|
|
865
|
+
resultSoFar.push(await pending);
|
|
866
|
+
for (let i = pendingIdx + 1; i < nextVnodes.length; i++) {
|
|
867
|
+
const reconciled = this.reconcileFiber(currentChildren[i], nextVnodes[i], parentFiber, childScope);
|
|
868
|
+
resultSoFar.push(isThenable(reconciled) ? await reconciled : reconciled);
|
|
869
|
+
}
|
|
870
|
+
return resultSoFar;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Full-diff reconcile: keyed/unkeyed Map + destroy orphans.
|
|
874
|
+
* Always async — internal branching is too complex for an efficient sync path.
|
|
875
|
+
*
|
|
876
|
+
* @param {RuntimeFiber<unknown>[]} currentChildren - current child fibers
|
|
877
|
+
* @param {VirtualServiceNode[]} nextVnodes - new vnodes
|
|
878
|
+
* @param {RuntimeFiber<unknown>} parentFiber - parent fiber
|
|
879
|
+
* @param {ContextScope} childScope - children scope
|
|
880
|
+
* @returns {Promise<RuntimeFiber<unknown>[]>}
|
|
881
|
+
*/
|
|
882
|
+
async reconcileChildrenFullDiff(currentChildren, nextVnodes, parentFiber, childScope) {
|
|
883
|
+
// Check for keyed children before creating a Map (6.06x speedup for unkeyed-only)
|
|
884
|
+
let hasKeyedCurrent = false;
|
|
885
|
+
for (const child of currentChildren) {
|
|
886
|
+
if (child.vnode.key !== undefined) {
|
|
887
|
+
hasKeyedCurrent = true;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const unkeyedCurrent = [];
|
|
892
|
+
const nextChildren = [];
|
|
893
|
+
let unkeyedIdx = 0;
|
|
894
|
+
if (hasKeyedCurrent) {
|
|
895
|
+
// Acquire Map from the depth-indexed pool (5.1x: Map.clear() vs new Map())
|
|
896
|
+
const keyedCurrentMap = this.acquireKeyedMap();
|
|
897
|
+
this.reconcileDepth++;
|
|
898
|
+
try {
|
|
899
|
+
// Build map of current children by key (for keyed matching)
|
|
900
|
+
for (const child of currentChildren) {
|
|
901
|
+
const key = child.vnode.key;
|
|
902
|
+
if (key !== undefined) {
|
|
903
|
+
keyedCurrentMap.set(key, child);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
unkeyedCurrent.push(child);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
for (const nextVnode of nextVnodes) {
|
|
910
|
+
const nextKey = nextVnode.key;
|
|
911
|
+
if (nextKey !== undefined && keyedCurrentMap.has(nextKey)) {
|
|
912
|
+
const currentFiber = keyedCurrentMap.get(nextKey);
|
|
913
|
+
if (currentFiber === undefined) {
|
|
914
|
+
throw new Error(`[Effectable] GraphRuntime: fiber with key "${nextKey}" not found in map.`);
|
|
915
|
+
}
|
|
916
|
+
keyedCurrentMap.delete(nextKey);
|
|
917
|
+
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
918
|
+
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
919
|
+
}
|
|
920
|
+
else if (nextKey === undefined && unkeyedIdx < unkeyedCurrent.length) {
|
|
921
|
+
const currentFiber = unkeyedCurrent[unkeyedIdx];
|
|
922
|
+
unkeyedIdx += 1;
|
|
923
|
+
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
924
|
+
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
// New node — PLACE
|
|
928
|
+
const newRes = this.materialize(nextVnode, parentFiber, childScope);
|
|
929
|
+
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// Destroy remaining unpaired current children (keyed)
|
|
933
|
+
for (const [, orphan] of keyedCurrentMap) {
|
|
934
|
+
const d = this.destroyFiber(orphan);
|
|
935
|
+
if (isThenable(d)) {
|
|
936
|
+
await d;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
finally {
|
|
941
|
+
this.reconcileDepth--;
|
|
942
|
+
this.releaseKeyedMap();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
// No keyed children — skip Map, positional reconcile
|
|
947
|
+
for (const child of currentChildren) {
|
|
948
|
+
unkeyedCurrent.push(child);
|
|
949
|
+
}
|
|
950
|
+
for (const nextVnode of nextVnodes) {
|
|
951
|
+
if (unkeyedIdx < unkeyedCurrent.length) {
|
|
952
|
+
const currentFiber = unkeyedCurrent[unkeyedIdx];
|
|
953
|
+
unkeyedIdx += 1;
|
|
954
|
+
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
955
|
+
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
// New node — PLACE
|
|
959
|
+
const newRes = this.materialize(nextVnode, parentFiber, childScope);
|
|
960
|
+
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Destroy remaining unpaired unkeyed children
|
|
965
|
+
for (let i = unkeyedIdx; i < unkeyedCurrent.length; i += 1) {
|
|
966
|
+
const orphan = unkeyedCurrent[i];
|
|
967
|
+
if (orphan !== undefined) {
|
|
968
|
+
const d = this.destroyFiber(orphan);
|
|
969
|
+
if (isThenable(d)) {
|
|
970
|
+
await d;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return nextChildren;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Whether children are stable: same count, same type and key per position.
|
|
978
|
+
* Only for N≤32 — with more children the check does not pay off.
|
|
979
|
+
* When children are stable, reconcileChildren skips the full diff (9.31x speedup).
|
|
980
|
+
*
|
|
981
|
+
* @param {RuntimeFiber<unknown>[]} current - current child fibers
|
|
982
|
+
* @param {VirtualServiceNode[]} next - new child virtual nodes
|
|
983
|
+
* @returns {boolean} true if children are stable and the fast-path may be used
|
|
984
|
+
*/
|
|
985
|
+
isStableChildren(current, next) {
|
|
986
|
+
const n = current.length;
|
|
987
|
+
if (n !== next.length || n > 32) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
for (let i = 0; i < n; i++) {
|
|
991
|
+
if (current[i].vnode.type !== next[i].type) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if ((current[i].vnode.key ?? null) !== (next[i].key ?? null)) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Returns a Map from the depth-indexed pool for the current recursion depth.
|
|
1002
|
+
* On first visit to a depth — creates a new Map (lazy alloc).
|
|
1003
|
+
* Repeated calls at the same depth get an already-cleared Map.
|
|
1004
|
+
*
|
|
1005
|
+
* @returns {Map<string, RuntimeFiber<unknown>>} Map for the current recursion depth
|
|
1006
|
+
*/
|
|
1007
|
+
acquireKeyedMap() {
|
|
1008
|
+
const depth = this.reconcileDepth;
|
|
1009
|
+
if (depth >= this.keyedMapPool.length) {
|
|
1010
|
+
this.keyedMapPool.push(new Map());
|
|
1011
|
+
}
|
|
1012
|
+
return this.keyedMapPool[depth];
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Clears the Map for the current recursion depth and returns it to the pool.
|
|
1016
|
+
* Called in finally after the keyed diff completes.
|
|
1017
|
+
*
|
|
1018
|
+
* @returns {void}
|
|
1019
|
+
*/
|
|
1020
|
+
releaseKeyedMap() {
|
|
1021
|
+
this.keyedMapPool[this.reconcileDepth].clear();
|
|
1022
|
+
}
|
|
1023
|
+
// ---------------------------------------------------------------------------
|
|
1024
|
+
// Destroy
|
|
1025
|
+
// ---------------------------------------------------------------------------
|
|
1026
|
+
/**
|
|
1027
|
+
* Unmounts a fiber and its entire subtree.
|
|
1028
|
+
* Order: children first (recursively), then the parent.
|
|
1029
|
+
* Afterward: clears ref.current.
|
|
1030
|
+
*
|
|
1031
|
+
* Returns `void` synchronously if the whole subtree is sync (up to 266x speedup
|
|
1032
|
+
* on an 85-node tree); otherwise a Promise. `await` works correctly with either union branch.
|
|
1033
|
+
*
|
|
1034
|
+
* @param {RuntimeFiber} fiber - fiber to destroy
|
|
1035
|
+
* @returns {void | Promise<void>}
|
|
1036
|
+
*/
|
|
1037
|
+
destroyFiber(fiber) {
|
|
1038
|
+
const children = fiber.children;
|
|
1039
|
+
const n = children.length;
|
|
1040
|
+
// Sync recursion over children until the first async
|
|
1041
|
+
for (let i = 0; i < n; i++) {
|
|
1042
|
+
const childRes = this.destroyFiber(children[i]);
|
|
1043
|
+
if (isThenable(childRes)) {
|
|
1044
|
+
return this.continueDestroyAsync(fiber, children, i, childRes);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const instance = fiber.instance;
|
|
1048
|
+
if (instance === null) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
this.clearUpdateHook(instance);
|
|
1052
|
+
this.dirtyFibers.delete(fiber);
|
|
1053
|
+
const shutdownRes = fiber.engine.runShutdown(instance);
|
|
1054
|
+
if (isThenable(shutdownRes)) {
|
|
1055
|
+
return this.finalizeDestroyAsync(fiber, shutdownRes);
|
|
1056
|
+
}
|
|
1057
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1058
|
+
// Clear ref after unmount
|
|
1059
|
+
const ref = fiber.vnode.ref;
|
|
1060
|
+
if (ref !== undefined) {
|
|
1061
|
+
ref.current = null;
|
|
1062
|
+
}
|
|
1063
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Async continuation of {@link destroyFiber} after one of the children returned a Promise.
|
|
1067
|
+
*
|
|
1068
|
+
* @param {RuntimeFiber<unknown>} fiber - current fiber
|
|
1069
|
+
* @param {Fiber[]} children - children list
|
|
1070
|
+
* @param {number} pendingIdx - index of the pending child
|
|
1071
|
+
* @param {PromiseLike<void>} pending - Promise from destroying the child
|
|
1072
|
+
* @returns {Promise<void>}
|
|
1073
|
+
*/
|
|
1074
|
+
async continueDestroyAsync(fiber, children, pendingIdx, pending) {
|
|
1075
|
+
await pending;
|
|
1076
|
+
for (let i = pendingIdx + 1; i < children.length; i++) {
|
|
1077
|
+
const r = this.destroyFiber(children[i]);
|
|
1078
|
+
if (isThenable(r)) {
|
|
1079
|
+
await r;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const instance = fiber.instance;
|
|
1083
|
+
if (instance === null) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
this.clearUpdateHook(instance);
|
|
1087
|
+
this.dirtyFibers.delete(fiber);
|
|
1088
|
+
const shutdownRes = fiber.engine.runShutdown(instance);
|
|
1089
|
+
if (isThenable(shutdownRes)) {
|
|
1090
|
+
await shutdownRes;
|
|
1091
|
+
}
|
|
1092
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1093
|
+
const ref = fiber.vnode.ref;
|
|
1094
|
+
if (ref !== undefined) {
|
|
1095
|
+
ref.current = null;
|
|
1096
|
+
}
|
|
1097
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Async finalization of {@link destroyFiber} when children were destroyed synchronously
|
|
1101
|
+
* but `runShutdown` returned a Promise.
|
|
1102
|
+
*
|
|
1103
|
+
* @param {RuntimeFiber<unknown>} fiber
|
|
1104
|
+
* @param {PromiseLike<unknown>} pendingShutdown
|
|
1105
|
+
* @returns {Promise<void>}
|
|
1106
|
+
*/
|
|
1107
|
+
async finalizeDestroyAsync(fiber, pendingShutdown) {
|
|
1108
|
+
await pendingShutdown;
|
|
1109
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1110
|
+
const ref = fiber.vnode.ref;
|
|
1111
|
+
if (ref !== undefined) {
|
|
1112
|
+
ref.current = null;
|
|
1113
|
+
}
|
|
1114
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1115
|
+
}
|
|
1116
|
+
// ---------------------------------------------------------------------------
|
|
1117
|
+
// Helpers
|
|
1118
|
+
// ---------------------------------------------------------------------------
|
|
1119
|
+
/**
|
|
1120
|
+
* Returns the list of child VirtualServiceNode: first from the instance's compose(),
|
|
1121
|
+
* then from explicit vnode children (fallback).
|
|
1122
|
+
*
|
|
1123
|
+
* @param {Component<unknown, unknown>} instance - component instance
|
|
1124
|
+
* @param {VirtualServiceNode[]} explicitChildren - children from vnode.children
|
|
1125
|
+
* @returns {VirtualServiceNode[]}
|
|
1126
|
+
*/
|
|
1127
|
+
getChildVnodes(instance, explicitChildren) {
|
|
1128
|
+
if (typeof instance.compose !== 'function') {
|
|
1129
|
+
return explicitChildren;
|
|
1130
|
+
}
|
|
1131
|
+
const composed = instance.compose();
|
|
1132
|
+
if (composed === null) {
|
|
1133
|
+
return [];
|
|
1134
|
+
}
|
|
1135
|
+
if (Array.isArray(composed)) {
|
|
1136
|
+
return composed;
|
|
1137
|
+
}
|
|
1138
|
+
return [composed];
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Builds the child scope: if the instance is a ContextProvider — extends the scope with its values.
|
|
1142
|
+
*
|
|
1143
|
+
* @param {Component<unknown, unknown>} instance - component instance
|
|
1144
|
+
* @param {ContextScope} parentScope - parent scope
|
|
1145
|
+
* @returns {ContextScope}
|
|
1146
|
+
*/
|
|
1147
|
+
buildChildScope(instance, parentScope) {
|
|
1148
|
+
// Symbol flag instead of instanceof: 1.90x speedup on the negative path (ordinary components)
|
|
1149
|
+
if (instance[context_1.IS_CONTEXT_PROVIDER] === true) {
|
|
1150
|
+
return instance.applyToScope(parentScope);
|
|
1151
|
+
}
|
|
1152
|
+
return parentScope;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
exports.GraphRuntime = GraphRuntime;
|
|
1156
|
+
// ---------------------------------------------------------------------------
|
|
1157
|
+
// effectTag types (re-export for convenience)
|
|
1158
|
+
// ---------------------------------------------------------------------------
|
|
1159
|
+
/**
|
|
1160
|
+
* Creates a node effectTag from a situation (identity helper for {@link FiberEffectTag}).
|
|
1161
|
+
*
|
|
1162
|
+
* @param {FiberEffectTag} tag - value from {@link FIBER_EFFECT_TAG} or null
|
|
1163
|
+
* @returns {FiberEffectTag}
|
|
1164
|
+
*/
|
|
1165
|
+
function makeFiberEffectTag(tag) {
|
|
1166
|
+
return tag;
|
|
1167
|
+
}
|
|
1168
|
+
//# sourceMappingURL=GraphRuntime.js.map
|