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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/build/LICENSE +21 -0
  4. package/build/bootstrap/bootstrap.d.ts +32 -0
  5. package/build/bootstrap/bootstrap.d.ts.map +1 -0
  6. package/build/bootstrap/bootstrap.js +179 -0
  7. package/build/bootstrap/bootstrap.js.map +1 -0
  8. package/build/bootstrap/index.d.ts +9 -0
  9. package/build/bootstrap/index.d.ts.map +1 -0
  10. package/build/bootstrap/index.js +14 -0
  11. package/build/bootstrap/index.js.map +1 -0
  12. package/build/bootstrap/types.d.ts +105 -0
  13. package/build/bootstrap/types.d.ts.map +1 -0
  14. package/build/bootstrap/types.js +26 -0
  15. package/build/bootstrap/types.js.map +1 -0
  16. package/build/component/Component.d.ts +126 -0
  17. package/build/component/Component.d.ts.map +1 -0
  18. package/build/component/Component.js +123 -0
  19. package/build/component/Component.js.map +1 -0
  20. package/build/component/GraphRuntime.d.ts +457 -0
  21. package/build/component/GraphRuntime.d.ts.map +1 -0
  22. package/build/component/GraphRuntime.js +1168 -0
  23. package/build/component/GraphRuntime.js.map +1 -0
  24. package/build/component/constants.d.ts +1 -0
  25. package/build/component/constants.d.ts.map +1 -0
  26. package/build/component/constants.js +2 -0
  27. package/build/component/constants.js.map +1 -0
  28. package/build/component/context.d.ts +199 -0
  29. package/build/component/context.d.ts.map +1 -0
  30. package/build/component/context.js +262 -0
  31. package/build/component/context.js.map +1 -0
  32. package/build/component/graphRuntime.constants.d.ts +13 -0
  33. package/build/component/graphRuntime.constants.d.ts.map +1 -0
  34. package/build/component/graphRuntime.constants.js +15 -0
  35. package/build/component/graphRuntime.constants.js.map +1 -0
  36. package/build/component/h.d.ts +48 -0
  37. package/build/component/h.d.ts.map +1 -0
  38. package/build/component/h.js +72 -0
  39. package/build/component/h.js.map +1 -0
  40. package/build/component/index.d.ts +19 -0
  41. package/build/component/index.d.ts.map +1 -0
  42. package/build/component/index.js +39 -0
  43. package/build/component/index.js.map +1 -0
  44. package/build/component/lifecycle.d.ts +179 -0
  45. package/build/component/lifecycle.d.ts.map +1 -0
  46. package/build/component/lifecycle.js +367 -0
  47. package/build/component/lifecycle.js.map +1 -0
  48. package/build/component/refs.d.ts +105 -0
  49. package/build/component/refs.d.ts.map +1 -0
  50. package/build/component/refs.js +128 -0
  51. package/build/component/refs.js.map +1 -0
  52. package/build/component/types.d.ts +169 -0
  53. package/build/component/types.d.ts.map +1 -0
  54. package/build/component/types.js +40 -0
  55. package/build/component/types.js.map +1 -0
  56. package/build/connect/connect.d.ts +31 -0
  57. package/build/connect/connect.d.ts.map +1 -0
  58. package/build/connect/connect.js +500 -0
  59. package/build/connect/connect.js.map +1 -0
  60. package/build/connect/index.d.ts +8 -0
  61. package/build/connect/index.d.ts.map +1 -0
  62. package/build/connect/index.js +11 -0
  63. package/build/connect/index.js.map +1 -0
  64. package/build/connect/types.d.ts +90 -0
  65. package/build/connect/types.d.ts.map +1 -0
  66. package/build/connect/types.js +8 -0
  67. package/build/connect/types.js.map +1 -0
  68. package/build/index.d.ts +26 -0
  69. package/build/index.d.ts.map +1 -0
  70. package/build/index.js +60 -0
  71. package/build/index.js.map +1 -0
  72. package/build/runtime/BusDecorators.d.ts +113 -0
  73. package/build/runtime/BusDecorators.d.ts.map +1 -0
  74. package/build/runtime/BusDecorators.js +302 -0
  75. package/build/runtime/BusDecorators.js.map +1 -0
  76. package/build/runtime/CommandBus.d.ts +43 -0
  77. package/build/runtime/CommandBus.d.ts.map +1 -0
  78. package/build/runtime/CommandBus.js +65 -0
  79. package/build/runtime/CommandBus.js.map +1 -0
  80. package/build/runtime/EventBus.d.ts +50 -0
  81. package/build/runtime/EventBus.d.ts.map +1 -0
  82. package/build/runtime/EventBus.js +90 -0
  83. package/build/runtime/EventBus.js.map +1 -0
  84. package/build/runtime/HandleRegistry.d.ts +143 -0
  85. package/build/runtime/HandleRegistry.d.ts.map +1 -0
  86. package/build/runtime/HandleRegistry.js +244 -0
  87. package/build/runtime/HandleRegistry.js.map +1 -0
  88. package/build/runtime/QueryBus.d.ts +43 -0
  89. package/build/runtime/QueryBus.d.ts.map +1 -0
  90. package/build/runtime/QueryBus.js +65 -0
  91. package/build/runtime/QueryBus.js.map +1 -0
  92. package/build/runtime/index.d.ts +13 -0
  93. package/build/runtime/index.d.ts.map +1 -0
  94. package/build/runtime/index.js +32 -0
  95. package/build/runtime/index.js.map +1 -0
  96. package/build/runtime/types.d.ts +79 -0
  97. package/build/runtime/types.d.ts.map +1 -0
  98. package/build/runtime/types.js +8 -0
  99. package/build/runtime/types.js.map +1 -0
  100. package/build/store/createStore.d.ts +36 -0
  101. package/build/store/createStore.d.ts.map +1 -0
  102. package/build/store/createStore.js +131 -0
  103. package/build/store/createStore.js.map +1 -0
  104. package/build/store/index.d.ts +14 -0
  105. package/build/store/index.d.ts.map +1 -0
  106. package/build/store/index.js +39 -0
  107. package/build/store/index.js.map +1 -0
  108. package/build/store/middleware.d.ts +38 -0
  109. package/build/store/middleware.d.ts.map +1 -0
  110. package/build/store/middleware.js +99 -0
  111. package/build/store/middleware.js.map +1 -0
  112. package/build/store/selector.d.ts +62 -0
  113. package/build/store/selector.d.ts.map +1 -0
  114. package/build/store/selector.js +226 -0
  115. package/build/store/selector.js.map +1 -0
  116. package/build/store/semanticStateTree.d.ts +91 -0
  117. package/build/store/semanticStateTree.d.ts.map +1 -0
  118. package/build/store/semanticStateTree.js +263 -0
  119. package/build/store/semanticStateTree.js.map +1 -0
  120. package/build/store/types.d.ts +259 -0
  121. package/build/store/types.d.ts.map +1 -0
  122. package/build/store/types.js +28 -0
  123. package/build/store/types.js.map +1 -0
  124. 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