effectable 1.1.0 → 1.1.1-canary.1
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/build/bootstrap/bootstrap.d.ts.map +1 -1
- package/build/bootstrap/bootstrap.js +6 -2
- package/build/bootstrap/bootstrap.js.map +1 -1
- package/build/bootstrap/types.d.ts +4 -2
- package/build/bootstrap/types.d.ts.map +1 -1
- package/build/component/Component.d.ts.map +1 -1
- package/build/component/Component.js +16 -4
- package/build/component/Component.js.map +1 -1
- package/build/component/GraphRuntime.d.ts +29 -7
- package/build/component/GraphRuntime.d.ts.map +1 -1
- package/build/component/GraphRuntime.js +233 -72
- package/build/component/GraphRuntime.js.map +1 -1
- package/build/component/lifecycle.d.ts +11 -2
- package/build/component/lifecycle.d.ts.map +1 -1
- package/build/component/lifecycle.js +21 -4
- package/build/component/lifecycle.js.map +1 -1
- package/build/connect/connect.d.ts.map +1 -1
- package/build/connect/connect.js +162 -34
- package/build/connect/connect.js.map +1 -1
- package/build/runtime/BusDecorators.d.ts.map +1 -1
- package/build/runtime/BusDecorators.js +18 -9
- package/build/runtime/BusDecorators.js.map +1 -1
- package/build/runtime/EventBus.d.ts.map +1 -1
- package/build/runtime/EventBus.js +11 -3
- package/build/runtime/EventBus.js.map +1 -1
- package/build/store/middleware.d.ts.map +1 -1
- package/build/store/middleware.js +15 -4
- package/build/store/middleware.js.map +1 -1
- package/build/store/selector.d.ts.map +1 -1
- package/build/store/selector.js +6 -2
- package/build/store/selector.js.map +1 -1
- package/package.json +1 -1
|
@@ -354,8 +354,9 @@ class GraphRuntime {
|
|
|
354
354
|
journal.rolledBack = true;
|
|
355
355
|
const cleanupErrors = [];
|
|
356
356
|
const instance = fiber.instance;
|
|
357
|
-
// 1. Disable scheduler hook
|
|
358
|
-
|
|
357
|
+
// 1. Disable scheduler hook (pre-mount buffer may be attached before children,
|
|
358
|
+
// before journal.schedulerHookAttached is set after successful startup).
|
|
359
|
+
if (instance !== null) {
|
|
359
360
|
try {
|
|
360
361
|
this.clearUpdateHook(instance);
|
|
361
362
|
this.dirtyFibers.delete(fiber);
|
|
@@ -382,46 +383,29 @@ class GraphRuntime {
|
|
|
382
383
|
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
383
384
|
}
|
|
384
385
|
}
|
|
385
|
-
// 4.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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.
|
|
386
|
+
// 4. Destroy mounted children in reverse order BEFORE parent onUnmount.
|
|
387
|
+
// Documented teardown contract is children → parent; running parent
|
|
388
|
+
// runFailedCleanup first left children alive during parent onUnmount and
|
|
389
|
+
// also called parent onUnmount when startup never ran (wasMounted=true).
|
|
399
390
|
// Pass cleanupErrors so nested destroy is best-effort: a throwing
|
|
400
391
|
// ref-clear/disposer on one grandchild must not skip remaining siblings.
|
|
401
392
|
// Those nodes were never attached to currentRoot, so failStop cannot reclaim them.
|
|
402
|
-
const
|
|
393
|
+
const destroyChildrenThenParentCleanup = () => {
|
|
403
394
|
const children = journal.mountedChildren;
|
|
404
395
|
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
405
396
|
try {
|
|
406
397
|
const destroyRes = this.destroyFiber(children[i], cleanupErrors);
|
|
407
398
|
if (isThenable(destroyRes)) {
|
|
408
|
-
return this.continueRollbackDestroyAsync(children, i, destroyRes, primaryError, cleanupErrors);
|
|
399
|
+
return this.continueRollbackDestroyAsync(children, i, destroyRes, primaryError, cleanupErrors, fiber, instance);
|
|
409
400
|
}
|
|
410
401
|
}
|
|
411
402
|
catch (err) {
|
|
412
403
|
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
413
404
|
}
|
|
414
405
|
}
|
|
415
|
-
|
|
416
|
-
this.finalizeRollback(primaryError, cleanupErrors);
|
|
406
|
+
return this.finishRollbackParentCleanup(fiber, instance, primaryError, cleanupErrors);
|
|
417
407
|
};
|
|
418
|
-
|
|
419
|
-
return cleanupPromise.then(() => destroyChildrenAndFinalize(), (err) => {
|
|
420
|
-
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
421
|
-
return destroyChildrenAndFinalize();
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
return destroyChildrenAndFinalize();
|
|
408
|
+
return destroyChildrenThenParentCleanup();
|
|
425
409
|
}
|
|
426
410
|
/**
|
|
427
411
|
* Async continuation of rollback child destruction after one child's destroy returned a Promise.
|
|
@@ -431,9 +415,11 @@ class GraphRuntime {
|
|
|
431
415
|
* @param {Promise<void>} pending - Promise from destroying the previous child
|
|
432
416
|
* @param {Error} primaryError - original materialization error
|
|
433
417
|
* @param {Error[]} cleanupErrors - accumulated cleanup errors
|
|
418
|
+
* @param {RuntimeFiber<unknown>} fiber - parent fiber being rolled back
|
|
419
|
+
* @param {Component<unknown, unknown> | null} instance - parent instance (for post-child cleanup)
|
|
434
420
|
* @returns {Promise<void>}
|
|
435
421
|
*/
|
|
436
|
-
async continueRollbackDestroyAsync(children, lastIdx, pending, primaryError, cleanupErrors) {
|
|
422
|
+
async continueRollbackDestroyAsync(children, lastIdx, pending, primaryError, cleanupErrors, fiber, instance) {
|
|
437
423
|
try {
|
|
438
424
|
await pending;
|
|
439
425
|
}
|
|
@@ -451,6 +437,44 @@ class GraphRuntime {
|
|
|
451
437
|
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
452
438
|
}
|
|
453
439
|
}
|
|
440
|
+
const cleanupRes = this.finishRollbackParentCleanup(fiber, instance, primaryError, cleanupErrors);
|
|
441
|
+
if (isThenable(cleanupRes)) {
|
|
442
|
+
await cleanupRes;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* After rollback destroyed children: run parent failed-cleanup only when startup
|
|
447
|
+
* actually ran (`status !== 'registered'`), then attach cleanup errors and rethrow.
|
|
448
|
+
*
|
|
449
|
+
* @param {RuntimeFiber<unknown>} fiber - parent fiber being rolled back
|
|
450
|
+
* @param {Component<unknown, unknown> | null} instance - parent instance
|
|
451
|
+
* @param {Error} primaryError - original materialization error
|
|
452
|
+
* @param {Error[]} cleanupErrors - accumulated cleanup errors
|
|
453
|
+
* @returns {void | Promise<void>} always rejects via {@link finalizeRollback}
|
|
454
|
+
*/
|
|
455
|
+
finishRollbackParentCleanup(fiber, instance, primaryError, cleanupErrors) {
|
|
456
|
+
if (instance !== null) {
|
|
457
|
+
// `registered` ⇒ runStartup never entered; do not invent an onUnmount.
|
|
458
|
+
// `failed` (deferFailedCleanup) / other post-startup statuses ⇒ wasMounted.
|
|
459
|
+
const wasMounted = fiber.engine.getStatus() !== 'registered';
|
|
460
|
+
if (wasMounted) {
|
|
461
|
+
try {
|
|
462
|
+
const cleanupRes = fiber.engine.runFailedCleanup(instance, true);
|
|
463
|
+
if (isThenable(cleanupRes)) {
|
|
464
|
+
return cleanupRes.then(() => {
|
|
465
|
+
this.finalizeRollback(primaryError, cleanupErrors);
|
|
466
|
+
}, (err) => {
|
|
467
|
+
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
468
|
+
this.finalizeRollback(primaryError, cleanupErrors);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
477
|
+
}
|
|
454
478
|
this.finalizeRollback(primaryError, cleanupErrors);
|
|
455
479
|
}
|
|
456
480
|
/**
|
|
@@ -486,10 +510,12 @@ class GraphRuntime {
|
|
|
486
510
|
}
|
|
487
511
|
}
|
|
488
512
|
/**
|
|
489
|
-
* Pre-mount buffer: `setState`
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
513
|
+
* Pre-mount buffer: `setState` cannot yet schedule reconcile (the live hook is
|
|
514
|
+
* injected after startup). Marks the fiber; {@link injectUpdateHook} after
|
|
515
|
+
* startup will call {@link scheduleUpdate} (deferred until the mount pass completes).
|
|
516
|
+
*
|
|
517
|
+
* Injected before child materialization so descendant `onMount` callbacks that
|
|
518
|
+
* `setState` an ancestor are buffered instead of silently dropped.
|
|
493
519
|
*
|
|
494
520
|
* @param {Component<unknown, unknown>} instance - instance before/during startup
|
|
495
521
|
* @param {RuntimeFiber<unknown>} fiber - instance fiber
|
|
@@ -608,16 +634,30 @@ class GraphRuntime {
|
|
|
608
634
|
finally {
|
|
609
635
|
this.operationInProgress = false;
|
|
610
636
|
}
|
|
637
|
+
// setState during reconcile/unmount defers the microtask (operationInProgress).
|
|
638
|
+
// Kick once the queue is idle so the flush cannot overlap in-flight graph mutations.
|
|
639
|
+
if (this.dirtyFibers.size > 0 && this._state === RUNTIME_STATE.ACTIVE) {
|
|
640
|
+
this.scheduleDirtyFlushMicrotask();
|
|
641
|
+
}
|
|
611
642
|
}
|
|
612
643
|
/**
|
|
613
644
|
* Queues one dirty-flush microtask and publishes {@link activeFlush} for await from `reconcile`.
|
|
614
645
|
*
|
|
615
|
-
* Skip scheduling when runtime is FAILED
|
|
646
|
+
* Skip scheduling when runtime is not ACTIVE (IDLE / FAILED / UNMOUNTING / UNMOUNTED),
|
|
647
|
+
* a public graph operation is in flight, or an async flush is already running. Callers that
|
|
648
|
+
* mutate `dirtyFibers` during those windows must kick this method again after the tree is
|
|
649
|
+
* ACTIVE and idle ({@link GraphRuntime.mount} / {@link processOperationQueue}), or after the
|
|
650
|
+
* outer flush finishes (end-of-pass kick).
|
|
616
651
|
*
|
|
617
652
|
* @returns {void}
|
|
618
653
|
*/
|
|
619
654
|
scheduleDirtyFlushMicrotask() {
|
|
620
|
-
|
|
655
|
+
// Skip while an async flush is in flight: setState during PLACE/onMount only
|
|
656
|
+
// enqueues dirtyFibers; the outer pass kicks the next microtask when it finishes.
|
|
657
|
+
if (this.flushScheduled ||
|
|
658
|
+
this.flushing ||
|
|
659
|
+
this.operationInProgress ||
|
|
660
|
+
this.state !== RUNTIME_STATE.ACTIVE) {
|
|
621
661
|
return;
|
|
622
662
|
}
|
|
623
663
|
this.flushScheduled = true;
|
|
@@ -652,10 +692,20 @@ class GraphRuntime {
|
|
|
652
692
|
*/
|
|
653
693
|
async flushDirtyFibers() {
|
|
654
694
|
this.flushScheduled = false;
|
|
655
|
-
if (this.state === RUNTIME_STATE.FAILED || this.state === RUNTIME_STATE.UNMOUNTING || this.state === RUNTIME_STATE.UNMOUNTED
|
|
695
|
+
if (this.state === RUNTIME_STATE.FAILED || this.state === RUNTIME_STATE.UNMOUNTING || this.state === RUNTIME_STATE.UNMOUNTED) {
|
|
656
696
|
this.dirtyFibers.clear();
|
|
657
697
|
return;
|
|
658
698
|
}
|
|
699
|
+
// Re-entrant call while an async flush awaits: keep dirtyFibers for the outer
|
|
700
|
+
// pass's end-of-flush kick. Clearing here would silently drop setState updates.
|
|
701
|
+
if (this.flushing) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
// Do not mutate the graph until mount has published currentRoot, and never
|
|
705
|
+
// overlap a public reconcile/unmount (those leave dirtyFibers queued).
|
|
706
|
+
if (this.state !== RUNTIME_STATE.ACTIVE || this.operationInProgress) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
659
709
|
this.dirtyFlushPassCount += 1;
|
|
660
710
|
this.flushing = true;
|
|
661
711
|
const snapshot = Array.from(this.dirtyFibers);
|
|
@@ -673,9 +723,17 @@ class GraphRuntime {
|
|
|
673
723
|
}
|
|
674
724
|
catch (error) {
|
|
675
725
|
this.flushing = false;
|
|
676
|
-
// Notify error handler before fail-stop
|
|
726
|
+
// Notify error handler before fail-stop. The hook must not be allowed to
|
|
727
|
+
// skip fail-stop: a throwing observer would leave the runtime ACTIVE with
|
|
728
|
+
// fibers already run through runFiberFailedCleanup, and the microtask
|
|
729
|
+
// `.catch(() => {})` would swallow the failure silently.
|
|
677
730
|
if (this.onAutoReconcileError !== null) {
|
|
678
|
-
|
|
731
|
+
try {
|
|
732
|
+
this.onAutoReconcileError(error);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// Observer/logging failures are non-fatal relative to fail-stop.
|
|
736
|
+
}
|
|
679
737
|
}
|
|
680
738
|
// Fail-stop on unrecoverable dirty-flush error
|
|
681
739
|
const failError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -694,9 +752,14 @@ class GraphRuntime {
|
|
|
694
752
|
this.dirtyFibers.clear();
|
|
695
753
|
this.dirtyFlushPassCount = 0;
|
|
696
754
|
const loopError = new Error(`GraphRuntime: dirty flush exceeded ${String(graphRuntime_constants_1.GRAPH_RUNTIME_MAX_DIRTY_FLUSH_PASSES)} passes (anti-loop)`);
|
|
697
|
-
//
|
|
755
|
+
// Same contract as the catch path: observer throw must not skip fail-stop.
|
|
698
756
|
if (this.onAutoReconcileError !== null) {
|
|
699
|
-
|
|
757
|
+
try {
|
|
758
|
+
this.onAutoReconcileError(loopError);
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
// Observer/logging failures are non-fatal relative to fail-stop.
|
|
762
|
+
}
|
|
700
763
|
}
|
|
701
764
|
// Fail-stop on loop limit
|
|
702
765
|
const failRes = this.failStop(loopError);
|
|
@@ -736,7 +799,7 @@ class GraphRuntime {
|
|
|
736
799
|
return childrenRes.then((nextChildren) => {
|
|
737
800
|
fiber.children = nextChildren;
|
|
738
801
|
}, (error) => {
|
|
739
|
-
const cleanupResult = this.runFiberFailedCleanup(fiber);
|
|
802
|
+
const cleanupResult = this.runFiberFailedCleanup(fiber, error);
|
|
740
803
|
if (isThenable(cleanupResult)) {
|
|
741
804
|
return cleanupResult.then(() => {
|
|
742
805
|
throw error;
|
|
@@ -748,7 +811,7 @@ class GraphRuntime {
|
|
|
748
811
|
fiber.children = childrenRes;
|
|
749
812
|
}
|
|
750
813
|
catch (error) {
|
|
751
|
-
const cleanupResult = this.runFiberFailedCleanup(fiber);
|
|
814
|
+
const cleanupResult = this.runFiberFailedCleanup(fiber, error);
|
|
752
815
|
if (isThenable(cleanupResult)) {
|
|
753
816
|
return cleanupResult.then(() => {
|
|
754
817
|
throw error;
|
|
@@ -777,7 +840,13 @@ class GraphRuntime {
|
|
|
777
840
|
try {
|
|
778
841
|
const res = rt.materialize(root, null, initialScope);
|
|
779
842
|
rt.currentRoot = isThenable(res) ? await res : res;
|
|
843
|
+
if (rt._state === RUNTIME_STATE.FAILED) {
|
|
844
|
+
throw rt.terminalError ?? new Error('[Effectable] GraphRuntime: terminal failure during mount.');
|
|
845
|
+
}
|
|
780
846
|
rt.state = RUNTIME_STATE.ACTIVE;
|
|
847
|
+
if (rt.dirtyFibers.size > 0) {
|
|
848
|
+
rt.scheduleDirtyFlushMicrotask();
|
|
849
|
+
}
|
|
781
850
|
return rt;
|
|
782
851
|
}
|
|
783
852
|
catch (error) {
|
|
@@ -1069,11 +1138,30 @@ class GraphRuntime {
|
|
|
1069
1138
|
mountedChildren: [],
|
|
1070
1139
|
},
|
|
1071
1140
|
};
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1141
|
+
// Buffer setState before children mount: a descendant onMount may call
|
|
1142
|
+
// ancestor setState (via props callback). Without this hook, that update
|
|
1143
|
+
// mutates state but never schedules reconcile.
|
|
1144
|
+
this.injectPreMountUpdateHook(instance, fiber);
|
|
1145
|
+
// compose() / duplicate-key validation can throw after the pre-mount hook is
|
|
1146
|
+
// attached. Roll back so the hook is cleared (otherwise the failed fiber is
|
|
1147
|
+
// abandoned with a live SCHEDULE_UPDATE_HOOK and no teardown).
|
|
1148
|
+
let childVnodes;
|
|
1149
|
+
try {
|
|
1150
|
+
childVnodes = this.getChildVnodes(instance, vnode.children);
|
|
1151
|
+
// Validate unique keys BEFORE materialization (Option A: React v16.5 contract)
|
|
1152
|
+
// Prevent partial tree construction when duplicate keys are present
|
|
1153
|
+
this.validateUniqueKeys(childVnodes.map(vnode => ({ vnode, instance: null })), fiber, 'current');
|
|
1154
|
+
}
|
|
1155
|
+
catch (err) {
|
|
1156
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1157
|
+
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1158
|
+
if (isThenable(rollbackRes)) {
|
|
1159
|
+
return rollbackRes.then(() => {
|
|
1160
|
+
throw error;
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
throw error;
|
|
1164
|
+
}
|
|
1077
1165
|
for (let i = 0; i < childVnodes.length; i++) {
|
|
1078
1166
|
const childVnode = childVnodes[i];
|
|
1079
1167
|
let childRes;
|
|
@@ -1131,9 +1219,9 @@ class GraphRuntime {
|
|
|
1131
1219
|
throw err;
|
|
1132
1220
|
}
|
|
1133
1221
|
// Run lifecycle after all children are materialized.
|
|
1134
|
-
// Pre-mount hook
|
|
1135
|
-
this
|
|
1136
|
-
const startupRes = engine.runStartup(instance);
|
|
1222
|
+
// Pre-mount hook was injected before children (covers ancestor setState
|
|
1223
|
+
// from descendant onMount and setState during this node's onMount).
|
|
1224
|
+
const startupRes = engine.runStartup(instance, { deferFailedCleanup: true });
|
|
1137
1225
|
if (isThenable(startupRes)) {
|
|
1138
1226
|
return this.finalizeMaterializeAsync(fiber, engine, startupRes);
|
|
1139
1227
|
}
|
|
@@ -1225,8 +1313,8 @@ class GraphRuntime {
|
|
|
1225
1313
|
}
|
|
1226
1314
|
throw err;
|
|
1227
1315
|
}
|
|
1228
|
-
|
|
1229
|
-
const startupRes = engine.runStartup(instance);
|
|
1316
|
+
// Pre-mount hook already injected at the start of materialize (before children).
|
|
1317
|
+
const startupRes = engine.runStartup(instance, { deferFailedCleanup: true });
|
|
1230
1318
|
const resolved = isThenable(startupRes) ? await startupRes : startupRes;
|
|
1231
1319
|
if (!resolved.ok) {
|
|
1232
1320
|
const error = resolved.error instanceof Error ? resolved.error : new Error(String(resolved.error));
|
|
@@ -1292,7 +1380,9 @@ class GraphRuntime {
|
|
|
1292
1380
|
}
|
|
1293
1381
|
// Type or key changed — destroy the old node, create a new one.
|
|
1294
1382
|
// Sync fast-path if both destroy and materialize completed synchronously.
|
|
1295
|
-
|
|
1383
|
+
// Collect cleanup errors (ref clear / disposer) so a throwing finalize cannot
|
|
1384
|
+
// abort REPLACE and fail-stop the surviving tree — same best-effort contract as unmount.
|
|
1385
|
+
const destroyRes = this.destroyFiber(current, []);
|
|
1296
1386
|
if (isThenable(destroyRes)) {
|
|
1297
1387
|
return destroyRes.then(() => this.materialize(nextVnode, parentFiber, parentScope));
|
|
1298
1388
|
}
|
|
@@ -1328,7 +1418,7 @@ class GraphRuntime {
|
|
|
1328
1418
|
contextChanged = (0, context_1.injectContextFields)(instance, parentScope);
|
|
1329
1419
|
}
|
|
1330
1420
|
catch (error) {
|
|
1331
|
-
const cleanupResult = this.runFiberFailedCleanup(current);
|
|
1421
|
+
const cleanupResult = this.runFiberFailedCleanup(current, error);
|
|
1332
1422
|
if (isThenable(cleanupResult)) {
|
|
1333
1423
|
return cleanupResult.then(() => {
|
|
1334
1424
|
throw error;
|
|
@@ -1346,7 +1436,7 @@ class GraphRuntime {
|
|
|
1346
1436
|
instance.onUpdate(prevProps, instance.props);
|
|
1347
1437
|
}
|
|
1348
1438
|
catch (error) {
|
|
1349
|
-
const cleanupResult = this.runFiberFailedCleanup(current);
|
|
1439
|
+
const cleanupResult = this.runFiberFailedCleanup(current, error);
|
|
1350
1440
|
if (isThenable(cleanupResult)) {
|
|
1351
1441
|
return cleanupResult.then(() => {
|
|
1352
1442
|
throw error;
|
|
@@ -1355,15 +1445,16 @@ class GraphRuntime {
|
|
|
1355
1445
|
throw error;
|
|
1356
1446
|
}
|
|
1357
1447
|
}
|
|
1358
|
-
// Commit ref transition: clear old ref if changed, bind new ref
|
|
1359
|
-
this.commitRef(current.vnode.ref, instance, nextVnode.ref, instance);
|
|
1360
1448
|
// Reconcile child nodes (sync fast-path if all children are sync).
|
|
1449
|
+
// Ref commit is deferred to applyFiberUpdate so a compose()/child-reconcile
|
|
1450
|
+
// failure cannot leave nextRef.current pointing at an instance that failStop
|
|
1451
|
+
// will destroy while fiber.vnode.ref still holds the previous ref.
|
|
1361
1452
|
let nextChildVnodes;
|
|
1362
1453
|
try {
|
|
1363
1454
|
nextChildVnodes = this.getChildVnodes(instance, nextVnode.children);
|
|
1364
1455
|
}
|
|
1365
1456
|
catch (error) {
|
|
1366
|
-
const cleanupResult = this.runFiberFailedCleanup(current);
|
|
1457
|
+
const cleanupResult = this.runFiberFailedCleanup(current, error);
|
|
1367
1458
|
if (isThenable(cleanupResult)) {
|
|
1368
1459
|
return cleanupResult.then(() => {
|
|
1369
1460
|
throw error;
|
|
@@ -1382,28 +1473,91 @@ class GraphRuntime {
|
|
|
1382
1473
|
return current;
|
|
1383
1474
|
}
|
|
1384
1475
|
/**
|
|
1385
|
-
* Fiber cleanup after update/compose error:
|
|
1386
|
-
* Does not leave the node in `ready`.
|
|
1476
|
+
* Fiber cleanup after update/compose error: destroy children first (children → parent),
|
|
1477
|
+
* then `runFailedCleanup` + bus dispose. Does not leave the node in `ready`.
|
|
1478
|
+
* Child destroy / disposer errors are attached to `primaryError.rollbackErrors` when provided
|
|
1479
|
+
* so fail-stop observability still surfaces them (children are no longer destroyed in failStop).
|
|
1387
1480
|
*
|
|
1388
1481
|
* @param {RuntimeFiber<unknown>} fiber - fiber that failed
|
|
1482
|
+
* @param {unknown} [primaryError] - originating error to attach cleanup failures onto
|
|
1389
1483
|
* @returns {void | Promise<void>}
|
|
1390
1484
|
*/
|
|
1391
|
-
runFiberFailedCleanup(fiber) {
|
|
1485
|
+
runFiberFailedCleanup(fiber, primaryError) {
|
|
1392
1486
|
const instance = fiber.instance;
|
|
1393
1487
|
if (instance === null) {
|
|
1394
1488
|
return;
|
|
1395
1489
|
}
|
|
1396
1490
|
this.clearUpdateHook(instance);
|
|
1397
1491
|
this.dirtyFibers.delete(fiber);
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1492
|
+
const cleanupErrors = [];
|
|
1493
|
+
const children = fiber.children;
|
|
1494
|
+
const attachCleanupErrors = () => {
|
|
1495
|
+
if (cleanupErrors.length > 0 &&
|
|
1496
|
+
primaryError instanceof Error) {
|
|
1497
|
+
const existing = primaryError.rollbackErrors;
|
|
1498
|
+
primaryError.rollbackErrors =
|
|
1499
|
+
existing !== undefined ? existing.concat(cleanupErrors) : cleanupErrors.slice();
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
const finishParent = () => {
|
|
1503
|
+
fiber.children = [];
|
|
1504
|
+
const cleanupResult = fiber.engine.runFailedCleanup(instance, true);
|
|
1505
|
+
if (isThenable(cleanupResult)) {
|
|
1506
|
+
return cleanupResult.then(() => {
|
|
1507
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1508
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1509
|
+
attachCleanupErrors();
|
|
1510
|
+
}, (err) => {
|
|
1511
|
+
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1512
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1513
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1514
|
+
attachCleanupErrors();
|
|
1515
|
+
throw err;
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
this.disposeEffectableRuntimeBusWiring(fiber);
|
|
1519
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1520
|
+
attachCleanupErrors();
|
|
1521
|
+
};
|
|
1522
|
+
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
1523
|
+
try {
|
|
1524
|
+
const destroyRes = this.destroyFiber(children[i], cleanupErrors);
|
|
1525
|
+
if (isThenable(destroyRes)) {
|
|
1526
|
+
return destroyRes.then(async () => {
|
|
1527
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
1528
|
+
try {
|
|
1529
|
+
const r = this.destroyFiber(children[j], cleanupErrors);
|
|
1530
|
+
if (isThenable(r)) {
|
|
1531
|
+
await r;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
catch (err) {
|
|
1535
|
+
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return finishParent();
|
|
1539
|
+
}, async (err) => {
|
|
1540
|
+
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1541
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
1542
|
+
try {
|
|
1543
|
+
const r = this.destroyFiber(children[j], cleanupErrors);
|
|
1544
|
+
if (isThenable(r)) {
|
|
1545
|
+
await r;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
catch (inner) {
|
|
1549
|
+
cleanupErrors.push(inner instanceof Error ? inner : new Error(String(inner)));
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return finishParent();
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
catch (err) {
|
|
1557
|
+
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1558
|
+
}
|
|
1404
1559
|
}
|
|
1405
|
-
|
|
1406
|
-
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1560
|
+
return finishParent();
|
|
1407
1561
|
}
|
|
1408
1562
|
/**
|
|
1409
1563
|
* Applies the reconcile result to the current fiber in-place.
|
|
@@ -1418,6 +1572,11 @@ class GraphRuntime {
|
|
|
1418
1572
|
* @returns {void}
|
|
1419
1573
|
*/
|
|
1420
1574
|
applyFiberUpdate(current, nextVnode, parentFiber, parentScope, nextChildren) {
|
|
1575
|
+
const instance = current.instance;
|
|
1576
|
+
if (instance !== null) {
|
|
1577
|
+
// Commit ref only after compose + child reconcile succeeded.
|
|
1578
|
+
this.commitRef(current.vnode.ref, instance, nextVnode.ref, instance);
|
|
1579
|
+
}
|
|
1421
1580
|
current.vnode = nextVnode;
|
|
1422
1581
|
current.parentFiber = parentFiber;
|
|
1423
1582
|
current.children = nextChildren;
|
|
@@ -1584,9 +1743,11 @@ class GraphRuntime {
|
|
|
1584
1743
|
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
1585
1744
|
}
|
|
1586
1745
|
}
|
|
1587
|
-
// Destroy remaining unpaired current children (keyed)
|
|
1746
|
+
// Destroy remaining unpaired current children (keyed).
|
|
1747
|
+
// Best-effort: collect finalize errors so one throwing ref clear cannot
|
|
1748
|
+
// skip remaining orphans and fail-stop the whole runtime.
|
|
1588
1749
|
for (const [, orphan] of keyedCurrentMap) {
|
|
1589
|
-
const d = this.destroyFiber(orphan);
|
|
1750
|
+
const d = this.destroyFiber(orphan, []);
|
|
1590
1751
|
if (isThenable(d)) {
|
|
1591
1752
|
await d;
|
|
1592
1753
|
}
|
|
@@ -1616,11 +1777,11 @@ class GraphRuntime {
|
|
|
1616
1777
|
}
|
|
1617
1778
|
}
|
|
1618
1779
|
}
|
|
1619
|
-
// Destroy remaining unpaired unkeyed children
|
|
1780
|
+
// Destroy remaining unpaired unkeyed children (best-effort finalize errors).
|
|
1620
1781
|
for (let i = unkeyedIdx; i < unkeyedCurrent.length; i += 1) {
|
|
1621
1782
|
const orphan = unkeyedCurrent[i];
|
|
1622
1783
|
if (orphan !== undefined) {
|
|
1623
|
-
const d = this.destroyFiber(orphan);
|
|
1784
|
+
const d = this.destroyFiber(orphan, []);
|
|
1624
1785
|
if (isThenable(d)) {
|
|
1625
1786
|
await d;
|
|
1626
1787
|
}
|