effectable 1.0.0-canary.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -9
- package/build/bootstrap/bootstrap.d.ts.map +1 -1
- package/build/bootstrap/bootstrap.js +6 -12
- package/build/bootstrap/bootstrap.js.map +1 -1
- package/build/bootstrap/types.d.ts +1 -7
- package/build/bootstrap/types.d.ts.map +1 -1
- package/build/component/GraphRuntime.d.ts +18 -226
- package/build/component/GraphRuntime.d.ts.map +1 -1
- package/build/component/GraphRuntime.js +211 -944
- package/build/component/GraphRuntime.js.map +1 -1
- package/build/component/context.d.ts +11 -3
- package/build/component/context.d.ts.map +1 -1
- package/build/component/context.js +23 -24
- package/build/component/context.js.map +1 -1
- package/build/component/h.d.ts +6 -14
- package/build/component/h.d.ts.map +1 -1
- package/build/component/h.js +1 -19
- package/build/component/h.js.map +1 -1
- package/build/component/refs.d.ts +5 -7
- package/build/component/refs.d.ts.map +1 -1
- package/build/component/refs.js +22 -45
- package/build/component/refs.js.map +1 -1
- package/build/component/types.d.ts +5 -16
- package/build/component/types.d.ts.map +1 -1
- package/build/component/types.js.map +1 -1
- package/build/connect/connect.d.ts.map +1 -1
- package/build/connect/connect.js +12 -3
- package/build/connect/connect.js.map +1 -1
- package/build/connect/types.d.ts +2 -2
- package/build/connect/types.d.ts.map +1 -1
- package/build/runtime/BusDecorators.d.ts +0 -6
- package/build/runtime/BusDecorators.d.ts.map +1 -1
- package/build/runtime/BusDecorators.js +56 -117
- package/build/runtime/BusDecorators.js.map +1 -1
- package/build/runtime/CommandBus.d.ts +1 -1
- package/build/runtime/CommandBus.d.ts.map +1 -1
- package/build/runtime/CommandBus.js +3 -6
- package/build/runtime/CommandBus.js.map +1 -1
- package/build/runtime/HandleRegistry.d.ts +1 -2
- package/build/runtime/HandleRegistry.d.ts.map +1 -1
- package/build/runtime/HandleRegistry.js +7 -38
- package/build/runtime/HandleRegistry.js.map +1 -1
- package/build/runtime/QueryBus.d.ts +1 -1
- package/build/runtime/QueryBus.d.ts.map +1 -1
- package/build/runtime/QueryBus.js +3 -6
- package/build/runtime/QueryBus.js.map +1 -1
- package/build/store/createStore.d.ts +2 -2
- package/build/store/createStore.d.ts.map +1 -1
- package/build/store/createStore.js +4 -31
- package/build/store/createStore.js.map +1 -1
- package/build/store/middleware.d.ts +1 -16
- package/build/store/middleware.d.ts.map +1 -1
- package/build/store/middleware.js +15 -16
- package/build/store/middleware.js.map +1 -1
- package/build/store/types.d.ts +4 -11
- package/build/store/types.d.ts.map +1 -1
- package/build/store/types.js.map +1 -1
- package/package.json +3 -10
|
@@ -5,14 +5,10 @@
|
|
|
5
5
|
* Responsibilities:
|
|
6
6
|
* - Materialize a VirtualServiceNode tree into real component instances (Fiber tree).
|
|
7
7
|
* - Fiber-like reconcile: diff current vs next trees by key + type, assign effectTags.
|
|
8
|
-
* Reconciliation mutates the live graph (not an isolated work-in-progress tree).
|
|
9
8
|
* - Drive lifecycle via LifecycleEngine: startup in topological order (children before parent),
|
|
10
|
-
* shutdown in
|
|
9
|
+
* shutdown in reverse order (parent before children).
|
|
11
10
|
* - Inject contexts (@UseContext) and bind refs on mount.
|
|
12
11
|
* - Pass updated props into existing instances during reconcile.
|
|
13
|
-
* - Serialize all graph operations through a single operation queue.
|
|
14
|
-
* - Fail-stop on unrecoverable errors: mark runtime FAILED, reject later reconcile,
|
|
15
|
-
* unmount stays safe.
|
|
16
12
|
*
|
|
17
13
|
* Current limitations:
|
|
18
14
|
* - Work loop is synchronous (no priority lanes — next increment).
|
|
@@ -45,20 +41,6 @@ function isThenable(value) {
|
|
|
45
41
|
typeof value.then === 'function');
|
|
46
42
|
}
|
|
47
43
|
// ---------------------------------------------------------------------------
|
|
48
|
-
// Runtime state machine
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
/**
|
|
51
|
-
* Runtime state literals.
|
|
52
|
-
* Private to GraphRuntime; reduced set without mounting/reconciling.
|
|
53
|
-
*/
|
|
54
|
-
const RUNTIME_STATE = {
|
|
55
|
-
IDLE: 'idle',
|
|
56
|
-
ACTIVE: 'active',
|
|
57
|
-
FAILED: 'failed',
|
|
58
|
-
UNMOUNTING: 'unmounting',
|
|
59
|
-
UNMOUNTED: 'unmounted',
|
|
60
|
-
};
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
44
|
// GraphRuntime
|
|
63
45
|
// ---------------------------------------------------------------------------
|
|
64
46
|
/**
|
|
@@ -74,32 +56,13 @@ const RUNTIME_STATE = {
|
|
|
74
56
|
class GraphRuntime {
|
|
75
57
|
/** Current root fiber tree (current tree). */
|
|
76
58
|
currentRoot = null;
|
|
59
|
+
/** Whether unmount has completed. */
|
|
60
|
+
unmounted = false;
|
|
77
61
|
/**
|
|
78
62
|
* Entry counter for {@link continueStableReconcileAsync} (test/debug probe).
|
|
79
63
|
* Not reset automatically — compare before/after around reconcile.
|
|
80
64
|
*/
|
|
81
65
|
stableAsyncContinueCount = 0;
|
|
82
|
-
/**
|
|
83
|
-
* Backing field for {@link GraphRuntime.state}. Writes and most reads go
|
|
84
|
-
* through the accessor; post-await re-reads use `_state` (TS 6 still narrows getters).
|
|
85
|
-
*/
|
|
86
|
-
_state = RUNTIME_STATE.IDLE;
|
|
87
|
-
/**
|
|
88
|
-
* Runtime state machine.
|
|
89
|
-
* IDLE → ACTIVE (on mount) → FAILED | UNMOUNTING → UNMOUNTED.
|
|
90
|
-
* FAILED is terminal: subsequent reconcile rejects, unmount is safe.
|
|
91
|
-
*/
|
|
92
|
-
get state() {
|
|
93
|
-
return this._state;
|
|
94
|
-
}
|
|
95
|
-
set state(next) {
|
|
96
|
-
this._state = next;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Terminal error captured by failStop().
|
|
100
|
-
* Stored to reject later reconcile calls with the same error.
|
|
101
|
-
*/
|
|
102
|
-
terminalError = null;
|
|
103
66
|
/**
|
|
104
67
|
* Runtime buses for auto-wiring decorators on nodes (optional, set in {@link GraphRuntime.mount}).
|
|
105
68
|
*/
|
|
@@ -139,81 +102,10 @@ class GraphRuntime {
|
|
|
139
102
|
* Set via the fourth argument of {@link GraphRuntime.mount}.
|
|
140
103
|
*/
|
|
141
104
|
onAutoReconcileError = null;
|
|
142
|
-
/**
|
|
143
|
-
* Operation queue: serializes reconcile and unmount.
|
|
144
|
-
* Each operation is a Promise-returning function executed sequentially.
|
|
145
|
-
*/
|
|
146
|
-
operationQueue = [];
|
|
147
|
-
/**
|
|
148
|
-
* Whether an operation is currently running.
|
|
149
|
-
*/
|
|
150
|
-
operationInProgress = false;
|
|
151
|
-
/**
|
|
152
|
-
* Cached unmount promise for concurrent unmount callers.
|
|
153
|
-
*/
|
|
154
|
-
cachedUnmountPromise = null;
|
|
155
|
-
/**
|
|
156
|
-
* Pending fail-stop teardown work.
|
|
157
|
-
* When failStop nulls currentRoot but destroy is async, this tracks the in-flight cleanup.
|
|
158
|
-
* unmount() must await this before concluding teardown is finished.
|
|
159
|
-
*/
|
|
160
|
-
pendingTeardown = null;
|
|
161
105
|
/**
|
|
162
106
|
* Instances are created only via {@link GraphRuntime.mount}; direct `new GraphRuntime()` is unavailable externally.
|
|
163
107
|
*/
|
|
164
108
|
constructor() { }
|
|
165
|
-
/**
|
|
166
|
-
* Fail-stop: mark the runtime as failed, disable scheduling, tear down the graph best-effort.
|
|
167
|
-
* After fail-stop:
|
|
168
|
-
* - state is FAILED
|
|
169
|
-
* - terminalError is set
|
|
170
|
-
* - currentRoot is null (even if destroyFiber throws)
|
|
171
|
-
* - later reconcile() rejects with the terminal error
|
|
172
|
-
* - unmount() is safe and joinable
|
|
173
|
-
*
|
|
174
|
-
* No failed reconcile leaves the runtime active with a partial graph.
|
|
175
|
-
* Primary-error rules: cleanup errors attached as rollbackErrors, never replace primary.
|
|
176
|
-
*
|
|
177
|
-
* @param {Error} error - unrecoverable error that triggered fail-stop
|
|
178
|
-
* @returns {void | Promise<void>}
|
|
179
|
-
*/
|
|
180
|
-
failStop(error) {
|
|
181
|
-
// Idempotent: if already failed, skip
|
|
182
|
-
if (this.state === RUNTIME_STATE.FAILED || this.state === RUNTIME_STATE.UNMOUNTED) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
this.state = RUNTIME_STATE.FAILED;
|
|
186
|
-
this.terminalError = error;
|
|
187
|
-
// Disable scheduling immediately
|
|
188
|
-
this.dirtyFibers.clear();
|
|
189
|
-
this.flushScheduled = false;
|
|
190
|
-
// Best-effort teardown of the current/partial graph
|
|
191
|
-
// MUST set currentRoot = null even if destroyFiber throws
|
|
192
|
-
if (this.currentRoot !== null) {
|
|
193
|
-
const root = this.currentRoot;
|
|
194
|
-
this.currentRoot = null;
|
|
195
|
-
// Collect cleanup errors during fail-stop (attach as rollbackErrors)
|
|
196
|
-
const cleanupErrors = [];
|
|
197
|
-
const destroyRes = this.destroyFiber(root, cleanupErrors);
|
|
198
|
-
if (isThenable(destroyRes)) {
|
|
199
|
-
// Async path: track pending teardown so unmount() can join
|
|
200
|
-
this.pendingTeardown = destroyRes
|
|
201
|
-
.then(() => {
|
|
202
|
-
if (cleanupErrors.length > 0) {
|
|
203
|
-
error.rollbackErrors = cleanupErrors;
|
|
204
|
-
}
|
|
205
|
-
})
|
|
206
|
-
.finally(() => {
|
|
207
|
-
this.pendingTeardown = null;
|
|
208
|
-
});
|
|
209
|
-
return this.pendingTeardown;
|
|
210
|
-
}
|
|
211
|
-
// Sync path: attach cleanup errors immediately
|
|
212
|
-
if (cleanupErrors.length > 0) {
|
|
213
|
-
error.rollbackErrors = cleanupErrors;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
109
|
/**
|
|
218
110
|
* Auto-wires runtime-bus decorators onto the instance before {@link LifecycleEngine.runStartup}.
|
|
219
111
|
*
|
|
@@ -244,225 +136,6 @@ class GraphRuntime {
|
|
|
244
136
|
}
|
|
245
137
|
delete fiber.effectableRuntimeBusDisposer;
|
|
246
138
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Identity-safe ref clearing: clears ref.current only if it still points to the expected owner.
|
|
249
|
-
* Prevents an old rollback from clearing a ref that a newer materialization already reused.
|
|
250
|
-
* No cast required (Component | null → unknown | null is assignable).
|
|
251
|
-
*
|
|
252
|
-
* @param {RefObject<unknown>} ref - ref object
|
|
253
|
-
* @param {Component<unknown, unknown>} expectedOwner - expected current owner
|
|
254
|
-
* @returns {void}
|
|
255
|
-
*/
|
|
256
|
-
clearRefSafe(ref, expectedOwner) {
|
|
257
|
-
if (ref.current === expectedOwner) {
|
|
258
|
-
ref.current = null;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Centralized ref ownership transition.
|
|
263
|
-
* Handles every ref binding/clearing operation: add, remove, replace.
|
|
264
|
-
*
|
|
265
|
-
* Rules:
|
|
266
|
-
* - Clear previousRef only if it still points to expectedPreviousOwner (identity-safe).
|
|
267
|
-
* - Bind nextRef to instance if nextRef is provided.
|
|
268
|
-
* - previousRef and nextRef can be the same object (ref reuse) or different (ref swap).
|
|
269
|
-
* - Do not let an old disposer clear a newer owner.
|
|
270
|
-
*
|
|
271
|
-
* No casts: Component | null → unknown | null is assignable (widening).
|
|
272
|
-
*
|
|
273
|
-
* @param {RefObject<unknown> | undefined} previousRef - ref to clear (can be undefined if no previous ref)
|
|
274
|
-
* @param {Component<unknown, unknown> | null} expectedPreviousOwner - expected owner of previousRef (null if unknown)
|
|
275
|
-
* @param {RefObject<unknown> | undefined} nextRef - ref to bind to instance (can be undefined if removing ref)
|
|
276
|
-
* @param {Component<unknown, unknown> | null} instance - instance to bind nextRef to (null when clearing only)
|
|
277
|
-
* @returns {void}
|
|
278
|
-
*/
|
|
279
|
-
commitRef(previousRef, expectedPreviousOwner, nextRef, instance) {
|
|
280
|
-
// Clear previous ref if it's different from next (ref swap) or if next is undefined (ref removal)
|
|
281
|
-
if (previousRef !== undefined && previousRef !== nextRef && expectedPreviousOwner !== null) {
|
|
282
|
-
this.clearRefSafe(previousRef, expectedPreviousOwner);
|
|
283
|
-
}
|
|
284
|
-
// Bind next ref to instance (Component | null → unknown | null, no cast)
|
|
285
|
-
if (nextRef !== undefined) {
|
|
286
|
-
nextRef.current = instance;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Finalize fiber destroy: dispose wiring, clear ref, update status.
|
|
291
|
-
* Collects errors when collectErrors is provided (best-effort cleanup).
|
|
292
|
-
* When collectErrors is null, errors are thrown immediately.
|
|
293
|
-
*
|
|
294
|
-
* Uses commitRef for identity-safe ref clearing.
|
|
295
|
-
*
|
|
296
|
-
* @param {RuntimeFiber<unknown>} fiber - fiber being finalized
|
|
297
|
-
* @param {Error[] | null} collectErrors - array to collect errors (null to throw)
|
|
298
|
-
* @returns {void}
|
|
299
|
-
*/
|
|
300
|
-
finalizeFiberDestroy(fiber, collectErrors) {
|
|
301
|
-
// Dispose runtime bus wiring
|
|
302
|
-
if (collectErrors !== null) {
|
|
303
|
-
try {
|
|
304
|
-
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
305
|
-
}
|
|
306
|
-
catch (err) {
|
|
307
|
-
collectErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
312
|
-
}
|
|
313
|
-
// Clear ref via commitRef (identity-safe clearing)
|
|
314
|
-
const ref = fiber.vnode.ref;
|
|
315
|
-
const instance = fiber.instance;
|
|
316
|
-
if (ref !== undefined && instance !== null) {
|
|
317
|
-
if (collectErrors !== null) {
|
|
318
|
-
try {
|
|
319
|
-
this.commitRef(ref, instance, undefined, null);
|
|
320
|
-
}
|
|
321
|
-
catch (err) {
|
|
322
|
-
collectErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
this.commitRef(ref, instance, undefined, null);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// Update lifecycle status
|
|
330
|
-
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Transactional rollback for failed fiber materialization.
|
|
334
|
-
* Releases acquired resources in reverse acquisition order:
|
|
335
|
-
* 1. disable scheduler hook
|
|
336
|
-
* 2. dispose runtime bus registrations
|
|
337
|
-
* 3. clear bound ref (identity-safe)
|
|
338
|
-
* 4. run failed-startup cleanup
|
|
339
|
-
* 5. destroy mounted children in reverse order
|
|
340
|
-
* 6. unlink the partial fiber
|
|
341
|
-
* Cleanup is best-effort: one failure does not skip remaining steps.
|
|
342
|
-
* Preserves the original materialization error; cleanup errors are attached.
|
|
343
|
-
* Rollback is idempotent.
|
|
344
|
-
*
|
|
345
|
-
* @param {RuntimeFiber<P>} fiber - fiber being rolled back
|
|
346
|
-
* @param {Error} primaryError - original materialization/startup error
|
|
347
|
-
* @returns {void | Promise<void>}
|
|
348
|
-
*/
|
|
349
|
-
rollbackFailedMaterialization(fiber, primaryError) {
|
|
350
|
-
const journal = fiber.constructionJournal;
|
|
351
|
-
if (journal === undefined || journal.rolledBack === true) {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
journal.rolledBack = true;
|
|
355
|
-
const cleanupErrors = [];
|
|
356
|
-
const instance = fiber.instance;
|
|
357
|
-
// 1. Disable scheduler hook
|
|
358
|
-
if (journal.schedulerHookAttached === true && instance !== null) {
|
|
359
|
-
try {
|
|
360
|
-
this.clearUpdateHook(instance);
|
|
361
|
-
this.dirtyFibers.delete(fiber);
|
|
362
|
-
}
|
|
363
|
-
catch (err) {
|
|
364
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// 2. Dispose runtime bus registrations
|
|
368
|
-
if (journal.busWiringAttached === true) {
|
|
369
|
-
try {
|
|
370
|
-
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
371
|
-
}
|
|
372
|
-
catch (err) {
|
|
373
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
// 3. Clear bound ref (identity-safe)
|
|
377
|
-
if (journal.refBound === true && fiber.vnode.ref !== undefined && journal.refOwner !== undefined) {
|
|
378
|
-
try {
|
|
379
|
-
this.commitRef(fiber.vnode.ref, journal.refOwner, undefined, null);
|
|
380
|
-
}
|
|
381
|
-
catch (err) {
|
|
382
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
// 4. Run failed-startup cleanup (if instance exists)
|
|
386
|
-
let cleanupPromise = null;
|
|
387
|
-
if (instance !== null) {
|
|
388
|
-
try {
|
|
389
|
-
const cleanupRes = fiber.engine.runFailedCleanup(instance, true);
|
|
390
|
-
if (isThenable(cleanupRes)) {
|
|
391
|
-
cleanupPromise = cleanupRes;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
catch (err) {
|
|
395
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
// 5. Destroy mounted children in reverse order
|
|
399
|
-
const destroyChildrenAndFinalize = () => {
|
|
400
|
-
const children = journal.mountedChildren;
|
|
401
|
-
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
402
|
-
try {
|
|
403
|
-
const destroyRes = this.destroyFiber(children[i]);
|
|
404
|
-
if (isThenable(destroyRes)) {
|
|
405
|
-
return this.continueRollbackDestroyAsync(children, i, destroyRes, primaryError, cleanupErrors);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch (err) {
|
|
409
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
// 6. Finalize: attach cleanup errors to primary error
|
|
413
|
-
this.finalizeRollback(primaryError, cleanupErrors);
|
|
414
|
-
};
|
|
415
|
-
if (cleanupPromise !== null) {
|
|
416
|
-
return cleanupPromise.then(() => destroyChildrenAndFinalize(), (err) => {
|
|
417
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
418
|
-
return destroyChildrenAndFinalize();
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
return destroyChildrenAndFinalize();
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Async continuation of rollback child destruction after one child's destroy returned a Promise.
|
|
425
|
-
*
|
|
426
|
-
* @param {RuntimeFiber<unknown>[]} children - mounted children
|
|
427
|
-
* @param {number} lastIdx - index of the last processed child
|
|
428
|
-
* @param {Promise<void>} pending - Promise from destroying the previous child
|
|
429
|
-
* @param {Error} primaryError - original materialization error
|
|
430
|
-
* @param {Error[]} cleanupErrors - accumulated cleanup errors
|
|
431
|
-
* @returns {Promise<void>}
|
|
432
|
-
*/
|
|
433
|
-
async continueRollbackDestroyAsync(children, lastIdx, pending, primaryError, cleanupErrors) {
|
|
434
|
-
try {
|
|
435
|
-
await pending;
|
|
436
|
-
}
|
|
437
|
-
catch (err) {
|
|
438
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
439
|
-
}
|
|
440
|
-
for (let i = lastIdx - 1; i >= 0; i -= 1) {
|
|
441
|
-
try {
|
|
442
|
-
const destroyRes = this.destroyFiber(children[i]);
|
|
443
|
-
if (isThenable(destroyRes)) {
|
|
444
|
-
await destroyRes;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
catch (err) {
|
|
448
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
this.finalizeRollback(primaryError, cleanupErrors);
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Attaches cleanup errors to the primary error and rethrows.
|
|
455
|
-
*
|
|
456
|
-
* @param {Error} primaryError - original materialization error
|
|
457
|
-
* @param {Error[]} cleanupErrors - cleanup errors
|
|
458
|
-
* @returns {never}
|
|
459
|
-
*/
|
|
460
|
-
finalizeRollback(primaryError, cleanupErrors) {
|
|
461
|
-
if (cleanupErrors.length > 0) {
|
|
462
|
-
primaryError.rollbackErrors = cleanupErrors;
|
|
463
|
-
}
|
|
464
|
-
throw primaryError;
|
|
465
|
-
}
|
|
466
139
|
/**
|
|
467
140
|
* Injects the scheduler hook onto the component instance after successful startup.
|
|
468
141
|
* The hook is called from {@link Component.setState} and enqueues the fiber for automatic reconcile.
|
|
@@ -515,13 +188,11 @@ class GraphRuntime {
|
|
|
515
188
|
* - If an ancestor of the fiber is already queued → skip (ancestor covers the subtree).
|
|
516
189
|
* - If descendants of the fiber are queued → remove them (fiber covers their subtrees).
|
|
517
190
|
*
|
|
518
|
-
* Skip scheduling when runtime is FAILED.
|
|
519
|
-
*
|
|
520
191
|
* @param {RuntimeFiber<unknown>} fiber - fiber whose subtree needs rebuild
|
|
521
192
|
* @returns {void}
|
|
522
193
|
*/
|
|
523
194
|
scheduleUpdate(fiber) {
|
|
524
|
-
if (this.
|
|
195
|
+
if (this.unmounted) {
|
|
525
196
|
return;
|
|
526
197
|
}
|
|
527
198
|
// If any ancestor is already queued — this fiber will be rebuilt as part of the ancestor
|
|
@@ -550,79 +221,23 @@ class GraphRuntime {
|
|
|
550
221
|
// Schedule microtask dirty-flush
|
|
551
222
|
this.scheduleDirtyFlushMicrotask();
|
|
552
223
|
}
|
|
553
|
-
/**
|
|
554
|
-
* Enqueues an operation and starts the queue processor if idle.
|
|
555
|
-
* Operations are executed sequentially; concurrent callers await the same in-flight operation.
|
|
556
|
-
* Serialize all graph mutations.
|
|
557
|
-
*
|
|
558
|
-
* @param {() => Promise<void>} operation - operation to enqueue
|
|
559
|
-
* @returns {Promise<void>}
|
|
560
|
-
*/
|
|
561
|
-
async enqueueOperation(operation) {
|
|
562
|
-
return new Promise((resolve, reject) => {
|
|
563
|
-
this.operationQueue.push(async () => {
|
|
564
|
-
try {
|
|
565
|
-
await operation();
|
|
566
|
-
resolve();
|
|
567
|
-
}
|
|
568
|
-
catch (error) {
|
|
569
|
-
reject(error);
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
if (!this.operationInProgress) {
|
|
573
|
-
// Start processing without awaiting to allow concurrent enqueuing
|
|
574
|
-
void this.processOperationQueue();
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Processes the operation queue: runs operations one at a time.
|
|
580
|
-
* Single serialized owner of tree mutations.
|
|
581
|
-
* Errors from individual operations are propagated to their callers but do not stop the queue.
|
|
582
|
-
*
|
|
583
|
-
* @returns {Promise<void>}
|
|
584
|
-
*/
|
|
585
|
-
async processOperationQueue() {
|
|
586
|
-
if (this.operationInProgress) {
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
this.operationInProgress = true;
|
|
590
|
-
try {
|
|
591
|
-
while (this.operationQueue.length > 0) {
|
|
592
|
-
const operation = this.operationQueue.shift();
|
|
593
|
-
if (operation === undefined) {
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
try {
|
|
597
|
-
await operation();
|
|
598
|
-
}
|
|
599
|
-
catch {
|
|
600
|
-
// Error is already propagated to the caller via the promise wrapper
|
|
601
|
-
// Continue processing the queue (don't poison it forever)
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
finally {
|
|
606
|
-
this.operationInProgress = false;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
224
|
/**
|
|
610
225
|
* Queues one dirty-flush microtask and publishes {@link activeFlush} for await from `reconcile`.
|
|
611
226
|
*
|
|
612
|
-
* Skip scheduling when runtime is FAILED.
|
|
613
|
-
*
|
|
614
227
|
* @returns {void}
|
|
615
228
|
*/
|
|
616
229
|
scheduleDirtyFlushMicrotask() {
|
|
617
|
-
if (this.flushScheduled || this.
|
|
230
|
+
if (this.flushScheduled || this.unmounted) {
|
|
618
231
|
return;
|
|
619
232
|
}
|
|
620
233
|
this.flushScheduled = true;
|
|
621
234
|
const flushWork = new Promise((resolve) => {
|
|
622
235
|
queueMicrotask(() => {
|
|
623
236
|
this.flushDirtyFibers()
|
|
624
|
-
.catch(() => {
|
|
625
|
-
|
|
237
|
+
.catch((err) => {
|
|
238
|
+
if (this.onAutoReconcileError !== null) {
|
|
239
|
+
this.onAutoReconcileError(err);
|
|
240
|
+
}
|
|
626
241
|
})
|
|
627
242
|
.finally(() => {
|
|
628
243
|
resolve();
|
|
@@ -642,14 +257,11 @@ class GraphRuntime {
|
|
|
642
257
|
* Guarded by the `flushing` flag against re-entrancy.
|
|
643
258
|
* The chain of repeat passes is capped by {@link GRAPH_RUNTIME_MAX_DIRTY_FLUSH_PASSES}.
|
|
644
259
|
*
|
|
645
|
-
* Respects state (UNMOUNTING/UNMOUNTED/FAILED) to cancel flush when unmount begins or failure occurs.
|
|
646
|
-
* On unrecoverable error, invokes onAutoReconcileError then fail-stops.
|
|
647
|
-
*
|
|
648
260
|
* @returns {Promise<void>}
|
|
649
261
|
*/
|
|
650
262
|
async flushDirtyFibers() {
|
|
651
263
|
this.flushScheduled = false;
|
|
652
|
-
if (this.
|
|
264
|
+
if (this.unmounted || this.flushing) {
|
|
653
265
|
this.dirtyFibers.clear();
|
|
654
266
|
return;
|
|
655
267
|
}
|
|
@@ -659,7 +271,7 @@ class GraphRuntime {
|
|
|
659
271
|
this.dirtyFibers.clear();
|
|
660
272
|
try {
|
|
661
273
|
for (const fiber of snapshot) {
|
|
662
|
-
if (this.
|
|
274
|
+
if (this.unmounted) {
|
|
663
275
|
break;
|
|
664
276
|
}
|
|
665
277
|
const res = this.reconcileDirtyFiber(fiber);
|
|
@@ -668,39 +280,15 @@ class GraphRuntime {
|
|
|
668
280
|
}
|
|
669
281
|
}
|
|
670
282
|
}
|
|
671
|
-
catch (error) {
|
|
672
|
-
this.flushing = false;
|
|
673
|
-
// Notify error handler before fail-stop
|
|
674
|
-
if (this.onAutoReconcileError !== null) {
|
|
675
|
-
this.onAutoReconcileError(error);
|
|
676
|
-
}
|
|
677
|
-
// Fail-stop on unrecoverable dirty-flush error
|
|
678
|
-
const failError = error instanceof Error ? error : new Error(String(error));
|
|
679
|
-
const failRes = this.failStop(failError);
|
|
680
|
-
if (isThenable(failRes)) {
|
|
681
|
-
await failRes;
|
|
682
|
-
}
|
|
683
|
-
throw failError;
|
|
684
|
-
}
|
|
685
283
|
finally {
|
|
686
284
|
this.flushing = false;
|
|
687
285
|
}
|
|
688
286
|
// If new dirty fibers appeared during flush — schedule the next pass
|
|
689
|
-
if (this.dirtyFibers.size > 0 && this.
|
|
287
|
+
if (this.dirtyFibers.size > 0 && !this.unmounted) {
|
|
690
288
|
if (this.dirtyFlushPassCount >= graphRuntime_constants_1.GRAPH_RUNTIME_MAX_DIRTY_FLUSH_PASSES) {
|
|
691
289
|
this.dirtyFibers.clear();
|
|
692
290
|
this.dirtyFlushPassCount = 0;
|
|
693
|
-
|
|
694
|
-
// Notify error handler before fail-stop
|
|
695
|
-
if (this.onAutoReconcileError !== null) {
|
|
696
|
-
this.onAutoReconcileError(loopError);
|
|
697
|
-
}
|
|
698
|
-
// Fail-stop on loop limit
|
|
699
|
-
const failRes = this.failStop(loopError);
|
|
700
|
-
if (isThenable(failRes)) {
|
|
701
|
-
await failRes;
|
|
702
|
-
}
|
|
703
|
-
throw loopError;
|
|
291
|
+
throw new Error(`GraphRuntime: dirty flush exceeded ${String(graphRuntime_constants_1.GRAPH_RUNTIME_MAX_DIRTY_FLUSH_PASSES)} passes (anti-loop)`);
|
|
704
292
|
}
|
|
705
293
|
this.scheduleDirtyFlushMicrotask();
|
|
706
294
|
}
|
|
@@ -718,7 +306,7 @@ class GraphRuntime {
|
|
|
718
306
|
* @returns {void | Promise<void>}
|
|
719
307
|
*/
|
|
720
308
|
reconcileDirtyFiber(fiber) {
|
|
721
|
-
if (this.
|
|
309
|
+
if (this.unmounted) {
|
|
722
310
|
return;
|
|
723
311
|
}
|
|
724
312
|
const instance = fiber.instance;
|
|
@@ -771,173 +359,63 @@ class GraphRuntime {
|
|
|
771
359
|
const rt = new GraphRuntime();
|
|
772
360
|
rt.effectableRuntimeBuses = typeof runtimeBuses === 'undefined' ? null : runtimeBuses;
|
|
773
361
|
rt.onAutoReconcileError = typeof onAutoReconcileError === 'function' ? onAutoReconcileError : null;
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
rt.state = RUNTIME_STATE.ACTIVE;
|
|
778
|
-
return rt;
|
|
779
|
-
}
|
|
780
|
-
catch (error) {
|
|
781
|
-
// Fail-stop on unrecoverable mount error
|
|
782
|
-
const failError = error instanceof Error ? error : new Error(String(error));
|
|
783
|
-
const failRes = rt.failStop(failError);
|
|
784
|
-
if (isThenable(failRes)) {
|
|
785
|
-
await failRes;
|
|
786
|
-
}
|
|
787
|
-
throw failError;
|
|
788
|
-
}
|
|
362
|
+
const res = rt.materialize(root, null, initialScope);
|
|
363
|
+
rt.currentRoot = isThenable(res) ? await res : res;
|
|
364
|
+
return rt;
|
|
789
365
|
}
|
|
790
366
|
/**
|
|
791
367
|
* Reconciles against a new tree.
|
|
792
|
-
*
|
|
368
|
+
* Builds a work-in-progress tree, computes effectTags, applies changes:
|
|
793
369
|
* - PLACE: create and mount a new node
|
|
794
370
|
* - UPDATE: update props on an existing instance, call onUpdate
|
|
795
371
|
* - DELETE: unmount and destroy a node
|
|
796
372
|
*
|
|
797
|
-
* All reconcile calls are serialized through the operation queue.
|
|
798
|
-
* Rejects with terminal error when runtime is FAILED.
|
|
799
|
-
*
|
|
800
373
|
* @param {VirtualServiceNode<P>} nextTree - new virtual tree
|
|
801
374
|
* @returns {Promise<void>}
|
|
802
|
-
* @throws {Error} if the runtime
|
|
375
|
+
* @throws {Error} if the runtime is already unmounted
|
|
803
376
|
*/
|
|
804
377
|
async reconcile(nextTree) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
// reconcile overlaps the snapshot auto-flush.
|
|
827
|
-
while (this.activeFlush !== null) {
|
|
828
|
-
await this.activeFlush;
|
|
829
|
-
}
|
|
830
|
-
if (this._state === RUNTIME_STATE.UNMOUNTING || this._state === RUNTIME_STATE.UNMOUNTED) {
|
|
831
|
-
throw new Error('[Effectable] GraphRuntime: reconcile attempted after unmount started.');
|
|
832
|
-
}
|
|
833
|
-
if (this._state === RUNTIME_STATE.FAILED) {
|
|
834
|
-
throw this.terminalError || new Error('[Effectable] GraphRuntime: reconcile attempted after terminal failure.');
|
|
835
|
-
}
|
|
836
|
-
// Manual reconcile covers the whole tree from the root: cancel pending auto-flush
|
|
837
|
-
// to avoid double-mounting components from concurrent reconcile paths.
|
|
838
|
-
this.dirtyFibers.clear();
|
|
839
|
-
this.flushScheduled = false;
|
|
840
|
-
this.dirtyFlushPassCount = 0;
|
|
841
|
-
try {
|
|
842
|
-
const res = this.reconcileFiber(this.currentRoot, nextTree, null, this.currentRoot.scope);
|
|
843
|
-
this.currentRoot = isThenable(res) ? await res : res;
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
// Fail-stop on unrecoverable reconcile error
|
|
847
|
-
// Let failStop destroy current tree and null currentRoot (single owner)
|
|
848
|
-
const failError = error instanceof Error ? error : new Error(String(error));
|
|
849
|
-
const failRes = this.failStop(failError);
|
|
850
|
-
if (isThenable(failRes)) {
|
|
851
|
-
await failRes;
|
|
852
|
-
}
|
|
853
|
-
throw failError;
|
|
854
|
-
}
|
|
855
|
-
});
|
|
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;
|
|
856
399
|
}
|
|
857
400
|
/**
|
|
858
401
|
* Fully unmounts the component tree.
|
|
859
|
-
* Calls onUnmount for each node (children before parent) and
|
|
402
|
+
* Calls onUnmount for each node in reverse order (children before parent) and
|
|
860
403
|
* moves stages to destroyed via LifecycleEngine.
|
|
861
404
|
*
|
|
862
|
-
* Unmount is serialized, cached promise returned for concurrent callers.
|
|
863
|
-
* Safe and joinable even when runtime is FAILED.
|
|
864
|
-
* Collects cleanup errors when `rejectOnCleanupError: true` is passed.
|
|
865
|
-
*
|
|
866
|
-
* @param {object} [options] - unmount options
|
|
867
|
-
* @param {boolean} [options.rejectOnCleanupError=false] - reject on cleanup errors
|
|
868
405
|
* @returns {Promise<void>}
|
|
869
406
|
*/
|
|
870
|
-
async unmount(
|
|
871
|
-
|
|
872
|
-
// If unmount is in progress, return the cached promise (HOLE 1)
|
|
873
|
-
// Must check BEFORE the UNMOUNTED early-return so concurrent callers join the in-flight unmount
|
|
874
|
-
if (this.cachedUnmountPromise !== null) {
|
|
875
|
-
return this.cachedUnmountPromise;
|
|
876
|
-
}
|
|
877
|
-
// If unmount already completed, return immediately
|
|
878
|
-
if (this.state === RUNTIME_STATE.UNMOUNTED) {
|
|
407
|
+
async unmount() {
|
|
408
|
+
if (this.unmounted) {
|
|
879
409
|
return;
|
|
880
410
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
// Create and cache the unmount promise
|
|
887
|
-
this.cachedUnmountPromise = this.enqueueOperation(async () => {
|
|
888
|
-
// Double-check unmounted state
|
|
889
|
-
if (this.state === RUNTIME_STATE.UNMOUNTED) {
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
// Cancel any pending dirty flush
|
|
893
|
-
this.dirtyFibers.clear();
|
|
894
|
-
this.flushScheduled = false;
|
|
895
|
-
// Wait for in-flight dirty flush to complete
|
|
896
|
-
if (this.activeFlush !== null) {
|
|
897
|
-
try {
|
|
898
|
-
await this.activeFlush;
|
|
899
|
-
}
|
|
900
|
-
catch {
|
|
901
|
-
// Ignore flush errors during unmount
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
// HOLE 1: stay UNMOUNTING during destroy (not UNMOUNTED)
|
|
905
|
-
// If already FAILED, keep FAILED state through destroy
|
|
906
|
-
// State transition to UNMOUNTED happens AFTER destroy completes
|
|
907
|
-
// If pendingTeardown is active (fail-stop in progress), await it first
|
|
908
|
-
if (this.pendingTeardown !== null) {
|
|
909
|
-
try {
|
|
910
|
-
await this.pendingTeardown;
|
|
911
|
-
}
|
|
912
|
-
catch {
|
|
913
|
-
// Ignore errors — they are already attached to the fail-stop primary error
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
if (this.currentRoot !== null) {
|
|
917
|
-
// Collect cleanup errors during unmount
|
|
918
|
-
const cleanupErrors = [];
|
|
919
|
-
const d = this.destroyFiber(this.currentRoot, cleanupErrors);
|
|
920
|
-
if (isThenable(d)) {
|
|
921
|
-
await d;
|
|
922
|
-
}
|
|
923
|
-
this.currentRoot = null;
|
|
924
|
-
// HOLE 1: set UNMOUNTED only after destroy finishes
|
|
925
|
-
// Transition even if FAILED — unmount is the terminal operation
|
|
926
|
-
this.state = RUNTIME_STATE.UNMOUNTED;
|
|
927
|
-
// Reject with cleanup errors when requested
|
|
928
|
-
if (rejectOnCleanupError && cleanupErrors.length > 0) {
|
|
929
|
-
if (cleanupErrors.length === 1) {
|
|
930
|
-
throw cleanupErrors[0];
|
|
931
|
-
}
|
|
932
|
-
throw new AggregateError(cleanupErrors, 'Cleanup errors during unmount');
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
else {
|
|
936
|
-
// No root to destroy — transition to UNMOUNTED
|
|
937
|
-
this.state = RUNTIME_STATE.UNMOUNTED;
|
|
411
|
+
this.unmounted = true;
|
|
412
|
+
if (this.currentRoot !== null) {
|
|
413
|
+
const d = this.destroyFiber(this.currentRoot);
|
|
414
|
+
if (isThenable(d)) {
|
|
415
|
+
await d;
|
|
938
416
|
}
|
|
939
|
-
|
|
940
|
-
|
|
417
|
+
this.currentRoot = null;
|
|
418
|
+
}
|
|
941
419
|
}
|
|
942
420
|
/**
|
|
943
421
|
* Returns the root component instance (for testing and introspection).
|
|
@@ -951,13 +429,12 @@ class GraphRuntime {
|
|
|
951
429
|
return this.currentRoot.instance;
|
|
952
430
|
}
|
|
953
431
|
/**
|
|
954
|
-
* Whether the runtime is active (
|
|
955
|
-
* Returns false when state is FAILED.
|
|
432
|
+
* Whether the runtime is active (unmount has not been called).
|
|
956
433
|
*
|
|
957
434
|
* @returns {boolean}
|
|
958
435
|
*/
|
|
959
436
|
isActive() {
|
|
960
|
-
return this.
|
|
437
|
+
return !this.unmounted;
|
|
961
438
|
}
|
|
962
439
|
/**
|
|
963
440
|
* Readonly snapshot of the root fiber tree for test/debug introspection.
|
|
@@ -993,15 +470,6 @@ class GraphRuntime {
|
|
|
993
470
|
getStableAsyncContinueCount() {
|
|
994
471
|
return this.stableAsyncContinueCount;
|
|
995
472
|
}
|
|
996
|
-
/**
|
|
997
|
-
* Current runtime state.
|
|
998
|
-
* Test/debug probe; not a production API.
|
|
999
|
-
*
|
|
1000
|
-
* @returns {RuntimeState} current state
|
|
1001
|
-
*/
|
|
1002
|
-
getState() {
|
|
1003
|
-
return this.state;
|
|
1004
|
-
}
|
|
1005
473
|
/**
|
|
1006
474
|
* Builds a deep readonly {@link FiberInspectNode} from a RuntimeFiber.
|
|
1007
475
|
*
|
|
@@ -1062,91 +530,44 @@ class GraphRuntime {
|
|
|
1062
530
|
effectTag: types_1.FIBER_EFFECT_TAG.PLACE,
|
|
1063
531
|
engine,
|
|
1064
532
|
scope: parentScope,
|
|
1065
|
-
constructionJournal: {
|
|
1066
|
-
mountedChildren: [],
|
|
1067
|
-
},
|
|
1068
533
|
};
|
|
1069
534
|
// Recursively materialize children before running the parent's lifecycle
|
|
1070
535
|
const childVnodes = this.getChildVnodes(instance, vnode.children);
|
|
1071
|
-
|
|
1072
|
-
// Prevent partial tree construction when duplicate keys are present
|
|
1073
|
-
this.validateUniqueKeys(childVnodes.map(vnode => ({ vnode, instance: null })), fiber, 'current');
|
|
536
|
+
const childFibers = [];
|
|
1074
537
|
for (let i = 0; i < childVnodes.length; i++) {
|
|
1075
538
|
const childVnode = childVnodes[i];
|
|
1076
|
-
|
|
1077
|
-
try {
|
|
1078
|
-
childRes = this.materialize(childVnode, fiber, childScope);
|
|
1079
|
-
}
|
|
1080
|
-
catch (err) {
|
|
1081
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1082
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1083
|
-
if (isThenable(rollbackRes)) {
|
|
1084
|
-
return rollbackRes.then(() => {
|
|
1085
|
-
throw error;
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
throw error;
|
|
1089
|
-
}
|
|
539
|
+
const childRes = this.materialize(childVnode, fiber, childScope);
|
|
1090
540
|
if (isThenable(childRes)) {
|
|
1091
541
|
// Hit an async child — continue the materialization tail in the async continuation.
|
|
1092
|
-
return this.continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes,
|
|
542
|
+
return this.continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childFibers, childRes, i);
|
|
1093
543
|
}
|
|
1094
|
-
|
|
544
|
+
childFibers.push(childRes);
|
|
1095
545
|
}
|
|
1096
|
-
fiber.children =
|
|
1097
|
-
// Bind ref to the instance
|
|
546
|
+
fiber.children = childFibers;
|
|
547
|
+
// Bind ref to the instance
|
|
1098
548
|
if (vnode.ref !== undefined) {
|
|
1099
|
-
|
|
1100
|
-
this.commitRef(undefined, null, vnode.ref, instance);
|
|
1101
|
-
fiber.constructionJournal.refBound = true;
|
|
1102
|
-
fiber.constructionJournal.refOwner = instance;
|
|
1103
|
-
}
|
|
1104
|
-
catch (err) {
|
|
1105
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1106
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1107
|
-
if (isThenable(rollbackRes)) {
|
|
1108
|
-
return rollbackRes.then(() => {
|
|
1109
|
-
throw error;
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
throw error;
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
try {
|
|
1116
|
-
this.attachEffectableRuntimeBusWiring(instance, fiber);
|
|
1117
|
-
if (fiber.effectableRuntimeBusDisposer !== undefined) {
|
|
1118
|
-
fiber.constructionJournal.busWiringAttached = true;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
catch (err) {
|
|
1122
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, err instanceof Error ? err : new Error(String(err)));
|
|
1123
|
-
if (isThenable(rollbackRes)) {
|
|
1124
|
-
return rollbackRes.then(() => {
|
|
1125
|
-
throw err;
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
throw err;
|
|
549
|
+
vnode.ref.current = instance;
|
|
1129
550
|
}
|
|
551
|
+
this.attachEffectableRuntimeBusWiring(instance, fiber);
|
|
1130
552
|
// Run lifecycle after all children are materialized.
|
|
1131
553
|
// Pre-mount hook buffers setState from onMount until injectUpdateHook.
|
|
1132
554
|
this.injectPreMountUpdateHook(instance, fiber);
|
|
1133
555
|
const startupRes = engine.runStartup(instance);
|
|
1134
556
|
if (isThenable(startupRes)) {
|
|
1135
|
-
return this.finalizeMaterializeAsync(fiber, engine, startupRes);
|
|
557
|
+
return this.finalizeMaterializeAsync(fiber, engine, childFibers, startupRes);
|
|
1136
558
|
}
|
|
1137
559
|
if (!startupRes.ok) {
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
if (isThenable(
|
|
1141
|
-
return
|
|
1142
|
-
throw error;
|
|
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;
|
|
1143
565
|
});
|
|
1144
566
|
}
|
|
1145
|
-
throw error;
|
|
567
|
+
throw startupRes.error;
|
|
1146
568
|
}
|
|
1147
569
|
fiber.lifecycleStatus = engine.getStatus();
|
|
1148
570
|
fiber.effectTag = null;
|
|
1149
|
-
fiber.constructionJournal.schedulerHookAttached = true;
|
|
1150
571
|
this.injectUpdateHook(instance, fiber);
|
|
1151
572
|
return fiber;
|
|
1152
573
|
}
|
|
@@ -1159,83 +580,38 @@ class GraphRuntime {
|
|
|
1159
580
|
* @param {LifecycleEngine} engine - lifecycle engine
|
|
1160
581
|
* @param {VirtualServiceNode<P>} vnode - virtual node
|
|
1161
582
|
* @param {VirtualServiceNode[]} childVnodes - all child vnodes
|
|
1162
|
-
* @param {
|
|
583
|
+
* @param {RuntimeFiber<unknown>[]} childFibers - already materialized child fibers
|
|
1163
584
|
* @param {Promise<RuntimeFiber<unknown>>} pending - Promise for the current child
|
|
1164
585
|
* @param {number} pendingIdx - index of the current child
|
|
1165
586
|
* @returns {Promise<RuntimeFiber<P>>}
|
|
1166
587
|
*/
|
|
1167
|
-
async continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes,
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
journal.mountedChildren.push(await pending);
|
|
1171
|
-
}
|
|
1172
|
-
catch (err) {
|
|
1173
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1174
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1175
|
-
if (isThenable(rollbackRes)) {
|
|
1176
|
-
await rollbackRes;
|
|
1177
|
-
}
|
|
1178
|
-
throw error;
|
|
1179
|
-
}
|
|
588
|
+
async continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childFibers, pending, pendingIdx) {
|
|
589
|
+
const childScope = this.buildChildScope(instance, fiber.scope);
|
|
590
|
+
childFibers.push(await pending);
|
|
1180
591
|
for (let i = pendingIdx + 1; i < childVnodes.length; i++) {
|
|
1181
592
|
const childVnode = childVnodes[i];
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
const resolvedChild = isThenable(childRes) ? await childRes : childRes;
|
|
1185
|
-
journal.mountedChildren.push(resolvedChild);
|
|
1186
|
-
}
|
|
1187
|
-
catch (err) {
|
|
1188
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1189
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1190
|
-
if (isThenable(rollbackRes)) {
|
|
1191
|
-
await rollbackRes;
|
|
1192
|
-
}
|
|
1193
|
-
throw error;
|
|
1194
|
-
}
|
|
593
|
+
const childRes = this.materialize(childVnode, fiber, childScope);
|
|
594
|
+
childFibers.push(isThenable(childRes) ? await childRes : childRes);
|
|
1195
595
|
}
|
|
1196
|
-
fiber.children =
|
|
596
|
+
fiber.children = childFibers;
|
|
1197
597
|
if (vnode.ref !== undefined) {
|
|
1198
|
-
|
|
1199
|
-
this.commitRef(undefined, null, vnode.ref, instance);
|
|
1200
|
-
journal.refBound = true;
|
|
1201
|
-
journal.refOwner = instance;
|
|
1202
|
-
}
|
|
1203
|
-
catch (err) {
|
|
1204
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1205
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1206
|
-
if (isThenable(rollbackRes)) {
|
|
1207
|
-
await rollbackRes;
|
|
1208
|
-
}
|
|
1209
|
-
throw error;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
try {
|
|
1213
|
-
this.attachEffectableRuntimeBusWiring(instance, fiber);
|
|
1214
|
-
if (fiber.effectableRuntimeBusDisposer !== undefined) {
|
|
1215
|
-
journal.busWiringAttached = true;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
catch (err) {
|
|
1219
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, err instanceof Error ? err : new Error(String(err)));
|
|
1220
|
-
if (isThenable(rollbackRes)) {
|
|
1221
|
-
await rollbackRes;
|
|
1222
|
-
}
|
|
1223
|
-
throw err;
|
|
598
|
+
vnode.ref.current = instance;
|
|
1224
599
|
}
|
|
600
|
+
this.attachEffectableRuntimeBusWiring(instance, fiber);
|
|
1225
601
|
this.injectPreMountUpdateHook(instance, fiber);
|
|
1226
602
|
const startupRes = engine.runStartup(instance);
|
|
1227
603
|
const resolved = isThenable(startupRes) ? await startupRes : startupRes;
|
|
1228
604
|
if (!resolved.ok) {
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
605
|
+
for (const c of childFibers) {
|
|
606
|
+
const d = this.destroyFiber(c);
|
|
607
|
+
if (isThenable(d)) {
|
|
608
|
+
await d;
|
|
609
|
+
}
|
|
1233
610
|
}
|
|
1234
|
-
throw error;
|
|
611
|
+
throw resolved.error;
|
|
1235
612
|
}
|
|
1236
613
|
fiber.lifecycleStatus = engine.getStatus();
|
|
1237
614
|
fiber.effectTag = null;
|
|
1238
|
-
journal.schedulerHookAttached = true;
|
|
1239
615
|
this.injectUpdateHook(instance, fiber);
|
|
1240
616
|
return fiber;
|
|
1241
617
|
}
|
|
@@ -1245,28 +621,59 @@ class GraphRuntime {
|
|
|
1245
621
|
* @template P node props type
|
|
1246
622
|
* @param {RuntimeFiber<P>} fiber - fiber of the subtree root node
|
|
1247
623
|
* @param {LifecycleEngine} engine - lifecycle engine for this node
|
|
624
|
+
* @param {RuntimeFiber<unknown>[]} childFibers - already mounted child fibers (for rollback on error)
|
|
1248
625
|
* @param {PromiseLike<import('./lifecycle').LifecycleTransitionResult>} pendingStartup - Promise of the `runStartup` result
|
|
1249
626
|
* @returns {Promise<RuntimeFiber<P>>} ready fiber, or rollback children and rethrow
|
|
1250
627
|
*/
|
|
1251
|
-
async finalizeMaterializeAsync(fiber, engine, pendingStartup) {
|
|
628
|
+
async finalizeMaterializeAsync(fiber, engine, childFibers, pendingStartup) {
|
|
1252
629
|
const result = await pendingStartup;
|
|
1253
|
-
const journal = fiber.constructionJournal;
|
|
1254
630
|
if (!result.ok) {
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
631
|
+
for (const c of childFibers) {
|
|
632
|
+
const d = this.destroyFiber(c);
|
|
633
|
+
if (isThenable(d)) {
|
|
634
|
+
await d;
|
|
635
|
+
}
|
|
1259
636
|
}
|
|
1260
|
-
throw error;
|
|
637
|
+
throw result.error;
|
|
1261
638
|
}
|
|
1262
639
|
fiber.lifecycleStatus = engine.getStatus();
|
|
1263
640
|
fiber.effectTag = null;
|
|
1264
641
|
if (fiber.instance !== null) {
|
|
1265
|
-
journal.schedulerHookAttached = true;
|
|
1266
642
|
this.injectUpdateHook(fiber.instance, fiber);
|
|
1267
643
|
}
|
|
1268
644
|
return fiber;
|
|
1269
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
|
+
}
|
|
1270
677
|
// ---------------------------------------------------------------------------
|
|
1271
678
|
// Reconcile
|
|
1272
679
|
// ---------------------------------------------------------------------------
|
|
@@ -1318,27 +725,10 @@ class GraphRuntime {
|
|
|
1318
725
|
else {
|
|
1319
726
|
instance.props = nextVnode.props;
|
|
1320
727
|
}
|
|
1321
|
-
// Re-inject context fields when parent scope changed
|
|
1322
|
-
let contextChanged = false;
|
|
1323
|
-
if (current.scope !== parentScope) {
|
|
1324
|
-
try {
|
|
1325
|
-
contextChanged = (0, context_1.injectContextFields)(instance, parentScope);
|
|
1326
|
-
}
|
|
1327
|
-
catch (error) {
|
|
1328
|
-
const cleanupResult = this.runFiberFailedCleanup(current);
|
|
1329
|
-
if (isThenable(cleanupResult)) {
|
|
1330
|
-
return cleanupResult.then(() => {
|
|
1331
|
-
throw error;
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
throw error;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
728
|
// Build scope for child nodes (ContextProvider may have updated values)
|
|
1338
729
|
const childScope = this.buildChildScope(instance, parentScope);
|
|
1339
|
-
// Call onUpdate if props
|
|
1340
|
-
|
|
1341
|
-
if ((propsChanged || contextChanged) && current.engine.canUpdate()) {
|
|
730
|
+
// Call onUpdate if props changed
|
|
731
|
+
if (prevProps !== instance.props && current.engine.canUpdate()) {
|
|
1342
732
|
try {
|
|
1343
733
|
instance.onUpdate(prevProps, instance.props);
|
|
1344
734
|
}
|
|
@@ -1352,8 +742,10 @@ class GraphRuntime {
|
|
|
1352
742
|
throw error;
|
|
1353
743
|
}
|
|
1354
744
|
}
|
|
1355
|
-
//
|
|
1356
|
-
|
|
745
|
+
// Update ref
|
|
746
|
+
if (nextVnode.ref !== undefined) {
|
|
747
|
+
nextVnode.ref.current = instance;
|
|
748
|
+
}
|
|
1357
749
|
// Reconcile child nodes (sync fast-path if all children are sync).
|
|
1358
750
|
let nextChildVnodes;
|
|
1359
751
|
try {
|
|
@@ -1477,60 +869,17 @@ class GraphRuntime {
|
|
|
1477
869
|
}
|
|
1478
870
|
return resultSoFar;
|
|
1479
871
|
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Validates that sibling keys are unique within a list.
|
|
1482
|
-
* Follows React v16.5 keyed child reconciliation contract: duplicate keys are invalid.
|
|
1483
|
-
* Throws a descriptive error including the duplicate key and parent component identity.
|
|
1484
|
-
*
|
|
1485
|
-
* @param {Array<{ vnode: { key?: string }; instance?: Component<unknown, unknown> | null }>} items - list of fibers or vnodes
|
|
1486
|
-
* @param {RuntimeFiber<unknown>} parentFiber - parent fiber (for error message)
|
|
1487
|
-
* @param {string} listName - "current" or "next" (for error message)
|
|
1488
|
-
* @returns {void}
|
|
1489
|
-
* @throws {Error} when duplicate keys are detected
|
|
1490
|
-
*/
|
|
1491
|
-
validateUniqueKeys(items, parentFiber, listName) {
|
|
1492
|
-
const seenKeys = new Set();
|
|
1493
|
-
for (const item of items) {
|
|
1494
|
-
const key = item.vnode.key;
|
|
1495
|
-
if (key !== undefined) {
|
|
1496
|
-
if (seenKeys.has(key)) {
|
|
1497
|
-
const parentInstance = parentFiber.instance;
|
|
1498
|
-
const parentName = parentInstance !== null
|
|
1499
|
-
? parentInstance.constructor.name
|
|
1500
|
-
: 'unknown';
|
|
1501
|
-
throw new Error(`[Effectable] GraphRuntime: duplicate key "${key}" in ${listName} children of ${parentName}. ` +
|
|
1502
|
-
`Sibling keys must be unique (React v16.5 keyed child reconciliation contract). ` +
|
|
1503
|
-
`Duplicates cause undefined matching behavior and lifecycle leaks.`);
|
|
1504
|
-
}
|
|
1505
|
-
seenKeys.add(key);
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
872
|
/**
|
|
1510
873
|
* Full-diff reconcile: keyed/unkeyed Map + destroy orphans.
|
|
1511
874
|
* Always async — internal branching is too complex for an efficient sync path.
|
|
1512
875
|
*
|
|
1513
|
-
* Contract: Sibling keys must be unique (React v16.5 keyed child reconciliation).
|
|
1514
|
-
* Validates both current and next children BEFORE any side effects.
|
|
1515
|
-
* Throws deterministic error on duplicate keys to prevent lifecycle leaks.
|
|
1516
|
-
*
|
|
1517
|
-
* HOLE 3: On throw during PLACE, cleans up previously placed new nodes
|
|
1518
|
-
* to prevent lifecycle leaks. Uses identity-safe check against currentChildren Set.
|
|
1519
|
-
*
|
|
1520
876
|
* @param {RuntimeFiber<unknown>[]} currentChildren - current child fibers
|
|
1521
877
|
* @param {VirtualServiceNode[]} nextVnodes - new vnodes
|
|
1522
878
|
* @param {RuntimeFiber<unknown>} parentFiber - parent fiber
|
|
1523
879
|
* @param {ContextScope} childScope - children scope
|
|
1524
880
|
* @returns {Promise<RuntimeFiber<unknown>[]>}
|
|
1525
|
-
* @throws {Error} when duplicate keys are detected in current or next children
|
|
1526
881
|
*/
|
|
1527
882
|
async reconcileChildrenFullDiff(currentChildren, nextVnodes, parentFiber, childScope) {
|
|
1528
|
-
// Validate unique keys BEFORE any side effects (Option A: React v16.5 contract)
|
|
1529
|
-
this.validateUniqueKeys(currentChildren, parentFiber, 'current');
|
|
1530
|
-
this.validateUniqueKeys(nextVnodes.map(vnode => ({ vnode, instance: null })), parentFiber, 'next');
|
|
1531
|
-
// HOLE 3: Build identity Set of currentChildren for rollback
|
|
1532
|
-
// Used to distinguish UPDATE (same object) from PLACE/REPLACE (new object)
|
|
1533
|
-
const currentChildrenSet = new Set(currentChildren);
|
|
1534
883
|
// Check for keyed children before creating a Map (6.06x speedup for unkeyed-only)
|
|
1535
884
|
let hasKeyedCurrent = false;
|
|
1536
885
|
for (const child of currentChildren) {
|
|
@@ -1542,65 +891,33 @@ class GraphRuntime {
|
|
|
1542
891
|
const unkeyedCurrent = [];
|
|
1543
892
|
const nextChildren = [];
|
|
1544
893
|
let unkeyedIdx = 0;
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
keyedCurrentMap.set(key, child);
|
|
1556
|
-
}
|
|
1557
|
-
else {
|
|
1558
|
-
unkeyedCurrent.push(child);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
for (const nextVnode of nextVnodes) {
|
|
1562
|
-
const nextKey = nextVnode.key;
|
|
1563
|
-
if (nextKey !== undefined && keyedCurrentMap.has(nextKey)) {
|
|
1564
|
-
const currentFiber = keyedCurrentMap.get(nextKey);
|
|
1565
|
-
if (currentFiber === undefined) {
|
|
1566
|
-
throw new Error(`[Effectable] GraphRuntime: fiber with key "${nextKey}" not found in map.`);
|
|
1567
|
-
}
|
|
1568
|
-
keyedCurrentMap.delete(nextKey);
|
|
1569
|
-
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
1570
|
-
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
1571
|
-
}
|
|
1572
|
-
else if (nextKey === undefined && unkeyedIdx < unkeyedCurrent.length) {
|
|
1573
|
-
const currentFiber = unkeyedCurrent[unkeyedIdx];
|
|
1574
|
-
unkeyedIdx += 1;
|
|
1575
|
-
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
1576
|
-
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
1577
|
-
}
|
|
1578
|
-
else {
|
|
1579
|
-
// New node — PLACE
|
|
1580
|
-
const newRes = this.materialize(nextVnode, parentFiber, childScope);
|
|
1581
|
-
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
1582
|
-
}
|
|
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);
|
|
1583
904
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
const d = this.destroyFiber(orphan);
|
|
1587
|
-
if (isThenable(d)) {
|
|
1588
|
-
await d;
|
|
1589
|
-
}
|
|
905
|
+
else {
|
|
906
|
+
unkeyedCurrent.push(child);
|
|
1590
907
|
}
|
|
1591
908
|
}
|
|
1592
|
-
finally {
|
|
1593
|
-
this.reconcileDepth--;
|
|
1594
|
-
this.releaseKeyedMap();
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
else {
|
|
1598
|
-
// No keyed children — skip Map, positional reconcile
|
|
1599
|
-
for (const child of currentChildren) {
|
|
1600
|
-
unkeyedCurrent.push(child);
|
|
1601
|
-
}
|
|
1602
909
|
for (const nextVnode of nextVnodes) {
|
|
1603
|
-
|
|
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) {
|
|
1604
921
|
const currentFiber = unkeyedCurrent[unkeyedIdx];
|
|
1605
922
|
unkeyedIdx += 1;
|
|
1606
923
|
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
@@ -1612,48 +929,49 @@ class GraphRuntime {
|
|
|
1612
929
|
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
1613
930
|
}
|
|
1614
931
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
for (let i = unkeyedIdx; i < unkeyedCurrent.length; i += 1) {
|
|
1618
|
-
const orphan = unkeyedCurrent[i];
|
|
1619
|
-
if (orphan !== undefined) {
|
|
932
|
+
// Destroy remaining unpaired current children (keyed)
|
|
933
|
+
for (const [, orphan] of keyedCurrentMap) {
|
|
1620
934
|
const d = this.destroyFiber(orphan);
|
|
1621
935
|
if (isThenable(d)) {
|
|
1622
936
|
await d;
|
|
1623
937
|
}
|
|
1624
938
|
}
|
|
1625
939
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
for (const child of
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
await d;
|
|
1643
|
-
}
|
|
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);
|
|
1644
956
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
957
|
+
else {
|
|
958
|
+
// New node — PLACE
|
|
959
|
+
const newRes = this.materialize(nextVnode, parentFiber, childScope);
|
|
960
|
+
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
1647
961
|
}
|
|
1648
962
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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
|
+
}
|
|
1654
972
|
}
|
|
1655
|
-
throw primaryError;
|
|
1656
973
|
}
|
|
974
|
+
return nextChildren;
|
|
1657
975
|
}
|
|
1658
976
|
/**
|
|
1659
977
|
* Whether children are stable: same count, same type and key per position.
|
|
@@ -1713,31 +1031,17 @@ class GraphRuntime {
|
|
|
1713
1031
|
* Returns `void` synchronously if the whole subtree is sync (up to 266x speedup
|
|
1714
1032
|
* on an 85-node tree); otherwise a Promise. `await` works correctly with either union branch.
|
|
1715
1033
|
*
|
|
1716
|
-
* Collects cleanup errors via `collectErrors` parameter (best-effort cleanup).
|
|
1717
|
-
*
|
|
1718
1034
|
* @param {RuntimeFiber} fiber - fiber to destroy
|
|
1719
|
-
* @param {Error[] | null} collectErrors - array to collect cleanup errors (null to throw immediately)
|
|
1720
1035
|
* @returns {void | Promise<void>}
|
|
1721
1036
|
*/
|
|
1722
|
-
destroyFiber(fiber
|
|
1037
|
+
destroyFiber(fiber) {
|
|
1723
1038
|
const children = fiber.children;
|
|
1724
1039
|
const n = children.length;
|
|
1725
1040
|
// Sync recursion over children until the first async
|
|
1726
1041
|
for (let i = 0; i < n; i++) {
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
return this.continueDestroyAsync(fiber, children, i, childRes, collectErrors);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
catch (err) {
|
|
1734
|
-
// Best-effort cleanup — collect error and continue
|
|
1735
|
-
if (collectErrors !== null) {
|
|
1736
|
-
collectErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1737
|
-
}
|
|
1738
|
-
else {
|
|
1739
|
-
throw err;
|
|
1740
|
-
}
|
|
1042
|
+
const childRes = this.destroyFiber(children[i]);
|
|
1043
|
+
if (isThenable(childRes)) {
|
|
1044
|
+
return this.continueDestroyAsync(fiber, children, i, childRes);
|
|
1741
1045
|
}
|
|
1742
1046
|
}
|
|
1743
1047
|
const instance = fiber.instance;
|
|
@@ -1748,17 +1052,15 @@ class GraphRuntime {
|
|
|
1748
1052
|
this.dirtyFibers.delete(fiber);
|
|
1749
1053
|
const shutdownRes = fiber.engine.runShutdown(instance);
|
|
1750
1054
|
if (isThenable(shutdownRes)) {
|
|
1751
|
-
return this.finalizeDestroyAsync(fiber, shutdownRes
|
|
1055
|
+
return this.finalizeDestroyAsync(fiber, shutdownRes);
|
|
1752
1056
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1057
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1058
|
+
// Clear ref after unmount
|
|
1059
|
+
const ref = fiber.vnode.ref;
|
|
1060
|
+
if (ref !== undefined) {
|
|
1061
|
+
ref.current = null;
|
|
1758
1062
|
}
|
|
1759
|
-
|
|
1760
|
-
// finalizeFiberDestroy uses commitRef for identity-safe ref clearing
|
|
1761
|
-
this.finalizeFiberDestroy(fiber, collectErrors);
|
|
1063
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1762
1064
|
}
|
|
1763
1065
|
/**
|
|
1764
1066
|
* Async continuation of {@link destroyFiber} after one of the children returned a Promise.
|
|
@@ -1767,37 +1069,14 @@ class GraphRuntime {
|
|
|
1767
1069
|
* @param {Fiber[]} children - children list
|
|
1768
1070
|
* @param {number} pendingIdx - index of the pending child
|
|
1769
1071
|
* @param {PromiseLike<void>} pending - Promise from destroying the child
|
|
1770
|
-
* @param {Error[] | null} collectErrors - array to collect cleanup errors
|
|
1771
1072
|
* @returns {Promise<void>}
|
|
1772
1073
|
*/
|
|
1773
|
-
async continueDestroyAsync(fiber, children, pendingIdx, pending
|
|
1774
|
-
|
|
1775
|
-
try {
|
|
1776
|
-
await pending;
|
|
1777
|
-
}
|
|
1778
|
-
catch (err) {
|
|
1779
|
-
if (collectErrors !== null) {
|
|
1780
|
-
collectErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1781
|
-
}
|
|
1782
|
-
else {
|
|
1783
|
-
throw err;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
// Continue destroying remaining children even if previous failed
|
|
1074
|
+
async continueDestroyAsync(fiber, children, pendingIdx, pending) {
|
|
1075
|
+
await pending;
|
|
1787
1076
|
for (let i = pendingIdx + 1; i < children.length; i++) {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
await r;
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
catch (err) {
|
|
1795
|
-
if (collectErrors !== null) {
|
|
1796
|
-
collectErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1797
|
-
}
|
|
1798
|
-
else {
|
|
1799
|
-
throw err;
|
|
1800
|
-
}
|
|
1077
|
+
const r = this.destroyFiber(children[i]);
|
|
1078
|
+
if (isThenable(r)) {
|
|
1079
|
+
await r;
|
|
1801
1080
|
}
|
|
1802
1081
|
}
|
|
1803
1082
|
const instance = fiber.instance;
|
|
@@ -1808,21 +1087,14 @@ class GraphRuntime {
|
|
|
1808
1087
|
this.dirtyFibers.delete(fiber);
|
|
1809
1088
|
const shutdownRes = fiber.engine.runShutdown(instance);
|
|
1810
1089
|
if (isThenable(shutdownRes)) {
|
|
1811
|
-
|
|
1812
|
-
if (typeof asyncRes === 'object' && asyncRes !== null && 'ok' in asyncRes && !asyncRes.ok) {
|
|
1813
|
-
if (collectErrors !== null) {
|
|
1814
|
-
collectErrors.push(asyncRes.error instanceof Error ? asyncRes.error : new Error(String(asyncRes.error)));
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1090
|
+
await shutdownRes;
|
|
1817
1091
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1092
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1093
|
+
const ref = fiber.vnode.ref;
|
|
1094
|
+
if (ref !== undefined) {
|
|
1095
|
+
ref.current = null;
|
|
1822
1096
|
}
|
|
1823
|
-
|
|
1824
|
-
// finalizeFiberDestroy uses commitRef for identity-safe ref clearing
|
|
1825
|
-
this.finalizeFiberDestroy(fiber, collectErrors);
|
|
1097
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1826
1098
|
}
|
|
1827
1099
|
/**
|
|
1828
1100
|
* Async finalization of {@link destroyFiber} when children were destroyed synchronously
|
|
@@ -1830,21 +1102,16 @@ class GraphRuntime {
|
|
|
1830
1102
|
*
|
|
1831
1103
|
* @param {RuntimeFiber<unknown>} fiber
|
|
1832
1104
|
* @param {PromiseLike<unknown>} pendingShutdown
|
|
1833
|
-
* @param {Error[] | null} collectErrors - array to collect cleanup errors
|
|
1834
1105
|
* @returns {Promise<void>}
|
|
1835
1106
|
*/
|
|
1836
|
-
async finalizeDestroyAsync(fiber, pendingShutdown
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
collectErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1843
|
-
}
|
|
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;
|
|
1844
1113
|
}
|
|
1845
|
-
|
|
1846
|
-
// finalizeFiberDestroy uses commitRef for identity-safe ref clearing
|
|
1847
|
-
this.finalizeFiberDestroy(fiber, collectErrors);
|
|
1114
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1848
1115
|
}
|
|
1849
1116
|
// ---------------------------------------------------------------------------
|
|
1850
1117
|
// Helpers
|