effectable 1.2.0-canary.4 → 1.2.0-canary.6
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/component/GraphRuntime.d.ts +102 -8
- package/build/component/GraphRuntime.d.ts.map +1 -1
- package/build/component/GraphRuntime.js +362 -107
- package/build/component/GraphRuntime.js.map +1 -1
- package/build/connect/connect.d.ts +12 -0
- package/build/connect/connect.d.ts.map +1 -1
- package/build/connect/connect.js +307 -22
- package/build/connect/connect.js.map +1 -1
- package/build/connect/index.d.ts +1 -1
- package/build/connect/index.d.ts.map +1 -1
- package/build/connect/index.js +2 -1
- package/build/connect/index.js.map +1 -1
- package/build/runtime/HandleRegistry.d.ts +14 -1
- package/build/runtime/HandleRegistry.d.ts.map +1 -1
- package/build/runtime/HandleRegistry.js +21 -2
- package/build/runtime/HandleRegistry.js.map +1 -1
- package/build/store/createStore.d.ts.map +1 -1
- package/build/store/createStore.js +39 -5
- package/build/store/createStore.js.map +1 -1
- package/build/store/middleware.d.ts.map +1 -1
- package/build/store/middleware.js +33 -8
- package/build/store/middleware.js.map +1 -1
- package/build/store/selector.d.ts.map +1 -1
- package/build/store/selector.js +9 -1
- package/build/store/selector.js.map +1 -1
- package/build/store/semanticStateTree.d.ts.map +1 -1
- package/build/store/semanticStateTree.js +5 -1
- package/build/store/semanticStateTree.js.map +1 -1
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ exports.makeFiberEffectTag = makeFiberEffectTag;
|
|
|
37
37
|
const types_1 = require("./types");
|
|
38
38
|
const lifecycle_1 = require("./lifecycle");
|
|
39
39
|
const context_1 = require("./context");
|
|
40
|
+
const refs_1 = require("./refs");
|
|
40
41
|
const BusDecorators_1 = require("../runtime/BusDecorators");
|
|
41
42
|
const graphRuntime_constants_1 = require("./graphRuntime.constants");
|
|
42
43
|
/**
|
|
@@ -170,6 +171,12 @@ class GraphRuntime {
|
|
|
170
171
|
* unmount() must await this before concluding teardown is finished.
|
|
171
172
|
*/
|
|
172
173
|
pendingTeardown = null;
|
|
174
|
+
/**
|
|
175
|
+
* Imperative ref values bound for owners that expose `@UseImperativeHandle` methods.
|
|
176
|
+
* `commitRef` assigns a limited handle (not the Component instance) into `ref.current`;
|
|
177
|
+
* identity-safe clear must still recognize that handle as owned by the instance.
|
|
178
|
+
*/
|
|
179
|
+
imperativeRefByOwner = new WeakMap();
|
|
173
180
|
/**
|
|
174
181
|
* Instances are created only via {@link GraphRuntime.mount}; direct `new GraphRuntime()` is unavailable externally.
|
|
175
182
|
*/
|
|
@@ -257,18 +264,47 @@ class GraphRuntime {
|
|
|
257
264
|
delete fiber.effectableRuntimeBusDisposer;
|
|
258
265
|
}
|
|
259
266
|
/**
|
|
260
|
-
*
|
|
267
|
+
* Builds the value assigned to `ref.current` for a mounted instance.
|
|
268
|
+
*
|
|
269
|
+
* When the constructor declares `@UseImperativeHandle` methods, only those methods
|
|
270
|
+
* are exposed (bound to the instance) — matching the refs.ts / CONCEPT contract.
|
|
271
|
+
* Without an allowlist, the full instance is assigned (legacy escape hatch).
|
|
272
|
+
*
|
|
273
|
+
* @param {Component<unknown, unknown>} instance - mounted component instance
|
|
274
|
+
* @returns {unknown} instance or limited imperative handle object
|
|
275
|
+
*/
|
|
276
|
+
resolveRefCurrentValue(instance) {
|
|
277
|
+
const methods = (0, refs_1.getImperativeHandleMethods)(instance.constructor);
|
|
278
|
+
if (methods.length === 0) {
|
|
279
|
+
this.imperativeRefByOwner.delete(instance);
|
|
280
|
+
return instance;
|
|
281
|
+
}
|
|
282
|
+
const handle = Object.create(null);
|
|
283
|
+
for (const { methodKey } of methods) {
|
|
284
|
+
const fn = instance[methodKey];
|
|
285
|
+
if (typeof fn !== 'function') {
|
|
286
|
+
throw new Error(`[Effectable.GraphRuntime] @UseImperativeHandle method is not a function: ${String(methodKey)}`);
|
|
287
|
+
}
|
|
288
|
+
handle[methodKey] = fn.bind(instance);
|
|
289
|
+
}
|
|
290
|
+
this.imperativeRefByOwner.set(instance, handle);
|
|
291
|
+
return handle;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Identity-safe ref clearing: clears ref.current only if it still points to the expected owner
|
|
295
|
+
* (full instance) or the limited imperative handle previously bound for that owner.
|
|
261
296
|
* Prevents an old rollback from clearing a ref that a newer materialization already reused.
|
|
262
|
-
* No cast required (Component | null → unknown | null is assignable).
|
|
263
297
|
*
|
|
264
298
|
* @param {RefObject<unknown>} ref - ref object
|
|
265
299
|
* @param {Component<unknown, unknown>} expectedOwner - expected current owner
|
|
266
300
|
* @returns {void}
|
|
267
301
|
*/
|
|
268
302
|
clearRefSafe(ref, expectedOwner) {
|
|
269
|
-
|
|
303
|
+
const boundHandle = this.imperativeRefByOwner.get(expectedOwner);
|
|
304
|
+
if (ref.current === expectedOwner || (boundHandle !== undefined && ref.current === boundHandle)) {
|
|
270
305
|
ref.current = null;
|
|
271
306
|
}
|
|
307
|
+
this.imperativeRefByOwner.delete(expectedOwner);
|
|
272
308
|
}
|
|
273
309
|
/**
|
|
274
310
|
* Centralized ref ownership transition.
|
|
@@ -276,12 +312,10 @@ class GraphRuntime {
|
|
|
276
312
|
*
|
|
277
313
|
* Rules:
|
|
278
314
|
* - Clear previousRef only if it still points to expectedPreviousOwner (identity-safe).
|
|
279
|
-
* - Bind nextRef to instance
|
|
315
|
+
* - Bind nextRef to the instance, or to a limited `@UseImperativeHandle` surface when declared.
|
|
280
316
|
* - previousRef and nextRef can be the same object (ref reuse) or different (ref swap).
|
|
281
317
|
* - Do not let an old disposer clear a newer owner.
|
|
282
318
|
*
|
|
283
|
-
* No casts: Component | null → unknown | null is assignable (widening).
|
|
284
|
-
*
|
|
285
319
|
* @param {RefObject<unknown> | undefined} previousRef - ref to clear (can be undefined if no previous ref)
|
|
286
320
|
* @param {Component<unknown, unknown> | null} expectedPreviousOwner - expected owner of previousRef (null if unknown)
|
|
287
321
|
* @param {RefObject<unknown> | undefined} nextRef - ref to bind to instance (can be undefined if removing ref)
|
|
@@ -293,9 +327,27 @@ class GraphRuntime {
|
|
|
293
327
|
if (previousRef !== undefined && previousRef !== nextRef && expectedPreviousOwner !== null) {
|
|
294
328
|
this.clearRefSafe(previousRef, expectedPreviousOwner);
|
|
295
329
|
}
|
|
296
|
-
// Bind next ref
|
|
330
|
+
// Bind next ref: limited handle when @UseImperativeHandle is present, else full instance.
|
|
331
|
+
// Custom setters may assign `current` then throw. Without a catch, UPDATE ref-swap
|
|
332
|
+
// leaves the new ref holding the instance/handle while `fiber.vnode.ref` still points at
|
|
333
|
+
// the previous ref object — fail-stop finalize clears only the old ref (zombie nextRef).
|
|
334
|
+
// Materialize assign-then-throw is covered by journal.refBound (#96/#98); this path covers
|
|
335
|
+
// the UPDATE swap hole and is a safe no-op when the setter never assigned.
|
|
297
336
|
if (nextRef !== undefined) {
|
|
298
|
-
|
|
337
|
+
try {
|
|
338
|
+
nextRef.current = instance === null ? null : this.resolveRefCurrentValue(instance);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (instance !== null) {
|
|
342
|
+
try {
|
|
343
|
+
this.clearRefSafe(nextRef, instance);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Best-effort: do not mask the original setter error.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
299
351
|
}
|
|
300
352
|
}
|
|
301
353
|
/**
|
|
@@ -345,7 +397,7 @@ class GraphRuntime {
|
|
|
345
397
|
* Transactional rollback for failed fiber materialization.
|
|
346
398
|
* Releases acquired resources in reverse acquisition order:
|
|
347
399
|
* 1. disable scheduler hook
|
|
348
|
-
* 2. destroy mounted children in
|
|
400
|
+
* 2. destroy mounted children in compose order (same as {@link destroyFiber})
|
|
349
401
|
* 3. run failed-startup cleanup (parent onUnmount when startup ran)
|
|
350
402
|
* 4. dispose runtime bus registrations
|
|
351
403
|
* 5. clear bound ref (identity-safe)
|
|
@@ -382,16 +434,18 @@ class GraphRuntime {
|
|
|
382
434
|
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
383
435
|
}
|
|
384
436
|
}
|
|
385
|
-
// 2. Destroy mounted children in
|
|
386
|
-
// Documented teardown contract is children → parent
|
|
387
|
-
//
|
|
388
|
-
// onUnmount
|
|
437
|
+
// 2. Destroy mounted children in compose order BEFORE parent onUnmount / bus / ref.
|
|
438
|
+
// Documented teardown contract is children → parent with siblings in compose
|
|
439
|
+
// order (matches destroyFiber / README shutdown). Reverse sibling order made
|
|
440
|
+
// failure-path onUnmount observe a different live-sibling set than clean unmount.
|
|
441
|
+
// Children must also run before bus dispose + ref clear so child onUnmount still
|
|
442
|
+
// sees a live parent ref / parent @On* subscriptions.
|
|
389
443
|
// Pass cleanupErrors so nested destroy is best-effort: a throwing
|
|
390
444
|
// ref-clear/disposer on one grandchild must not skip remaining siblings.
|
|
391
445
|
// Those nodes were never attached to currentRoot, so failStop cannot reclaim them.
|
|
392
446
|
const destroyChildrenThenParentCleanup = () => {
|
|
393
447
|
const children = journal.mountedChildren;
|
|
394
|
-
for (let i =
|
|
448
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
395
449
|
try {
|
|
396
450
|
const destroyRes = this.destroyFiber(children[i], cleanupErrors);
|
|
397
451
|
if (isThenable(destroyRes)) {
|
|
@@ -410,7 +464,7 @@ class GraphRuntime {
|
|
|
410
464
|
* Async continuation of rollback child destruction after one child's destroy returned a Promise.
|
|
411
465
|
*
|
|
412
466
|
* @param {RuntimeFiber<unknown>[]} children - mounted children
|
|
413
|
-
* @param {number}
|
|
467
|
+
* @param {number} pendingIdx - index of the child whose destroy is pending
|
|
414
468
|
* @param {Promise<void>} pending - Promise from destroying the previous child
|
|
415
469
|
* @param {Error} primaryError - original materialization error
|
|
416
470
|
* @param {Error[]} cleanupErrors - accumulated cleanup errors
|
|
@@ -418,14 +472,15 @@ class GraphRuntime {
|
|
|
418
472
|
* @param {Component<unknown, unknown> | null} instance - parent instance (for post-child cleanup)
|
|
419
473
|
* @returns {Promise<void>}
|
|
420
474
|
*/
|
|
421
|
-
async continueRollbackDestroyAsync(children,
|
|
475
|
+
async continueRollbackDestroyAsync(children, pendingIdx, pending, primaryError, cleanupErrors, fiber, instance) {
|
|
422
476
|
try {
|
|
423
477
|
await pending;
|
|
424
478
|
}
|
|
425
479
|
catch (err) {
|
|
426
480
|
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
427
481
|
}
|
|
428
|
-
|
|
482
|
+
// Remaining siblings in compose order (pendingIdx+1 … n-1), matching destroyFiber.
|
|
483
|
+
for (let i = pendingIdx + 1; i < children.length; i += 1) {
|
|
429
484
|
try {
|
|
430
485
|
const destroyRes = this.destroyFiber(children[i], cleanupErrors);
|
|
431
486
|
if (isThenable(destroyRes)) {
|
|
@@ -1138,6 +1193,221 @@ class GraphRuntime {
|
|
|
1138
1193
|
// ---------------------------------------------------------------------------
|
|
1139
1194
|
// Materialize
|
|
1140
1195
|
// ---------------------------------------------------------------------------
|
|
1196
|
+
/**
|
|
1197
|
+
* Runs deferred {@link LifecycleEngine.runStartup} for a fiber subtree in
|
|
1198
|
+
* children→parent order (siblings in compose order).
|
|
1199
|
+
*
|
|
1200
|
+
* Call only after the subtree (and any later siblings that must observe
|
|
1201
|
+
* publishes) have completed bus wiring. Idempotent for fibers that already
|
|
1202
|
+
* started.
|
|
1203
|
+
*
|
|
1204
|
+
* @param {RuntimeFiber<unknown>} fiber - subtree root
|
|
1205
|
+
* @returns {void | Promise<void>}
|
|
1206
|
+
*/
|
|
1207
|
+
flushDeferredLifecycleTree(fiber) {
|
|
1208
|
+
const children = fiber.children;
|
|
1209
|
+
for (let i = 0; i < children.length; i++) {
|
|
1210
|
+
const child = children[i];
|
|
1211
|
+
const childFlush = this.flushDeferredLifecycleTree(child);
|
|
1212
|
+
if (isThenable(childFlush)) {
|
|
1213
|
+
return this.continueFlushDeferredLifecycleAsync(fiber, children, i, childFlush);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return this.runDeferredLifecycleIfNeeded(fiber);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Async continuation of {@link flushDeferredLifecycleTree} after a child flush returns a Promise.
|
|
1220
|
+
*
|
|
1221
|
+
* @param {RuntimeFiber<unknown>} fiber - subtree root
|
|
1222
|
+
* @param {RuntimeFiber<unknown>[]} children - child fibers
|
|
1223
|
+
* @param {number} pendingIdx - index of the pending child
|
|
1224
|
+
* @param {PromiseLike<void>} pending - pending child flush
|
|
1225
|
+
* @returns {Promise<void>}
|
|
1226
|
+
*/
|
|
1227
|
+
async continueFlushDeferredLifecycleAsync(fiber, children, pendingIdx, pending) {
|
|
1228
|
+
await pending;
|
|
1229
|
+
for (let i = pendingIdx + 1; i < children.length; i++) {
|
|
1230
|
+
const childFlush = this.flushDeferredLifecycleTree(children[i]);
|
|
1231
|
+
if (isThenable(childFlush)) {
|
|
1232
|
+
await childFlush;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const selfRes = this.runDeferredLifecycleIfNeeded(fiber);
|
|
1236
|
+
if (isThenable(selfRes)) {
|
|
1237
|
+
await selfRes;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Runs startup + update-hook injection when {@link FiberConstructionJournal.lifecycleDeferred}
|
|
1242
|
+
* is set; no-op otherwise.
|
|
1243
|
+
*
|
|
1244
|
+
* @param {RuntimeFiber<unknown>} fiber - fiber to start
|
|
1245
|
+
* @returns {void | Promise<void>}
|
|
1246
|
+
*/
|
|
1247
|
+
runDeferredLifecycleIfNeeded(fiber) {
|
|
1248
|
+
const journal = fiber.constructionJournal;
|
|
1249
|
+
if (journal === undefined || journal.lifecycleDeferred !== true) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const instance = fiber.instance;
|
|
1253
|
+
if (instance === null) {
|
|
1254
|
+
journal.lifecycleDeferred = false;
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const startupRes = fiber.engine.runStartup(instance, { deferFailedCleanup: true });
|
|
1258
|
+
if (isThenable(startupRes)) {
|
|
1259
|
+
return this.finalizeDeferredLifecycleAsync(fiber, startupRes);
|
|
1260
|
+
}
|
|
1261
|
+
if (!startupRes.ok) {
|
|
1262
|
+
const error = startupRes.error instanceof Error
|
|
1263
|
+
? startupRes.error
|
|
1264
|
+
: new Error(String(startupRes.error));
|
|
1265
|
+
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1266
|
+
if (isThenable(rollbackRes)) {
|
|
1267
|
+
return rollbackRes.then(() => {
|
|
1268
|
+
throw error;
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
journal.lifecycleDeferred = false;
|
|
1274
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1275
|
+
fiber.effectTag = null;
|
|
1276
|
+
journal.schedulerHookAttached = true;
|
|
1277
|
+
this.injectUpdateHook(instance, fiber);
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Async path for {@link runDeferredLifecycleIfNeeded} when startup returns a Promise.
|
|
1281
|
+
*
|
|
1282
|
+
* @param {RuntimeFiber<unknown>} fiber - fiber being started
|
|
1283
|
+
* @param {PromiseLike<import('./lifecycle').LifecycleTransitionResult>} pendingStartup - pending startup
|
|
1284
|
+
* @returns {Promise<void>}
|
|
1285
|
+
*/
|
|
1286
|
+
async finalizeDeferredLifecycleAsync(fiber, pendingStartup) {
|
|
1287
|
+
const result = await pendingStartup;
|
|
1288
|
+
const journal = fiber.constructionJournal;
|
|
1289
|
+
const instance = fiber.instance;
|
|
1290
|
+
if (!result.ok) {
|
|
1291
|
+
const error = result.error instanceof Error ? result.error : new Error(String(result.error));
|
|
1292
|
+
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1293
|
+
if (isThenable(rollbackRes)) {
|
|
1294
|
+
await rollbackRes;
|
|
1295
|
+
}
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
journal.lifecycleDeferred = false;
|
|
1299
|
+
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1300
|
+
fiber.effectTag = null;
|
|
1301
|
+
if (instance !== null) {
|
|
1302
|
+
journal.schedulerHookAttached = true;
|
|
1303
|
+
this.injectUpdateHook(instance, fiber);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* After bus wiring: either defer lifecycle (sibling/parent batching) or flush
|
|
1308
|
+
* deferred children and run this fiber's startup.
|
|
1309
|
+
*
|
|
1310
|
+
* @template P node props type
|
|
1311
|
+
* @param {RuntimeFiber<P>} fiber - fiber that just finished wiring
|
|
1312
|
+
* @param {Component<unknown, P>} instance - component instance
|
|
1313
|
+
* @param {LifecycleEngine} engine - lifecycle engine
|
|
1314
|
+
* @param {boolean} deferLifecycle - when true, skip startup until a parent flush
|
|
1315
|
+
* @returns {RuntimeFiber<P> | Promise<RuntimeFiber<P>>}
|
|
1316
|
+
*/
|
|
1317
|
+
completeMaterializeAfterWiring(fiber, instance, engine, deferLifecycle) {
|
|
1318
|
+
const journal = fiber.constructionJournal;
|
|
1319
|
+
if (deferLifecycle) {
|
|
1320
|
+
journal.lifecycleDeferred = true;
|
|
1321
|
+
return fiber;
|
|
1322
|
+
}
|
|
1323
|
+
// Children were materialized with deferLifecycle=true — start them (and their
|
|
1324
|
+
// subtrees) before this node's onMount so sibling/parent @On* handlers exist.
|
|
1325
|
+
// If a deferred sibling fails during flush after an earlier sibling started,
|
|
1326
|
+
// roll back this parent (destroys all mounted children) — same contract as
|
|
1327
|
+
// the pre-defer depth-first materialize catch around each child.
|
|
1328
|
+
try {
|
|
1329
|
+
const children = fiber.children;
|
|
1330
|
+
for (let i = 0; i < children.length; i++) {
|
|
1331
|
+
const childFlush = this.flushDeferredLifecycleTree(children[i]);
|
|
1332
|
+
if (isThenable(childFlush)) {
|
|
1333
|
+
return this.continueCompleteMaterializeAfterWiringAsync(fiber, instance, engine, children, i, childFlush);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return this.runMaterializeStartup(fiber, instance, engine);
|
|
1337
|
+
}
|
|
1338
|
+
catch (err) {
|
|
1339
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1340
|
+
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1341
|
+
if (isThenable(rollbackRes)) {
|
|
1342
|
+
return rollbackRes.then(() => {
|
|
1343
|
+
throw error;
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
throw error;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Async continuation after a deferred child flush during materialize completion.
|
|
1351
|
+
*
|
|
1352
|
+
* @template P node props type
|
|
1353
|
+
* @param {RuntimeFiber<P>} fiber - parent fiber
|
|
1354
|
+
* @param {Component<unknown, P>} instance - parent instance
|
|
1355
|
+
* @param {LifecycleEngine} engine - parent engine
|
|
1356
|
+
* @param {RuntimeFiber<unknown>[]} children - child fibers
|
|
1357
|
+
* @param {number} pendingIdx - pending child index
|
|
1358
|
+
* @param {PromiseLike<void>} pending - pending flush
|
|
1359
|
+
* @returns {Promise<RuntimeFiber<P>>}
|
|
1360
|
+
*/
|
|
1361
|
+
async continueCompleteMaterializeAfterWiringAsync(fiber, instance, engine, children, pendingIdx, pending) {
|
|
1362
|
+
try {
|
|
1363
|
+
await pending;
|
|
1364
|
+
for (let i = pendingIdx + 1; i < children.length; i++) {
|
|
1365
|
+
const childFlush = this.flushDeferredLifecycleTree(children[i]);
|
|
1366
|
+
if (isThenable(childFlush)) {
|
|
1367
|
+
await childFlush;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return this.runMaterializeStartup(fiber, instance, engine);
|
|
1371
|
+
}
|
|
1372
|
+
catch (err) {
|
|
1373
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1374
|
+
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1375
|
+
if (isThenable(rollbackRes)) {
|
|
1376
|
+
await rollbackRes;
|
|
1377
|
+
}
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Runs startup for a fiber that is completing materialize (not deferred).
|
|
1383
|
+
*
|
|
1384
|
+
* @template P node props type
|
|
1385
|
+
* @param {RuntimeFiber<P>} fiber - fiber
|
|
1386
|
+
* @param {Component<unknown, P>} instance - instance
|
|
1387
|
+
* @param {LifecycleEngine} engine - engine
|
|
1388
|
+
* @returns {RuntimeFiber<P> | Promise<RuntimeFiber<P>>}
|
|
1389
|
+
*/
|
|
1390
|
+
runMaterializeStartup(fiber, instance, engine) {
|
|
1391
|
+
const startupRes = engine.runStartup(instance, { deferFailedCleanup: true });
|
|
1392
|
+
if (isThenable(startupRes)) {
|
|
1393
|
+
return this.finalizeMaterializeAsync(fiber, engine, startupRes);
|
|
1394
|
+
}
|
|
1395
|
+
if (!startupRes.ok) {
|
|
1396
|
+
const error = startupRes.error instanceof Error ? startupRes.error : new Error(String(startupRes.error));
|
|
1397
|
+
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1398
|
+
if (isThenable(rollbackRes)) {
|
|
1399
|
+
return rollbackRes.then(() => {
|
|
1400
|
+
throw error;
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
throw error;
|
|
1404
|
+
}
|
|
1405
|
+
fiber.lifecycleStatus = engine.getStatus();
|
|
1406
|
+
fiber.effectTag = null;
|
|
1407
|
+
fiber.constructionJournal.schedulerHookAttached = true;
|
|
1408
|
+
this.injectUpdateHook(instance, fiber);
|
|
1409
|
+
return fiber;
|
|
1410
|
+
}
|
|
1141
1411
|
/**
|
|
1142
1412
|
* Creates a RuntimeFiber for a virtual node: instantiates the component,
|
|
1143
1413
|
* injects contexts, builds the child scope (for ContextProvider),
|
|
@@ -1150,9 +1420,11 @@ class GraphRuntime {
|
|
|
1150
1420
|
* @param {VirtualServiceNode<P>} vnode - virtual node
|
|
1151
1421
|
* @param {RuntimeFiber | null} parentFiber - parent fiber
|
|
1152
1422
|
* @param {ContextScope} parentScope - parent scope
|
|
1423
|
+
* @param {boolean} [deferLifecycle=false] - when true, wire buses but defer onMount
|
|
1424
|
+
* until {@link flushDeferredLifecycleTree} (used for sibling batching)
|
|
1153
1425
|
* @returns {RuntimeFiber<P> | Promise<RuntimeFiber<P>>}
|
|
1154
1426
|
*/
|
|
1155
|
-
materialize(vnode, parentFiber, parentScope) {
|
|
1427
|
+
materialize(vnode, parentFiber, parentScope, deferLifecycle = false) {
|
|
1156
1428
|
const engine = new lifecycle_1.LifecycleEngine();
|
|
1157
1429
|
// Constructor is stored as ComponentConstructor<unknown> for covariance,
|
|
1158
1430
|
// but invoked with concrete props P. The cast is safe: P extends unknown.
|
|
@@ -1209,11 +1481,32 @@ class GraphRuntime {
|
|
|
1209
1481
|
}
|
|
1210
1482
|
throw error;
|
|
1211
1483
|
}
|
|
1484
|
+
// Wire parent buses BEFORE children materialize/onMount. Children run onMount while
|
|
1485
|
+
// descending; if the parent is not subscribed yet, child publishes to @OnEvent /
|
|
1486
|
+
// @OnCommand on the parent are silently dropped (mirror of #80 teardown order).
|
|
1487
|
+
try {
|
|
1488
|
+
this.attachEffectableRuntimeBusWiring(instance, fiber);
|
|
1489
|
+
if (fiber.effectableRuntimeBusDisposer !== undefined) {
|
|
1490
|
+
fiber.constructionJournal.busWiringAttached = true;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
catch (err) {
|
|
1494
|
+
const rollbackRes = this.rollbackFailedMaterialization(fiber, err instanceof Error ? err : new Error(String(err)));
|
|
1495
|
+
if (isThenable(rollbackRes)) {
|
|
1496
|
+
return rollbackRes.then(() => {
|
|
1497
|
+
throw err;
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
throw err;
|
|
1501
|
+
}
|
|
1212
1502
|
for (let i = 0; i < childVnodes.length; i++) {
|
|
1213
1503
|
const childVnode = childVnodes[i];
|
|
1214
1504
|
let childRes;
|
|
1215
1505
|
try {
|
|
1216
|
-
|
|
1506
|
+
// Defer child lifecycle so all siblings (and this parent) finish bus wiring
|
|
1507
|
+
// before any onMount runs — otherwise earlier-sibling publishes miss later
|
|
1508
|
+
// siblings' @On* handlers (and parent handlers miss child onMount publishes).
|
|
1509
|
+
childRes = this.materialize(childVnode, fiber, childScope, true);
|
|
1217
1510
|
}
|
|
1218
1511
|
catch (err) {
|
|
1219
1512
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1227,17 +1520,21 @@ class GraphRuntime {
|
|
|
1227
1520
|
}
|
|
1228
1521
|
if (isThenable(childRes)) {
|
|
1229
1522
|
// Hit an async child — continue the materialization tail in the async continuation.
|
|
1230
|
-
return this.continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childScope, childRes, i);
|
|
1523
|
+
return this.continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childScope, childRes, i, deferLifecycle);
|
|
1231
1524
|
}
|
|
1232
1525
|
fiber.constructionJournal.mountedChildren.push(childRes);
|
|
1233
1526
|
}
|
|
1234
1527
|
fiber.children = fiber.constructionJournal.mountedChildren;
|
|
1235
|
-
// Bind ref to the instance (centralized via commitRef)
|
|
1528
|
+
// Bind ref to the instance (centralized via commitRef).
|
|
1529
|
+
// Mark refBound/refOwner BEFORE commitRef: a setter may assign `current`
|
|
1530
|
+
// then throw. If the flags were set only after a successful return,
|
|
1531
|
+
// rollback would skip the identity-safe clear and leave a zombie ref
|
|
1532
|
+
// (root was never published, so failStop cannot reclaim it).
|
|
1236
1533
|
if (vnode.ref !== undefined) {
|
|
1237
1534
|
try {
|
|
1238
|
-
this.commitRef(undefined, null, vnode.ref, instance);
|
|
1239
1535
|
fiber.constructionJournal.refBound = true;
|
|
1240
1536
|
fiber.constructionJournal.refOwner = instance;
|
|
1537
|
+
this.commitRef(undefined, null, vnode.ref, instance);
|
|
1241
1538
|
}
|
|
1242
1539
|
catch (err) {
|
|
1243
1540
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1250,47 +1547,12 @@ class GraphRuntime {
|
|
|
1250
1547
|
throw error;
|
|
1251
1548
|
}
|
|
1252
1549
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
if (fiber.effectableRuntimeBusDisposer !== undefined) {
|
|
1256
|
-
fiber.constructionJournal.busWiringAttached = true;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
catch (err) {
|
|
1260
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, err instanceof Error ? err : new Error(String(err)));
|
|
1261
|
-
if (isThenable(rollbackRes)) {
|
|
1262
|
-
return rollbackRes.then(() => {
|
|
1263
|
-
throw err;
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
throw err;
|
|
1267
|
-
}
|
|
1268
|
-
// Run lifecycle after all children are materialized.
|
|
1269
|
-
// Pre-mount hook was injected before children (covers ancestor setState
|
|
1270
|
-
// from descendant onMount and setState during this node's onMount).
|
|
1271
|
-
const startupRes = engine.runStartup(instance, { deferFailedCleanup: true });
|
|
1272
|
-
if (isThenable(startupRes)) {
|
|
1273
|
-
return this.finalizeMaterializeAsync(fiber, engine, startupRes);
|
|
1274
|
-
}
|
|
1275
|
-
if (!startupRes.ok) {
|
|
1276
|
-
const error = startupRes.error instanceof Error ? startupRes.error : new Error(String(startupRes.error));
|
|
1277
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1278
|
-
if (isThenable(rollbackRes)) {
|
|
1279
|
-
return rollbackRes.then(() => {
|
|
1280
|
-
throw error;
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
throw error;
|
|
1284
|
-
}
|
|
1285
|
-
fiber.lifecycleStatus = engine.getStatus();
|
|
1286
|
-
fiber.effectTag = null;
|
|
1287
|
-
fiber.constructionJournal.schedulerHookAttached = true;
|
|
1288
|
-
this.injectUpdateHook(instance, fiber);
|
|
1289
|
-
return fiber;
|
|
1550
|
+
// Parent buses already wired before children (see above). Do not wire again here.
|
|
1551
|
+
return this.completeMaterializeAfterWiring(fiber, instance, engine, deferLifecycle);
|
|
1290
1552
|
}
|
|
1291
1553
|
/**
|
|
1292
1554
|
* Async continuation of {@link materialize} after a child returned a Promise.
|
|
1293
|
-
* Finishes remaining child fibers with `await`, then
|
|
1555
|
+
* Finishes remaining child fibers with `await`, then completes wiring / lifecycle.
|
|
1294
1556
|
*
|
|
1295
1557
|
* @param {RuntimeFiber<P>} fiber - current fiber
|
|
1296
1558
|
* @param {Component<unknown, P>} instance - component instance
|
|
@@ -1300,9 +1562,10 @@ class GraphRuntime {
|
|
|
1300
1562
|
* @param {ContextScope} childScope - scope for child nodes
|
|
1301
1563
|
* @param {Promise<RuntimeFiber<unknown>>} pending - Promise for the current child
|
|
1302
1564
|
* @param {number} pendingIdx - index of the current child
|
|
1565
|
+
* @param {boolean} deferLifecycle - parent defer flag from {@link materialize}
|
|
1303
1566
|
* @returns {Promise<RuntimeFiber<P>>}
|
|
1304
1567
|
*/
|
|
1305
|
-
async continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childScope, pending, pendingIdx) {
|
|
1568
|
+
async continueMaterializeAsync(fiber, instance, engine, vnode, childVnodes, childScope, pending, pendingIdx, deferLifecycle) {
|
|
1306
1569
|
const journal = fiber.constructionJournal;
|
|
1307
1570
|
try {
|
|
1308
1571
|
journal.mountedChildren.push(await pending);
|
|
@@ -1318,7 +1581,7 @@ class GraphRuntime {
|
|
|
1318
1581
|
for (let i = pendingIdx + 1; i < childVnodes.length; i++) {
|
|
1319
1582
|
const childVnode = childVnodes[i];
|
|
1320
1583
|
try {
|
|
1321
|
-
const childRes = this.materialize(childVnode, fiber, childScope);
|
|
1584
|
+
const childRes = this.materialize(childVnode, fiber, childScope, true);
|
|
1322
1585
|
const resolvedChild = isThenable(childRes) ? await childRes : childRes;
|
|
1323
1586
|
journal.mountedChildren.push(resolvedChild);
|
|
1324
1587
|
}
|
|
@@ -1332,11 +1595,12 @@ class GraphRuntime {
|
|
|
1332
1595
|
}
|
|
1333
1596
|
}
|
|
1334
1597
|
fiber.children = journal.mountedChildren;
|
|
1598
|
+
// Mark refBound/refOwner BEFORE commitRef (see sync materialize path).
|
|
1335
1599
|
if (vnode.ref !== undefined) {
|
|
1336
1600
|
try {
|
|
1337
|
-
this.commitRef(undefined, null, vnode.ref, instance);
|
|
1338
1601
|
journal.refBound = true;
|
|
1339
1602
|
journal.refOwner = instance;
|
|
1603
|
+
this.commitRef(undefined, null, vnode.ref, instance);
|
|
1340
1604
|
}
|
|
1341
1605
|
catch (err) {
|
|
1342
1606
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1347,35 +1611,9 @@ class GraphRuntime {
|
|
|
1347
1611
|
throw error;
|
|
1348
1612
|
}
|
|
1349
1613
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
journal.busWiringAttached = true;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
catch (err) {
|
|
1357
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, err instanceof Error ? err : new Error(String(err)));
|
|
1358
|
-
if (isThenable(rollbackRes)) {
|
|
1359
|
-
await rollbackRes;
|
|
1360
|
-
}
|
|
1361
|
-
throw err;
|
|
1362
|
-
}
|
|
1363
|
-
// Pre-mount hook already injected at the start of materialize (before children).
|
|
1364
|
-
const startupRes = engine.runStartup(instance, { deferFailedCleanup: true });
|
|
1365
|
-
const resolved = isThenable(startupRes) ? await startupRes : startupRes;
|
|
1366
|
-
if (!resolved.ok) {
|
|
1367
|
-
const error = resolved.error instanceof Error ? resolved.error : new Error(String(resolved.error));
|
|
1368
|
-
const rollbackRes = this.rollbackFailedMaterialization(fiber, error);
|
|
1369
|
-
if (isThenable(rollbackRes)) {
|
|
1370
|
-
await rollbackRes;
|
|
1371
|
-
}
|
|
1372
|
-
throw error;
|
|
1373
|
-
}
|
|
1374
|
-
fiber.lifecycleStatus = engine.getStatus();
|
|
1375
|
-
fiber.effectTag = null;
|
|
1376
|
-
journal.schedulerHookAttached = true;
|
|
1377
|
-
this.injectUpdateHook(instance, fiber);
|
|
1378
|
-
return fiber;
|
|
1614
|
+
// Parent buses were wired before the child loop in materialize(); async continuation
|
|
1615
|
+
// must not register handlers a second time.
|
|
1616
|
+
return this.completeMaterializeAfterWiring(fiber, instance, engine, deferLifecycle);
|
|
1379
1617
|
}
|
|
1380
1618
|
/**
|
|
1381
1619
|
* Async finalization of {@link materialize} when child fibers were gathered synchronously but {@link LifecycleEngine.runStartup} returned a Promise.
|
|
@@ -1417,9 +1655,12 @@ class GraphRuntime {
|
|
|
1417
1655
|
* @param {VirtualServiceNode<P>} nextVnode - new virtual node
|
|
1418
1656
|
* @param {RuntimeFiber | null} parentFiber - parent fiber
|
|
1419
1657
|
* @param {ContextScope} parentScope - parent scope
|
|
1658
|
+
* @param {boolean} [deferLifecycle=false] - when true, REPLACE materialize wires buses but
|
|
1659
|
+
* defers onMount until the sibling batch flush in {@link reconcileChildren} (same contract
|
|
1660
|
+
* as PLACE). Root reconcile must leave this false — nothing else flushes the root.
|
|
1420
1661
|
* @returns {Promise<RuntimeFiber<P>>}
|
|
1421
1662
|
*/
|
|
1422
|
-
reconcileFiber(current, nextVnode, parentFiber, parentScope) {
|
|
1663
|
+
reconcileFiber(current, nextVnode, parentFiber, parentScope, deferLifecycle = false) {
|
|
1423
1664
|
const sameType = current.vnode.type === nextVnode.type;
|
|
1424
1665
|
const sameKey = (current.vnode.key ?? null) === (nextVnode.key ?? null);
|
|
1425
1666
|
if (sameType && sameKey) {
|
|
@@ -1429,11 +1670,15 @@ class GraphRuntime {
|
|
|
1429
1670
|
// Sync fast-path if both destroy and materialize completed synchronously.
|
|
1430
1671
|
// Collect cleanup errors (ref clear / disposer) so a throwing finalize cannot
|
|
1431
1672
|
// abort REPLACE and fail-stop the surviving tree — same best-effort contract as unmount.
|
|
1673
|
+
//
|
|
1674
|
+
// When deferLifecycle is set (child reconcile batch), REPLACE must not run onMount
|
|
1675
|
+
// before later sibling buses are wired — otherwise a replaced publisher's mount-time
|
|
1676
|
+
// publish is silently dropped by a not-yet-wired PLACE/REPLACE listener beside it.
|
|
1432
1677
|
const destroyRes = this.destroyFiber(current, []);
|
|
1433
1678
|
if (isThenable(destroyRes)) {
|
|
1434
|
-
return destroyRes.then(() => this.materialize(nextVnode, parentFiber, parentScope));
|
|
1679
|
+
return destroyRes.then(() => this.materialize(nextVnode, parentFiber, parentScope, deferLifecycle));
|
|
1435
1680
|
}
|
|
1436
|
-
return this.materialize(nextVnode, parentFiber, parentScope);
|
|
1681
|
+
return this.materialize(nextVnode, parentFiber, parentScope, deferLifecycle);
|
|
1437
1682
|
}
|
|
1438
1683
|
/**
|
|
1439
1684
|
* Updates an existing fiber: applies new props to the instance,
|
|
@@ -1591,12 +1836,13 @@ class GraphRuntime {
|
|
|
1591
1836
|
fiber.lifecycleStatus = fiber.engine.getStatus();
|
|
1592
1837
|
attachCleanupErrors();
|
|
1593
1838
|
};
|
|
1594
|
-
|
|
1839
|
+
// Siblings in compose order — same contract as destroyFiber / clean unmount.
|
|
1840
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
1595
1841
|
try {
|
|
1596
1842
|
const destroyRes = this.destroyFiber(children[i], cleanupErrors);
|
|
1597
1843
|
if (isThenable(destroyRes)) {
|
|
1598
1844
|
return destroyRes.then(async () => {
|
|
1599
|
-
for (let j = i
|
|
1845
|
+
for (let j = i + 1; j < children.length; j += 1) {
|
|
1600
1846
|
try {
|
|
1601
1847
|
const r = this.destroyFiber(children[j], cleanupErrors);
|
|
1602
1848
|
if (isThenable(r)) {
|
|
@@ -1610,7 +1856,7 @@ class GraphRuntime {
|
|
|
1610
1856
|
return finishParent();
|
|
1611
1857
|
}, async (err) => {
|
|
1612
1858
|
cleanupErrors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1613
|
-
for (let j = i
|
|
1859
|
+
for (let j = i + 1; j < children.length; j += 1) {
|
|
1614
1860
|
try {
|
|
1615
1861
|
const r = this.destroyFiber(children[j], cleanupErrors);
|
|
1616
1862
|
if (isThenable(r)) {
|
|
@@ -1753,7 +1999,7 @@ class GraphRuntime {
|
|
|
1753
1999
|
const n = nextVnodes.length;
|
|
1754
2000
|
const stableResult = [];
|
|
1755
2001
|
for (let i = 0; i < n; i++) {
|
|
1756
|
-
const reconciled = this.reconcileFiber(currentChildren[i], nextVnodes[i], parentFiber, childScope);
|
|
2002
|
+
const reconciled = this.reconcileFiber(currentChildren[i], nextVnodes[i], parentFiber, childScope, true);
|
|
1757
2003
|
if (isThenable(reconciled)) {
|
|
1758
2004
|
return this.continueStableReconcileAsync(stableResult, reconciled, i, nextVnodes, currentChildren, parentFiber, childScope);
|
|
1759
2005
|
}
|
|
@@ -1780,7 +2026,7 @@ class GraphRuntime {
|
|
|
1780
2026
|
this.stableAsyncContinueCount += 1;
|
|
1781
2027
|
resultSoFar.push(await pending);
|
|
1782
2028
|
for (let i = pendingIdx + 1; i < nextVnodes.length; i++) {
|
|
1783
|
-
const reconciled = this.reconcileFiber(currentChildren[i], nextVnodes[i], parentFiber, childScope);
|
|
2029
|
+
const reconciled = this.reconcileFiber(currentChildren[i], nextVnodes[i], parentFiber, childScope, true);
|
|
1784
2030
|
resultSoFar.push(isThenable(reconciled) ? await reconciled : reconciled);
|
|
1785
2031
|
}
|
|
1786
2032
|
return resultSoFar;
|
|
@@ -1874,18 +2120,18 @@ class GraphRuntime {
|
|
|
1874
2120
|
throw new Error(`[Effectable] GraphRuntime: fiber with key "${nextKey}" not found in map.`);
|
|
1875
2121
|
}
|
|
1876
2122
|
keyedCurrentMap.delete(nextKey);
|
|
1877
|
-
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
2123
|
+
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope, true);
|
|
1878
2124
|
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
1879
2125
|
}
|
|
1880
2126
|
else if (nextKey === undefined && unkeyedIdx < unkeyedCurrent.length) {
|
|
1881
2127
|
const currentFiber = unkeyedCurrent[unkeyedIdx];
|
|
1882
2128
|
unkeyedIdx += 1;
|
|
1883
|
-
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
2129
|
+
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope, true);
|
|
1884
2130
|
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
1885
2131
|
}
|
|
1886
2132
|
else {
|
|
1887
|
-
// New node — PLACE
|
|
1888
|
-
const newRes = this.materialize(nextVnode, parentFiber, childScope);
|
|
2133
|
+
// New node — PLACE: defer startup so later sibling buses wire first.
|
|
2134
|
+
const newRes = this.materialize(nextVnode, parentFiber, childScope, true);
|
|
1889
2135
|
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
1890
2136
|
}
|
|
1891
2137
|
}
|
|
@@ -1913,12 +2159,12 @@ class GraphRuntime {
|
|
|
1913
2159
|
if (unkeyedIdx < unkeyedCurrent.length) {
|
|
1914
2160
|
const currentFiber = unkeyedCurrent[unkeyedIdx];
|
|
1915
2161
|
unkeyedIdx += 1;
|
|
1916
|
-
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope);
|
|
2162
|
+
const reconciledRes = this.reconcileFiber(currentFiber, nextVnode, parentFiber, childScope, true);
|
|
1917
2163
|
nextChildren.push(isThenable(reconciledRes) ? await reconciledRes : reconciledRes);
|
|
1918
2164
|
}
|
|
1919
2165
|
else {
|
|
1920
|
-
// New node — PLACE
|
|
1921
|
-
const newRes = this.materialize(nextVnode, parentFiber, childScope);
|
|
2166
|
+
// New node — PLACE: defer startup so later sibling buses wire first.
|
|
2167
|
+
const newRes = this.materialize(nextVnode, parentFiber, childScope, true);
|
|
1922
2168
|
nextChildren.push(isThenable(newRes) ? await newRes : newRes);
|
|
1923
2169
|
}
|
|
1924
2170
|
}
|
|
@@ -1933,6 +2179,15 @@ class GraphRuntime {
|
|
|
1933
2179
|
}
|
|
1934
2180
|
}
|
|
1935
2181
|
}
|
|
2182
|
+
// Flush PLACE/REPLACE startups in compose order after every new sibling is wired.
|
|
2183
|
+
for (const child of nextChildren) {
|
|
2184
|
+
if (child.constructionJournal?.lifecycleDeferred === true) {
|
|
2185
|
+
const flush = this.flushDeferredLifecycleTree(child);
|
|
2186
|
+
if (isThenable(flush)) {
|
|
2187
|
+
await flush;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
1936
2191
|
return nextChildren;
|
|
1937
2192
|
}
|
|
1938
2193
|
catch (primaryError) {
|