@solidjs/signals 2.0.0-rc.8 → 2.0.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/dev-shared.js +1399 -285
  2. package/dist/dev.attribution.js +459 -307
  3. package/dist/dev.js +1864 -435
  4. package/dist/observe/affects.js +3 -1
  5. package/dist/observe/attribution.js +7 -1
  6. package/dist/observe/boundaries.js +209 -154
  7. package/dist/observe/core/action.js +18 -8
  8. package/dist/observe/core/async.js +220 -110
  9. package/dist/observe/core/attribution-costs.js +66 -0
  10. package/dist/observe/core/attribution-feedback.js +282 -0
  11. package/dist/observe/core/attribution-hooks.js +23 -1
  12. package/dist/observe/core/attribution-queries.js +28 -0
  13. package/dist/observe/core/attribution.js +262 -485
  14. package/dist/observe/core/constants.js +34 -1
  15. package/dist/observe/core/context.js +3 -3
  16. package/dist/observe/core/core.js +867 -367
  17. package/dist/observe/core/dev.js +78 -17
  18. package/dist/observe/core/effect.js +67 -51
  19. package/dist/observe/core/error-hooks.js +71 -0
  20. package/dist/observe/core/external.js +4 -4
  21. package/dist/observe/core/graph.js +37 -37
  22. package/dist/observe/core/heap.js +45 -45
  23. package/dist/observe/core/invariants.js +2 -0
  24. package/dist/observe/core/lanes.js +86 -49
  25. package/dist/observe/core/optimistic.js +242 -95
  26. package/dist/observe/core/owner.js +98 -84
  27. package/dist/observe/core/scheduler.js +543 -305
  28. package/dist/observe/core/verdict.js +247 -129
  29. package/dist/observe/index.js +7 -3
  30. package/dist/observe/map.js +126 -124
  31. package/dist/observe/signals.js +33 -17
  32. package/dist/observe/store/index.js +2 -0
  33. package/dist/observe/store/next/optimistic.js +141 -132
  34. package/dist/observe/store/next/projection.js +34 -21
  35. package/dist/observe/store/next/reconcile.js +58 -56
  36. package/dist/observe/store/next/store.js +260 -146
  37. package/dist/observe/store/store.js +5 -3
  38. package/dist/observe/store/utils.js +948 -135
  39. package/dist/prod/attribution.js +26 -17
  40. package/dist/prod/boundaries.js +128 -76
  41. package/dist/prod/core/action.js +16 -8
  42. package/dist/prod/core/async.js +251 -143
  43. package/dist/prod/core/constants.js +34 -1
  44. package/dist/prod/core/context.js +3 -3
  45. package/dist/prod/core/core.js +843 -347
  46. package/dist/prod/core/dev.js +17 -1
  47. package/dist/prod/core/effect.js +48 -34
  48. package/dist/prod/core/error-hooks.js +71 -0
  49. package/dist/prod/core/external.js +4 -4
  50. package/dist/prod/core/graph.js +37 -37
  51. package/dist/prod/core/heap.js +45 -45
  52. package/dist/prod/core/lanes.js +90 -53
  53. package/dist/prod/core/optimistic.js +247 -100
  54. package/dist/prod/core/owner.js +57 -45
  55. package/dist/prod/core/scheduler.js +543 -305
  56. package/dist/prod/core/verdict.js +245 -127
  57. package/dist/prod/index.js +7 -3
  58. package/dist/prod/map.js +112 -112
  59. package/dist/prod/signals.js +28 -12
  60. package/dist/prod/store/next/optimistic.js +135 -128
  61. package/dist/prod/store/next/projection.js +31 -20
  62. package/dist/prod/store/next/store.js +325 -252
  63. package/dist/prod/store/store.js +2 -2
  64. package/dist/prod/store/utils.js +946 -135
  65. package/dist/types/attribution.d.ts +7 -2
  66. package/dist/types/attribution.prod.d.ts +10 -1
  67. package/dist/types/boundaries.d.ts +10 -1
  68. package/dist/types/core/action.d.ts +12 -5
  69. package/dist/types/core/attribution-costs.d.ts +35 -0
  70. package/dist/types/core/attribution-feedback.d.ts +133 -0
  71. package/dist/types/core/attribution-hooks.d.ts +28 -3
  72. package/dist/types/core/attribution-queries.d.ts +10 -0
  73. package/dist/types/core/attribution.d.ts +51 -172
  74. package/dist/types/core/constants.d.ts +33 -0
  75. package/dist/types/core/core.d.ts +186 -5
  76. package/dist/types/core/dev.d.ts +129 -9
  77. package/dist/types/core/error-hooks.d.ts +71 -0
  78. package/dist/types/core/index.d.ts +3 -1
  79. package/dist/types/core/invariants.d.ts +4 -0
  80. package/dist/types/core/lanes.d.ts +31 -4
  81. package/dist/types/core/scheduler.d.ts +95 -2
  82. package/dist/types/core/types.d.ts +3 -0
  83. package/dist/types/index.d.ts +2 -2
  84. package/dist/types/signals.d.ts +8 -0
  85. package/dist/types/store/index.d.ts +2 -1
  86. package/dist/types/store/next/optimistic.d.ts +1 -1
  87. package/dist/types/store/next/store.d.ts +6 -5
  88. package/dist/types/store/next/target.d.ts +1 -1
  89. package/dist/types/store/utils.d.ts +177 -6
  90. package/package.json +1 -1
@@ -200,6 +200,18 @@ const CONFIG_SLOT_NODE = 1 << 18;
200
200
  * paths (asyncWrite, transition-held recompute); cleared by a fresh optimistic
201
201
  * write (a new override re-masks) and by the revert. */
202
202
  const CONFIG_OVERRIDE_SUPERSEDED = 1 << 19;
203
+ /** HELD children (#3404): this node's `_firstChild` chain (and `_disposal`
204
+ * list) was built by a recompute whose result has not committed — a staged
205
+ * value, a pending window, or a run under a held transaction. A later
206
+ * recompute may tear those children down immediately: nothing observable
207
+ * was ever built on them. Unset, the children belong to the committed frame
208
+ * and a recompute defers them as zombies (`_pendingFirstChild`) until this
209
+ * node commits — regardless of whether the recompute runs under a
210
+ * transaction. A parked node (status propagation stamps `_transition`
211
+ * without recomputing) recomputed when its source lands otherwise disposed
212
+ * its committed children mid-hold, running their cleanups before the
213
+ * transaction's atomic reveal. Cleared by `commitPendingNode`. */
214
+ const CONFIG_HELD_CHILDREN = 1 << 20;
203
215
  /** In-flight async node whose inputs were PUBLISHED while it was pending: a
204
216
  * batch or transaction committed with the node still `STATUS_PENDING` (an
205
217
  * unobserved flight, #3305), so the inputs are on screen and the node's
@@ -211,6 +223,27 @@ const CONFIG_OVERRIDE_SUPERSEDED = 1 << 19;
211
223
  * lane-revealed. Set by `commitPendingNodes`; cleared when the node next
212
224
  * enters pending fresh (a new flight from a settled state). */
213
225
  const CONFIG_INPUTS_PUBLISHED = 1 << 21;
226
+ /** A28 (4): the node was written inside a recompute that ran OUTSIDE a flush
227
+ * (a creation-time compute — boundary machinery, a mapArray's first run). Such
228
+ * a write is promoted at that recompute's end: readers in the same block see
229
+ * it. Cleared when the next flush begins; set only on that rare path. */
230
+ const CONFIG_PROMOTED = 1 << 22;
231
+ /** A28 for same-tick adoption: the node was staged outside a flush and then
232
+ * adopted by a transaction (initTransition) before any flush carried the
233
+ * staging — the stamp says "held", but nothing flushed is staged for it, so
234
+ * on no channel is the write visible yet: `latest()` answers the committed
235
+ * value, the verdict sees nothing pending (as the store's leaves already did
236
+ * through their own selection). Cleared when the carrying flush re-stamps the
237
+ * transaction's pending nodes (reassignPendingTransition). */
238
+ const CONFIG_ADOPTED_UNFLUSHED = 1 << 24;
239
+ /** The node's active override is a DERIVED one: a lane pass published its
240
+ * speculative result into the override slot instead of `_value` (lanes
241
+ * stage — an optimistic derivation is an override, #3479). Its truth is not
242
+ * `_value` but a recompute from its inputs' truth, so the body-end
243
+ * supersession (`endOptimism`) and the authoritative-flight blockage
244
+ * (`transitionBlocked`) skip it; the revert drops the override and re-derives
245
+ * it (`resolveOptimisticNodes`). Cleared with the override. */
246
+ const CONFIG_DERIVED_OVERRIDE = 1 << 23;
214
247
  const STATUS_PENDING = 1 << 0;
215
248
  const STATUS_ERROR = 1 << 1;
216
249
  const STATUS_UNINITIALIZED = 1 << 2;
@@ -249,8 +282,18 @@ const defaultContext = {};
249
282
  const $REFRESH = Symbol("refresh");
250
283
 
251
284
  let attrHooks = null;
285
+ /**
286
+ * The installed engine, registered on `globalThis` as well (the records
287
+ * channel's reason, see `Records`): a wire layer bundled without a framework
288
+ * import — `@solidjs/web`'s server-function client — stamps the records it
289
+ * emits through the engine's `currentOrigin`, and this is its reach. The
290
+ * module binding stays the core's own read (one null check per hook site);
291
+ * the registration mirrors it.
292
+ */
293
+ const INSTALLED = Symbol.for("@solidjs/signals/observe/attribution");
252
294
  function setAttributionHooks(hooks) {
253
295
  attrHooks = hooks;
296
+ globalThis[INSTALLED] = hooks ?? undefined;
254
297
  }
255
298
  /**
256
299
  * Run `fn` as the handler of a user interaction: every root write it performs
@@ -305,6 +348,18 @@ function withOrigin(ref, fn) {
305
348
  hooks.originEnd();
306
349
  }
307
350
  }
351
+ /**
352
+ * The provenance a root write performed now would carry, as the installed
353
+ * engine sees it (`AttributionHooks.currentOrigin`); `undefined` with no
354
+ * engine, or when nothing is in effect. Reachable as
355
+ * `OBSERVE.attribution.currentOrigin` — how a runtime stamps a fact of its
356
+ * own (a server-function call) with the interaction or navigation it ran
357
+ * for, so an observer joins the two by identity.
358
+ */
359
+ function currentOrigin() {
360
+ const hooks = attrHooks;
361
+ return hooks === null ? undefined : hooks.currentOrigin();
362
+ }
308
363
 
309
364
  /** First warning when a change reaches (or a pass tracks) this many edges. */
310
365
  const GRAPH_SIZE_WARN_AT = 2000;
@@ -347,13 +402,59 @@ const attributionSlot = {
347
402
  return attrHooks;
348
403
  },
349
404
  withInteraction,
350
- withOrigin
405
+ withOrigin,
406
+ currentOrigin
351
407
  };
408
+ // The records channel is process-wide (see `Records`): a host that bundles
409
+ // the core into its server build beside an instrumented `--import`ed copy
410
+ // holds two of this module, and a listener installed through one must hear
411
+ // the records the render emits through the other. The registered key makes
412
+ // every copy find the one listener set; the object is generic — a Map of
413
+ // type to listener set — and carries no knowledge of the records.
414
+ const RECORDS = Symbol.for("@solidjs/signals/observe/records");
415
+ function recordsChannel() {
416
+ const g = globalThis;
417
+ if (g[RECORDS]) return g[RECORDS];
418
+ const listeners = new Map();
419
+ return (g[RECORDS] = {
420
+ subscribe(type, listener) {
421
+ let set = listeners.get(type);
422
+ if (!set) listeners.set(type, (set = new Set()));
423
+ set.add(listener);
424
+ return () => {
425
+ set.delete(listener);
426
+ };
427
+ },
428
+ observed(type) {
429
+ const set = listeners.get(type);
430
+ return set !== undefined && set.size > 0;
431
+ },
432
+ emit(type, event, live) {
433
+ const set = listeners.get(type);
434
+ if (set === undefined || set.size === 0) return;
435
+ // Snapshot: a listener unsubscribing (itself or another) mid-delivery
436
+ // must not skip or double-call anyone this round. A throwing listener
437
+ // is reported; the others, and what was observed, are unaffected.
438
+ for (const listener of [...set]) {
439
+ try {
440
+ listener(event, live);
441
+ } catch (error) {
442
+ console.error(error);
443
+ }
444
+ }
445
+ }
446
+ });
447
+ }
352
448
  const OBSERVE = {
353
449
  diagnostics,
450
+ records: recordsChannel(),
354
451
  attribution: attributionSlot,
355
- subjectOf(event) {
356
- return eventSubjects.get(event);
452
+ // Replaced by solid-js's server entry (see `ServerObserve`); on the
453
+ // client the slot stays this placeholder. The cast: the interface is
454
+ // empty HERE and gains its members by augmentation downstream.
455
+ server: {},
456
+ subjectOf(record) {
457
+ return eventSubjects.get(record);
357
458
  },
358
459
  exclude(owner) {
359
460
  excludedOwners.add(owner);
@@ -464,13 +565,26 @@ function takeFooter(entry) {
464
565
  return consoleFooter(entry);
465
566
  }
466
567
  /**
467
- * The subject each emitted event was about, for the console step: events are
468
- * serializable records and cannot carry the node, but the console can show
469
- * what the node knowsa rendering runtime may stamp a binding effect with
470
- * the DOM element it writes (`_devElement`), and a live element reference
471
- * beside the message is the most addressable pointer a console can print.
568
+ * The subject each emitted event was about: events are serializable records
569
+ * and cannot carry the node, so the node is kept beside the record for the
570
+ * in-process consumers that want it the console step, which can show what
571
+ * the node knows (a rendering runtime may stamp a binding effect with the DOM
572
+ * element it writes, `_devElement`, and a live element reference beside the
573
+ * message is the most addressable pointer a console can print), and devtools
574
+ * that go from a re-run record back to the scope that ran. Keyed by the
575
+ * record object, so the subject lives exactly as long as some consumer holds
576
+ * the record (a ring buffer, a captured artifact) — the same lifetime the
577
+ * node had when records carried it directly.
472
578
  */
473
579
  const eventSubjects = new WeakMap();
580
+ /** Register `subject` as what `record` was about — see `Observe.subjectOf`. */
581
+ function recordSubject(record, subject) {
582
+ eventSubjects.set(record, subject);
583
+ }
584
+ /** The live subject `record` was about, if its emitter registered one. */
585
+ function subjectOf(record) {
586
+ return eventSubjects.get(record);
587
+ }
474
588
  /**
475
589
  * The console face of a diagnostic — ONE entry per finding: the message, the
476
590
  * owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
@@ -773,6 +887,47 @@ function laneHeld(lane) {
773
887
  for (const node of lane._pendingAsync) if (waitingTransition(node) !== null) return true;
774
888
  return false;
775
889
  }
890
+ /**
891
+ * Lanes mirror transitions (#3460): a render effect OFF a HELD lane that reads
892
+ * a value the lane is revealing — an override, a `latest()` shadow — sees the
893
+ * committed value, exactly as a stale reader of a held transaction does
894
+ * (A15 reveal corollary): it publishes now, with the frame that is on screen
895
+ * (the lane defers its own readers' runs, so the committed value is what is
896
+ * visible), entangles nothing — a sync write is never held by a lane — and
897
+ * re-derives at the release. The release re-run rides the lane's own render
898
+ * queue, which runs when the lane reveals (runLaneEffects) or its transaction
899
+ * commits (cleanupCompletedLanes). A reader ON the lane computes the lane's
900
+ * reveal and takes the value as before.
901
+ *
902
+ * OFF the lane is provenance, not membership — the transaction mirror
903
+ * exactly: a stale reader of a transaction is a pass that runs outside it. A
904
+ * pass under the lane's own transaction is the lane's work — its write (lane
905
+ * posture), or the landing of its async, which re-enters the transaction
906
+ * (#3334) and runs a member with no ambient lane. Read as an outsider, that
907
+ * pass published the committed view and queued a replay that revealed the
908
+ * override beside its unready derivation at the release (`1:0` for an
909
+ * optimistic frame that never became ready; #3479 review). Membership is the
910
+ * wrong test the other way: a member re-run by a sibling's sync write is a
911
+ * mainline pass and shows the committed view.
912
+ */
913
+ function readsHeldCommitted(owner, c) {
914
+ const lane = resolveLane(owner);
915
+ if (!lane || !laneHeld(lane)) return false;
916
+ if (ownsLane(lane, owner)) return false;
917
+ lane._effectQueues[0].push(() => c._flags & REACTIVE_DISPOSED || enqueueSub(c));
918
+ return true;
919
+ }
920
+ /** The ownership relation for a lane hold (core `ownsHold`, §6 ruling 2): the
921
+ * running pass owns `lane`'s hold if it runs under the transition that owns
922
+ * the lane (the node's, resolved through override ownership and merges) or
923
+ * inside the lane itself. */
924
+ function ownsLane(lane, owner) {
925
+ if (activeTransition !== null) {
926
+ const t = resolveTransition(owner);
927
+ if (t && ownsHold(t)) return true;
928
+ }
929
+ return currentOptimisticLane !== null && findLane(currentOptimisticLane) === lane;
930
+ }
776
931
  /**
777
932
  * Merge two lanes when their dependency graphs overlap.
778
933
  */
@@ -816,13 +971,6 @@ function resolveTransition(el) {
816
971
  }
817
972
  return resolveLane(el)?._transition ?? el._transition;
818
973
  }
819
- /**
820
- * Check if a node has an active optimistic override.
821
- */
822
- function hasActiveOverride(el) {
823
- const x = el._x;
824
- return x !== null && x._overrideValue !== undefined && x._overrideValue !== NOT_PENDING;
825
- }
826
974
  /**
827
975
  * Assign or merge a lane onto a node. At convergence points (node already has
828
976
  * a different active lane), merge unless the node has an active override.
@@ -831,17 +979,21 @@ function assignOrMergeLane(el, sourceLane) {
831
979
  const sourceRoot = findLane(sourceLane);
832
980
  const existing = el._x?._optimisticLane;
833
981
  if (existing) {
834
- // If the subscriber's lane was merged into another lane, it's stale
835
- // replace it with the new source lane instead of following the merge chain
836
- // (which would incorrectly merge the new lane into the old group)
837
- if (existing._mergedInto) {
838
- ext(el)._optimisticLane = sourceLane;
839
- el._config |= CONFIG_HAS_LANE;
840
- return;
841
- }
982
+ // A merged lane is followed to its root like any other: the root is where
983
+ // the subscriber's affinity lives now. Replacing it with the source lane
984
+ // outright (as this once did) skipped the parent/child check below an
985
+ // isPending reader of two async siblings had their two companion lanes
986
+ // merge, and the next parent-lane notification then moved it onto the
987
+ // held parent, where its verdict waited on the async it reports (#3409).
842
988
  const existingRoot = findLane(existing);
843
989
  if (activeLanes.has(existingRoot)) {
844
- if (existingRoot !== sourceRoot && !hasActiveOverride(el)) {
990
+ // A WRITTEN override is its own lane's source and merges nothing
991
+ // through it; a derived one (lanes stage, #3479) is a plain member —
992
+ // the shared reader that merges two writers' lanes carries one.
993
+ if (
994
+ existingRoot !== sourceRoot &&
995
+ (!hasActiveOverride(el) || el._config & CONFIG_DERIVED_OVERRIDE)
996
+ ) {
845
997
  // Parent-child lanes stay independent so isPending resolves without
846
998
  // waiting for the parent's async. The child keeps ownership.
847
999
  if (sourceRoot._parentLane && findLane(sourceRoot._parentLane) === existingRoot) {
@@ -872,8 +1024,16 @@ const zombieQueue = {
872
1024
  };
873
1025
  /** runHeap callback that discards a queued zombie recompute instead of running
874
1026
  * it: unlink pure recompute entries; strip just the recompute bit from dirtied
875
- * height-adjust entries so their height work still happens. */
1027
+ * height-adjust entries so their height work still happens. A zombie dirtied
1028
+ * through the lane channel (OPTIMISTIC_DIRTY — an override or a `latest()`
1029
+ * companion) runs instead (#3444): a zombie renders mainline until the commit
1030
+ * disposes it, and the lane's values ARE the mainline frame — the still-visible
1031
+ * branch a held `Show` is removing showed the old `latest(count)` beside the
1032
+ * new one outside. Its pass runs under the lane and its run lands on the
1033
+ * lane's effect queue, so a held lane defers it exactly as it defers every
1034
+ * other reader's. */
876
1035
  function cancelZombieRecompute(el) {
1036
+ if (el._flags & REACTIVE_OPTIMISTIC_DIRTY) return GlobalQueue._update(el);
877
1037
  if (el._flags & REACTIVE_IN_HEAP_HEIGHT) el._flags &= -12;
878
1038
  else {
879
1039
  deleteFromHeap(el, zombieQueue);
@@ -903,11 +1063,19 @@ let _hitUnhandledAsync = false;
903
1063
  // fact about the MOUNT ("the root mount will be deferred"), not about each
904
1064
  // pending render effect — N async siblings at mount used to produce N copies.
905
1065
  let _reportedUnhandledAsync = false;
906
- // Store property nodes that were created solely to carry a pending write (no
907
- // subscribers at write time). Swept after each flush that commits pending
908
- // values any still without subs get disposed via their `_unobserved` hook,
909
- // releasing the slot in the parent store's node map.
1066
+ // Store property nodes whose last subscriber left while they carried state
1067
+ // the backing cannot reconstruct an optimistic override (overrides live on
1068
+ // nodes, over a clone the setter discards) or a staged write. Releasing the
1069
+ // slot then would drop the override: an optimistic store key read `0` the
1070
+ // moment its only reader gated away while the action was live (S7). Swept
1071
+ // after each flush — a node still without subs whose override and staging
1072
+ // have resolved is released through the slot hook; one that regained a
1073
+ // subscriber leaves the set.
910
1074
  const transientStoreNodes = new Set();
1075
+ /** Slot hook's deferral: release this node when its carried state resolves. */
1076
+ function deferSlotRelease(node) {
1077
+ transientStoreNodes.add(node);
1078
+ }
911
1079
  function canUseSimpleSyncFlush(queue) {
912
1080
  const batch = queue._batch;
913
1081
  return (
@@ -1000,6 +1168,7 @@ function mergeTransitionState(target, outgoing) {
1000
1168
  if (attrHooks !== null) attrHooks.transitionMerged(target, outgoing);
1001
1169
  outgoing._done = target;
1002
1170
  target._actions.push(...outgoing._actions);
1171
+ target._acted ||= outgoing._acted;
1003
1172
  for (const lane of activeLanes) if (lane._transition === outgoing) lane._transition = target;
1004
1173
  if (outgoing._optimisticNodes.length) {
1005
1174
  // Move (don't copy): the global queue's batch may still be the outgoing
@@ -1134,11 +1303,47 @@ function schedule() {
1134
1303
  scheduled = true;
1135
1304
  if (!syncDepth && !globalQueue._running && !projectionWriteActive) queueMicrotask(flush);
1136
1305
  }
1306
+ /**
1307
+ * Parked transactions whose reporter set changed without a write. A
1308
+ * transaction completes when nothing live reports a flight it waits on, but
1309
+ * the flush only judges the ACTIVE transaction: a parked one is re-entered by
1310
+ * a stamped node's landing or an action's resume. A reporter that stops
1311
+ * counting for another reason — its loading boundary flipped to the fallback
1312
+ * (#3375), or it was disposed by ambient work (#3372) — is neither: the
1313
+ * pruning in `reporterBlocksSource` would drop it at the next check, but no
1314
+ * check comes, and the writes held with it stay staged. Such sites record the
1315
+ * transaction here (deduped: one idle pass per transaction, however many
1316
+ * reporters changed); the flush re-enters it on an otherwise idle pass, so
1317
+ * the re-evaluation adopts no unrelated ambient work.
1318
+ */
1319
+ const wokenTransitions = [];
1320
+ /** Wake every parked transaction — for a site that knows a reporter stopped
1321
+ * counting but not whose (a boundary reset). */
1322
+ function wakeParked() {
1323
+ for (const t of transitions) wokenTransitions.includes(t) || wokenTransitions.push(t);
1324
+ schedule();
1325
+ }
1326
+ /** Transactions a mainline tick has PROPOSED against (A34, #3494): a write to a
1327
+ * node one of them holds — the same value or another — is a second proposal
1328
+ * on a contested node, and the tick reveals with the hold ("both are
1329
+ * suggesting a value; if one finished before the other that would be odd").
1330
+ * Entered at the next flush's start, where the ambient batch is adopted;
1331
+ * never from the write itself, which left `activeTransition` set across the
1332
+ * caller's block and made creation after the write the transaction's (A29). */
1333
+ const batchJoins = [];
1137
1334
  /**
1138
1335
  * Permanently halts the reactive system. Called when a user error escapes
1139
1336
  * every boundary — app state is undefined at that point, so scheduling stops
1140
1337
  * entirely rather than limping along with a half-applied update.
1141
1338
  */
1339
+ /**
1340
+ * The key a root owner carries its client error hook under (`render`'s
1341
+ * `onError`) — registered, so a runtime writes it with no import of the hook
1342
+ * module (core/error-hooks.ts) and no property mangling in the way. Defined
1343
+ * HERE, not there: a runtime that only writes the key must not retain the
1344
+ * hook machinery (pay-for-use).
1345
+ */
1346
+ const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
1142
1347
  function haltReactivity(cause) {
1143
1348
  if (halted) return;
1144
1349
  halted = true;
@@ -1353,11 +1558,29 @@ class GlobalQueue extends Queue {
1353
1558
  * authoritative-observer wake for a silent confirm. Installed with the
1354
1559
  * optimistic engine; only reachable on a node that has an override. */
1355
1560
  static _supersedeOverride = null;
1561
+ /** The flush's pre-verdict step (#3427): once the transaction's action
1562
+ * bodies have all ended and nothing authoritative is left in flight, the
1563
+ * engine supersedes every override still in force with the truth it
1564
+ * reverts to, so the graph re-derives from it now, as the transaction's
1565
+ * held work, instead of after the flights the overrides fed have landed.
1566
+ * True when it superseded something: the caller re-runs the heap ahead of
1567
+ * the verdict. The engine owns every gate (acted, actions drained, has
1568
+ * overrides, no store edits, no authoritative flight); null without it. */
1569
+ static _endOptimism = null;
1356
1570
  /** read()'s value for a TRACKED reader of a superseded node (#3331): the
1357
1571
  * staged truth, unless the reader is a stale (render) reader of another
1358
1572
  * transaction — then the displayed override, as it keeps a foreign
1359
1573
  * transaction's committed value over its staged write. */
1360
- static _supersededRead = null;
1574
+ /** A tracked read of an active override: the lane outside-view rule
1575
+ * (#3460) and the A18 supersession selection (#3331) — see optimistic.ts. */
1576
+ static _overrideRead = null;
1577
+ /** A lane pass's publish for a memo (#3479, lanes stage): the speculative
1578
+ * result becomes a DERIVED override, `_value` stays committed — see
1579
+ * optimistic.ts laneOverride. Set with the engine, which a lane implies. */
1580
+ static _laneOverride = null;
1581
+ /** Verdict-layer recompute in progress (companion creation, latest()/
1582
+ * isPending() pulls): never born held — see core.ts enterStagedRead. */
1583
+ static _verdictPull = false;
1361
1584
  /** setSignal's authoritative (projection-write) landing on an override-
1362
1585
  * covered node (#3331 store twin): stage the truth for its transaction's
1363
1586
  * commit whatever its relation to the committed value — a landing equal to
@@ -1369,7 +1592,12 @@ class GlobalQueue extends Queue {
1369
1592
  flush() {
1370
1593
  if (this._running) return;
1371
1594
  this._running = true;
1595
+ resyncUnflushedCompanions(); // A28, see above
1372
1596
  try {
1597
+ // The tick proposed against a hold (#3494): adopt its batch into it.
1598
+ // Inside the try: the adoption runs user comparators (the no-proposal
1599
+ // drop), and a throw there must not leave `_running` set.
1600
+ while (batchJoins.length) this.initTransition(batchJoins.pop());
1373
1601
  if (true) devCheckFlushStart();
1374
1602
  // Before runHeap for the same reason as the fast drain above; late
1375
1603
  // subscribers (an effect reading a swept memo this flush) revive it,
@@ -1377,9 +1605,18 @@ class GlobalQueue extends Queue {
1377
1605
  sweepDormant();
1378
1606
  runHeap(dirtyQueue, GlobalQueue._update);
1379
1607
  if (activeTransition) {
1608
+ // The action bodies are over: the overrides they leave in force
1609
+ // revert at this settle, and the correction is this transaction's
1610
+ // held work — re-derived here, under it, ahead of the verdict — not a
1611
+ // waterfall after the flights the overrides fed (#3427). After the
1612
+ // heap, not before: a synchronous body's own writes (a refresh that
1613
+ // puts an override node's source in flight) are judged applied.
1614
+ if (GlobalQueue._endOptimism?.(activeTransition)) runHeap(dirtyQueue, GlobalQueue._update);
1380
1615
  const isComplete = transitionComplete(activeTransition);
1381
1616
  if (!isComplete) {
1382
1617
  const stashedTransition = activeTransition;
1618
+ // Parked: the unchanged passes' inputs are held; their tails stay (A30).
1619
+ heldTrims.length = 0;
1383
1620
  // When the parking batch IS the transition, all of its writes commit
1384
1621
  // only with it — every zombie recompute they queued would run against
1385
1622
  // a world the zombie never displays (zombies render mainline until
@@ -1481,6 +1718,17 @@ class GlobalQueue extends Queue {
1481
1718
  }
1482
1719
  if (true) DEV.hooks.onUpdate?.();
1483
1720
  } finally {
1721
+ // Re-enter a woken transaction (see wokenTransitions) only from an
1722
+ // idle pass: entering adopts the ambient batch, and staged or dirty
1723
+ // ambient work would be held behind flights it never read. `scheduled`
1724
+ // is that test here — after the park exit as well as the normal one:
1725
+ // it was recomputed from the heap this pass, every write since re-armed
1726
+ // it, and optimistic ambient nodes reverted with the finalize — so a
1727
+ // wake in a pass with work simply falls to the next. Entering re-arms
1728
+ // it itself; a dead (completed) wake is a bare return in
1729
+ // initTransition, and the loop moves on to the next.
1730
+ while (!scheduled && !activeTransition && wokenTransitions.length)
1731
+ this.initTransition(wokenTransitions.pop());
1484
1732
  this._running = false;
1485
1733
  }
1486
1734
  }
@@ -1559,9 +1807,45 @@ class GlobalQueue extends Queue {
1559
1807
  // must not entangle unrelated writes to it; the same rule holds one hop
1560
1808
  // downstream: propagation never queues pended subscribers as pending
1561
1809
  // nodes, see propagateAffectsMark, #2893.
1810
+ // Adopted outside a flush: the staging is still unflushed — the stamp
1811
+ // must not make it read as held-and-carried (CONFIG_ADOPTED_UNFLUSHED;
1812
+ // the carrying flush clears it in reassignPendingTransition).
1813
+ const adopted = this._running ? 0 : CONFIG_ADOPTED_UNFLUSHED;
1562
1814
  for (let i = 0; i < batch._pendingNodes.length; i++) {
1563
1815
  const node = batch._pendingNodes[i];
1816
+ // A tick that nets to the committed value proposed nothing (A34, #3494):
1817
+ // `setShow(false); setShow(true)` beside a write that opens a hold
1818
+ // left `show` staged at its own value, stamped, pending to the
1819
+ // verdict, and its next mainline write held by a flight it never
1820
+ // derived from. Unstage it here — its subscribers were walked at the
1821
+ // write and re-derive the same value. Writes only: a signal's staging
1822
+ // is always one, a computed's only under REACTIVE_MANUAL_WRITE
1823
+ // (`createSignal(fn)`'s setter, #3519 review) — otherwise it is its
1824
+ // pass's result, which may equal an uninitialized `undefined` (a
1825
+ // born-held first pass). `_equals: false` opts out. The unstaging is
1826
+ // the commit's own path (commitPendingNode with nothing staged): the
1827
+ // manual-write flag, companions and the rest are cleaned up as a
1828
+ // commit would, and the node is stamped nowhere. Unstamped
1829
+ // only: a node already a transaction's — arriving here as a parked
1830
+ // batch folds into a merge — carries a FLUSHED proposal a later
1831
+ // rewrite brought back to the committed value; it is held, not
1832
+ // proposal-free. Dropped, it kept the dead stamp, and the next write
1833
+ // to it queued under the merged transaction a value the commit then
1834
+ // skipped as another's (fuzzer latest-2 #1470, S3).
1835
+ if (
1836
+ node._transition === null &&
1837
+ node._pendingValue !== NOT_PENDING &&
1838
+ (!node._fn ||
1839
+ (node._flags & REACTIVE_MANUAL_WRITE && !(node._statusFlags & STATUS_UNINITIALIZED))) &&
1840
+ node._equals &&
1841
+ node._equals(node._value, node._pendingValue)
1842
+ ) {
1843
+ node._pendingValue = NOT_PENDING;
1844
+ commitPendingNode(node);
1845
+ continue;
1846
+ }
1564
1847
  node._transition = activeTransition;
1848
+ node._config |= adopted;
1565
1849
  activeTransition._pendingNodes.push(node);
1566
1850
  }
1567
1851
  for (let i = 0; i < batch._optimisticNodes.length; i++) {
@@ -1596,6 +1880,7 @@ class GlobalQueue extends Queue {
1596
1880
  function queuePendingNode(node) {
1597
1881
  lastStagedNodeName = node._name ?? null;
1598
1882
  currentBatch._pendingNodes.push(node);
1883
+ if (!globalQueue._running) markUnflushedStaged(); // A28
1599
1884
  }
1600
1885
  // Dev-only attribution for the flush loop guard (#3140): when the guard
1601
1886
  // trips, naming what the loop kept chewing on lets the app author attribute
@@ -1698,6 +1983,8 @@ function commitPendingNode(n) {
1698
1983
  if (n._pendingValue !== NOT_PENDING) {
1699
1984
  n._value = n._pendingValue;
1700
1985
  n._pendingValue = NOT_PENDING;
1986
+ // A node born held (recompute) initializes at this commit.
1987
+ c._statusFlags &= ~STATUS_UNINITIALIZED;
1701
1988
  // Set _modified for effects, but not for tracked effects (they handle their own scheduling)
1702
1989
  if (n._type && n._type !== EFFECT_TRACKED) n._modified = true;
1703
1990
  // A quiet re-ask classification preserved through a held landing dies
@@ -1712,6 +1999,14 @@ function commitPendingNode(n) {
1712
1999
  // store to an always-present computed slot.
1713
2000
  c._loading = false;
1714
2001
  c._flags &= ~REACTIVE_MANUAL_WRITE;
2002
+ // The children this commit publishes are the frame's now (#3404) — and so
2003
+ // are the dependencies of the pass that produced the value: the previous
2004
+ // frame's tail goes (A30, #3410; `recompute` left it for a staged pass). Only
2005
+ // after a clean pass: `_error` is cleared by a clean pass or by the node's
2006
+ // own landing (whose pass was clean), so a set `_error` means the last pass
2007
+ // threw, kept its full list, and `_depsTail` marks where it stopped.
2008
+ if (c._x?._error == null) trimStaleDeps(c);
2009
+ c._config &= ~CONFIG_HELD_CHILDREN;
1715
2010
  if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
1716
2011
  // A flight this commit leaves in the air (unobserved, or observed only by
1717
2012
  // a boundary) now has PUBLISHED inputs: its committed value is stale
@@ -1737,7 +2032,17 @@ function setStoreCommitHook(fn) {
1737
2032
  * confirming truth is committed and the override still displays, a torn
1738
2033
  * frame no timeline contains. */
1739
2034
  const heldRevealed = [];
2035
+ /** Unchanged passes with a stale dependency tail, waiting on this flush's
2036
+ * verdict (A30, #3469). A pass that changed nothing replaced nothing either —
2037
+ * and cannot know at its own tail whether the flush that ran it will park:
2038
+ * parked, its inputs are held and the committed frame still derives from the
2039
+ * tail (`b() ? b() : a()` computed `1` from the held `b`, equal to the `1` it
2040
+ * had from `a` — with `a` trimmed, the mainline `a = 2` never reached it).
2041
+ * Trimmed when the flush commits; dropped with a park, the tail stays linked
2042
+ * until a committing pass trims it (one spurious recompute at most). */
2043
+ const heldTrims = [];
1740
2044
  function commitPendingNodes() {
2045
+ while (heldTrims.length) trimStaleDeps(heldTrims.pop());
1741
2046
  const pendingNodes = currentBatch._pendingNodes;
1742
2047
  for (let i = 0; i < pendingNodes.length; i++) {
1743
2048
  const node = pendingNodes[i];
@@ -1884,6 +2189,7 @@ function shiftAffectsMarks(delta) {
1884
2189
  function reassignPendingTransition(pendingNodes) {
1885
2190
  for (let i = 0; i < pendingNodes.length; i++) {
1886
2191
  pendingNodes[i]._transition = activeTransition;
2192
+ pendingNodes[i]._config &= ~CONFIG_ADOPTED_UNFLUSHED; // this flush carried it
1887
2193
  }
1888
2194
  }
1889
2195
  const globalQueue = new GlobalQueue();
@@ -1985,13 +2291,65 @@ function flush(fn) {
1985
2291
  function runQueue(queue, type) {
1986
2292
  for (let i = 0; i < queue.length; i++) queue[i](type);
1987
2293
  }
1988
- function reporterBlocksSource(reporter, source) {
1989
- if (reporter._flags & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false;
2294
+ function reporterBlocksSource(reporter, source, verdict) {
2295
+ const flags = reporter._flags;
2296
+ if (flags & REACTIVE_DISPOSED) return false;
2297
+ // A zombie renders until the commit that disposes it (#3463): while its
2298
+ // removal is staged in a live transaction it is still on screen, and what
2299
+ // it displays must stay consistent with the frame — a held `Show`'s
2300
+ // `Details: 0` beside the lane's `Value: 1` otherwise. Its say is moot for
2301
+ // the verdict of the transaction that stages the removal (`verdict`): done,
2302
+ // and the commit disposes it; not done, and it stays parked regardless. A
2303
+ // zombie whose removal commits this flush (owner's pass not held) is dead.
2304
+ // The owner is stamped when the flush parks; held in this flush, its
2305
+ // staging transaction is the active one.
2306
+ if (flags & REACTIVE_ZOMBIE) {
2307
+ let p = reporter;
2308
+ while (p && p._flags & REACTIVE_ZOMBIE) p = p._parent;
2309
+ let t = p && (p._transition || (p._config & CONFIG_HELD_CHILDREN ? activeTransition : null));
2310
+ if (!t || (t = currentTransition(t))._done === true || t === verdict) return false;
2311
+ }
2312
+ // Fallback-caught async holds nothing. A collecting loading boundary
2313
+ // consumes the notification, so a reader under a fallback never registers —
2314
+ // but a reader registered while its boundary showed content stays
2315
+ // registered when the boundary's `on` later changes and it flips to the
2316
+ // fallback. The reader is behind the fallback now; if nothing outside the
2317
+ // boundary consumes the flight, the hold is over (A33, ruled 2026-09-12, #3375).
2318
+ for (let q = reporter._queue; q; q = q._parent)
2319
+ if (q._collectionType & STATUS_PENDING && !q._initialized) return false;
2320
+ // "Still derives from the source" is a question about THIS pass's reads:
2321
+ // the deps up to `_depsTail`. Past it lie the committed frame's — kept
2322
+ // linked by A30 until the commit trims them (a staged pass, an errored
2323
+ // one). Reading them here made a reporter whose pass had stopped reading
2324
+ // the source (a gate closed in the same flush as the write) look live, and
2325
+ // the hold it kept was the commit that would have trimmed the dep that
2326
+ // kept it (spec O3, same-flush form; fuzzer #3446 P1 cases 21/79). A
2327
+ // trimmed list ends at `_depsTail`, so the bound is free there; a pass
2328
+ // that read nothing has a null tail and derives from nothing.
2329
+ // The registration is trusted: a pending mark rides only this pass's links
2330
+ // (notifyStatus skips the kept tail), so a registered reporter read the
2331
+ // source, or threw on it.
1990
2332
  if (reporter._x?._pendingSources?.has(source)) return true;
1991
- for (let dep = reporter._deps; dep; dep = dep._nextDep) {
2333
+ const tail = reporter._depsTail;
2334
+ for (
2335
+ let dep = tail === null ? null : reporter._deps;
2336
+ dep;
2337
+ dep = dep === tail ? null : dep._nextDep
2338
+ ) {
1992
2339
  let current = dep._dep;
1993
2340
  while (current) {
1994
- if (current === source || current._firewall === source) return true;
2341
+ // Or through a memo pending on the flight (#3494): a stale reader
2342
+ // served a held memo's committed value never turned pending itself, so
2343
+ // its only trace of the flight is the memo between them — `copy` of
2344
+ // `details`. Judged dead, its transaction released `count=1` beside
2345
+ // the `Copy: 0` it displays. `_pendingSources` is transitive, so one
2346
+ // hop covers any depth.
2347
+ if (
2348
+ current === source ||
2349
+ current._firewall === source ||
2350
+ current._x?._pendingSources?.has(source)
2351
+ )
2352
+ return true;
1995
2353
  current = current._x?._parentSource;
1996
2354
  }
1997
2355
  }
@@ -2001,6 +2359,30 @@ function reporterBlocksSource(reporter, source) {
2001
2359
  reporter._x?._error.source === source
2002
2360
  );
2003
2361
  }
2362
+ /**
2363
+ * Does a live reporter of `transition` still observe `source` pending? Dead
2364
+ * reporters (disposed, behind a fallback, no longer reading the source) are
2365
+ * pruned as they are found, and the source's entry with them. Shared by the
2366
+ * settle verdict and the lane's hold check (`waitingTransition`): a live
2367
+ * action parks its transaction without a verdict, so this prune is the only
2368
+ * one an optimistic lane whose last async reader unmounted mid-action ever
2369
+ * gets — without it the lane held on the dead reporter's registration until
2370
+ * the flight it no longer observed landed (#3426).
2371
+ */
2372
+ function sourceObserved(transition, source, verdict) {
2373
+ const reporters = transition._asyncReporters.get(source);
2374
+ let kept = false;
2375
+ for (const reporter of reporters ?? []) {
2376
+ if (reporterBlocksSource(reporter, source, verdict)) return true;
2377
+ // A zombie the verdict passes over is kept, not pruned (#3463): moot for
2378
+ // this verdict, it still holds a lane's reveal while the transaction
2379
+ // stays parked on something else.
2380
+ if (verdict && reporter._flags & REACTIVE_ZOMBIE) kept = true;
2381
+ else reporters.delete(reporter);
2382
+ }
2383
+ if (!kept) transition._asyncReporters.delete(source);
2384
+ return false;
2385
+ }
2004
2386
  function transitionComplete(transition) {
2005
2387
  if (transition._done) return true;
2006
2388
  if (transition._actions.length) {
@@ -2009,17 +2391,22 @@ function transitionComplete(transition) {
2009
2391
  return false;
2010
2392
  }
2011
2393
  let done = true;
2012
- for (const [source, reporters] of transition._asyncReporters) {
2013
- let hasLive = false;
2014
- for (const reporter of reporters) {
2015
- if (reporterBlocksSource(reporter, source)) {
2016
- hasLive = true;
2017
- break;
2018
- }
2019
- reporters.delete(reporter);
2020
- }
2021
- if (!hasLive) transition._asyncReporters.delete(source);
2022
- else if (source._statusFlags & STATUS_PENDING && source._x?._error?.source === source) {
2394
+ for (const source of transition._asyncReporters.keys()) {
2395
+ // The source blocks while its OWN flight is up — the self entry in its
2396
+ // pending sources (added by notifyStatus's source path, with status;
2397
+ // retired by the landing and the supersede sweep, so it implies
2398
+ // STATUS_PENDING). `_error.source` is not that test: propagation from an
2399
+ // input that went pending later overwrites it with the input (#3375 — a
2400
+ // boundary-consumed load re-asked under a held derivation), and the
2401
+ // still-flying source read as settled, committing the writes it was
2402
+ // asked with ahead of its answer.
2403
+ // Not the self entry alone: a source whose own flight an upstream re-ask
2404
+ // superseded is still pending on that re-ask (#3462). Its reader cannot
2405
+ // render until the chain lands, and the landing folds this transaction in
2406
+ // (enterWaiting). Judged complete instead, a re-entry between the two (a
2407
+ // repeated write to a held signal) committed the held writes beside the
2408
+ // reader's stale frame.
2409
+ if (sourceObserved(transition, source, transition) && source._x?._pendingSources?.size) {
2023
2410
  done = false;
2024
2411
  break;
2025
2412
  }
@@ -2056,12 +2443,24 @@ function currentTransition(transition) {
2056
2443
  * the node, so a hold check must not assume it was recorded in the transaction
2057
2444
  * it happens to hold — lanes merge across transactions (#2912), and a merged
2058
2445
  * root's transaction knows nothing of the async its members' transactions
2059
- * observed (#3335). Null when nobody is waiting.
2446
+ * observed (#3335). Null when nobody is waiting — a registration whose every
2447
+ * reporter has since died is nobody (#3426).
2060
2448
  */
2061
2449
  function waitingTransition(source) {
2062
- for (const t of transitions) if (t._asyncReporters.has(source)) return t;
2450
+ for (const t of transitions) if (sourceObserved(t, source)) return t;
2063
2451
  return null;
2064
2452
  }
2453
+ /** A landing enters EVERY parked transaction still waiting on `source`, folding
2454
+ * them into the active one (A15: each reveal that discovered the flight
2455
+ * completes at its landing). The fold used to happen as the waiters' stamped
2456
+ * readers recomputed under the landing — recompute re-entering an effect's
2457
+ * stamp — which also folded in writes those readers merely shared a hole
2458
+ * with (#3407); effects no longer re-enter, so the landing folds explicitly.
2459
+ * Live iteration is safe: a merge deletes the outgoing (active) entry and
2460
+ * re-adds the visited one. */
2461
+ function enterWaiting(source) {
2462
+ for (const t of transitions) if (sourceObserved(t, source)) globalQueue.initTransition(t);
2463
+ }
2065
2464
  function runInTransition(transition, fn) {
2066
2465
  const prevTransition = activeTransition;
2067
2466
  try {
@@ -2279,6 +2678,19 @@ function disposeChildren(node, self = false, zombie) {
2279
2678
  // false, and notifies subscribers still watching the companion.
2280
2679
  const n = node;
2281
2680
  if (n._x?._pendingSignal || n._x?._latestValueComputed) GlobalQueue._snapCompanions(n);
2681
+ // A firewall's leaves have no lifecycle of their own, so a companion on
2682
+ // one of them outlives its source the same way (INV-9's rationale). The
2683
+ // firewall knows which leaves carry companions (CONFIG_CHILD_COMPANIONS,
2684
+ // #3038): snap them with it — the snap retires a shadow whose firewall is
2685
+ // disposed (spec O5).
2686
+ if (n._config & CONFIG_CHILD_COMPANIONS)
2687
+ n._x._companionChildren.forEach(GlobalQueue._snapCompanions);
2688
+ // A pending reader parked in a transaction may be the only thing holding
2689
+ // it (#3372): its death is a completion event the transaction must be
2690
+ // re-judged for, and nothing else re-enters a parked transaction.
2691
+ const t = n._transition;
2692
+ if (t && n._statusFlags & STATUS_PENDING && !wokenTransitions.includes(t))
2693
+ (wokenTransitions.push(t), schedule());
2282
2694
  }
2283
2695
  if (self && true) clearSignals(node);
2284
2696
  if (self && node._fn && node._x !== null) node._x._inFlight = null;
@@ -3002,6 +3414,13 @@ function handleAsync(el, result, setter) {
3002
3414
  // fired _flightTeardown. A future non-recompute registration path must
3003
3415
  // release it here before overwriting _inFlight.
3004
3416
  ext(el)._inFlight = result;
3417
+ // The run that asked this flight read every input without throwing: an
3418
+ // input still in flight was masked for it (an active override, A17), so
3419
+ // pending state those inputs propagated onto the node earlier does not
3420
+ // describe this answer. Drop it — the flight is the node's pending now.
3421
+ // The landing retires only the flight's own entry (landStatus, #3373), so
3422
+ // an entry that survived here would hold the node past its own answer.
3423
+ el._x._pendingSources = undefined;
3005
3424
  // Provenance of the question this flight asks (#3331): the action whose
3006
3425
  // window is registering it, or the flight whose landing is. Its landings
3007
3426
  // propagate under it (asyncWrite) so an override downstream can tell a
@@ -3032,7 +3451,12 @@ function handleAsync(el, result, setter) {
3032
3451
  // waiting on this flight into it at the landing — a reveal that
3033
3452
  // discovered the flight (#3305) would then wait on the owner's action
3034
3453
  // instead of on the flight (#3334). Enter the waiter: the transaction
3035
- // whose blocker this landing clears.
3454
+ // whose blocker this landing clears. Then every transaction waiting on
3455
+ // the flight folds in (enterWaiting): a reveal that discovered it
3456
+ // completes at its landing (A15) — a stampless node's fresh batch
3457
+ // included (its flight started under a batch that committed beneath it,
3458
+ // #3305). The fold used to happen as each stamped reader recomputed,
3459
+ // which effects no longer do (#3407).
3036
3460
  if (el._x?._optimisticLane) transition = waitingTransition(el) ?? transition;
3037
3461
  if (
3038
3462
  transition &&
@@ -3045,6 +3469,7 @@ function handleAsync(el, result, setter) {
3045
3469
  return;
3046
3470
  }
3047
3471
  globalQueue.initTransition(transition);
3472
+ enterWaiting(el);
3048
3473
  };
3049
3474
  const handleError = error => {
3050
3475
  if (el._x?._inFlight !== result) return;
@@ -3107,8 +3532,7 @@ function handleAsync(el, result, setter) {
3107
3532
  // old value as pending, a one-frame pulse to direct observers (#3178).
3108
3533
  // A truthy capture implies `_x` exists, so the restore writes it directly.
3109
3534
  const wasReask = el._x?._reask;
3110
- trimStaleDeps(el);
3111
- clearStatus(el);
3535
+ landStatus(el);
3112
3536
  if (wasReask) el._x._reask = true;
3113
3537
  const lane = resolveLane(el);
3114
3538
  if (lane) lane._pendingAsync.delete(el);
@@ -3123,8 +3547,15 @@ function handleAsync(el, result, setter) {
3123
3547
  handleError(error);
3124
3548
  return;
3125
3549
  }
3126
- if (wasUninitialized) clearStatus(el, true);
3127
- } else if (el._x?._overrideValue !== undefined) {
3550
+ if (wasUninitialized) landStatus(el, true);
3551
+ } else if (
3552
+ el._x?._overrideValue !== undefined &&
3553
+ !(lane && el._config & CONFIG_DERIVED_OVERRIDE)
3554
+ ) {
3555
+ // A derived override's landing UNDER its lane is the lane's own work
3556
+ // (the branch below); demoted — its source superseded (A18) — the
3557
+ // landing is the truth the correction asked for, and holds and
3558
+ // supersedes here like the sync twin (recompute). Otherwise:
3128
3559
  // Optimistic node — resting OR covered by an active override — holds
3129
3560
  // through the shared pending-node path, exactly like a plain async memo,
3130
3561
  // so the commit clears STATUS_UNINITIALIZED (#2806) and elevation to
@@ -3162,11 +3593,15 @@ function handleAsync(el, result, setter) {
3162
3593
  } else if (lane) {
3163
3594
  // Route through lane's effect queue for independent flushing
3164
3595
  const isEffect = el._type;
3165
- const prevValue = el._value;
3596
+ const prevValue = hasActiveOverride(el) ? unwrapOverride(el._x._overrideValue) : el._value;
3166
3597
  const equals = el._equals;
3167
3598
  try {
3168
3599
  if ((!isEffect && wasUninitialized) || !equals || !equals(value, prevValue)) {
3169
- el._value = value;
3600
+ // Lanes stage (#3479): a memo's landing under its lane is a derived
3601
+ // override, as its sync pass's result is (recompute) — `_value`
3602
+ // stays the committed truth for readers off the lane.
3603
+ if (isEffect) el._value = value;
3604
+ else GlobalQueue._laneOverride(el, value, lane);
3170
3605
  el._time = clock;
3171
3606
  // The latest() shadow write gives latest() effects independent lanes; the
3172
3607
  // _pendingSignal update is a no-op repeat of the clearStatus() call above
@@ -3212,6 +3647,15 @@ function handleAsync(el, result, setter) {
3212
3647
  if (el._pendingValue === NOT_PENDING) {
3213
3648
  el._loading = false;
3214
3649
  if (wasReask) el._x._reask = false;
3650
+ // The landing published: the dependency tail the flight's pass left
3651
+ // linked goes now (A30, #3410). A transition-held landing has not
3652
+ // replaced the committed frame — the committed value still derives
3653
+ // from the previous pass's inputs, and a mainline write to one of them
3654
+ // must reach this node and join its hold (its stamp) instead of
3655
+ // publishing beside the stale derivation (#3461: `b() ? b() : a()`
3656
+ // held on `b` dropped `a` at its landing, and `A: 1` then committed
3657
+ // beside `Selected: 0`). `commitPendingNode` trims a held landing.
3658
+ trimStaleDeps(el);
3215
3659
  }
3216
3660
  settlePendingSource(el);
3217
3661
  schedule();
@@ -3466,6 +3910,41 @@ function clearStatus(el, clearUninitialized = false) {
3466
3910
  const notify = statusNotifierOf(el);
3467
3911
  if (notify) notify.call(el);
3468
3912
  }
3913
+ /**
3914
+ * Status clear for a flight LANDING (asyncWrite). A landing answers the
3915
+ * node's OWN question — it retires the node's self entry, not the pending
3916
+ * state its sources propagated onto it. An input re-asked while this flight
3917
+ * was up (a second write to the signal feeding `a` while `b`'s first flight
3918
+ * is in the air, #3373) marks `b` pending on `a` by propagation, with `b`'s
3919
+ * flight still current: nothing superseded it (the re-ask only changed `a`'s
3920
+ * status, not yet its value), so the landing arrives, and a full clear made
3921
+ * `b` answer with the stale value — the transaction's reporter for `a` found
3922
+ * nothing pending below it and committed the newer signal beside the older
3923
+ * derived value (`2 / 1`); `isPending(b)` read false for the gap (#3376).
3924
+ * With another source still pending the node stays derivatively pending on
3925
+ * it; the landed value is written below (the staged answer is still the
3926
+ * answer for the inputs it was asked with) and the input's own settle
3927
+ * releases it, or its value change recomputes the node into a fresh flight.
3928
+ * `_blocked` clears like a full clear: a landing that passed the `_inFlight`
3929
+ * guard was not superseded by a re-run (recompute nulls `_inFlight` first),
3930
+ * so the flag is the flight's own registration throw — the input settling
3931
+ * unchanged must not re-run the node (an extra flight for the same inputs).
3932
+ * The node is already STATUS_PENDING in that branch (only notifyStatus fills
3933
+ * the set, with status; a loading-window park cannot coexist with a live
3934
+ * flight since registration drops the set), so the flags only change when
3935
+ * the first landing retires UNINITIALIZED. `_error` must move off self: a
3936
+ * reader thrown NotReady(self) would park on a retired entry. Companions
3937
+ * keep their verdict (pending before and after; the write re-syncs them).
3938
+ */
3939
+ function landStatus(el, clearUninitialized = false) {
3940
+ const sources = el._x?._pendingSources;
3941
+ // (The full clear below drops the set whether or not self was retired first.)
3942
+ if (sources && (sources.delete(el), sources.size)) {
3943
+ el._x._blocked = false;
3944
+ if (clearUninitialized) el._statusFlags = STATUS_PENDING;
3945
+ setPendingError(el, sources.values().next().value);
3946
+ } else clearStatus(el, clearUninitialized);
3947
+ }
3469
3948
  function notifyStatus(el, status, error, blockStatus, lane) {
3470
3949
  // Wrap regular errors to track source node
3471
3950
  if (
@@ -3477,10 +3956,24 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3477
3956
  const pendingSource =
3478
3957
  status === STATUS_PENDING && error instanceof NotReadyError ? error.source : undefined;
3479
3958
  const isSource = pendingSource === el;
3959
+ // An optimistic node (a WRITTEN override slot) pending derivatively is a
3960
+ // boundary: its override is the answer, pending stops here (A17). A
3961
+ // derived override (#3479) is a previous speculative answer on a plain
3962
+ // member — pending flows through it as through any memo.
3480
3963
  const isOptimisticBoundary =
3481
- status === STATUS_PENDING && el._x?._overrideValue !== undefined && !isSource;
3964
+ status === STATUS_PENDING &&
3965
+ el._x?._overrideValue !== undefined &&
3966
+ !(el._config & CONFIG_DERIVED_OVERRIDE) &&
3967
+ !isSource;
3482
3968
  const startsBlocking = isOptimisticBoundary && hasActiveOverride(el);
3483
3969
  if (!blockStatus) {
3970
+ // Lane before companions: the companion pokes below may create the
3971
+ // node's pending-signal lane, whose parent is read from the node's lane
3972
+ // at creation. Assigned after them (as it was), a node made pending by
3973
+ // propagation before it rode the lane got a parentless companion lane,
3974
+ // and the isPending reader that also depends on the node merged it into
3975
+ // the held lane — the verdict then waited on the async it reports (#3379).
3976
+ if (lane) assignOrMergeLane(el, lane);
3484
3977
  if (status === STATUS_PENDING && pendingSource) {
3485
3978
  addPendingSource(el, pendingSource);
3486
3979
  // A fresh flight from a settled state starts with its inputs unpublished
@@ -3505,9 +3998,6 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3505
3998
  )
3506
3999
  GlobalQueue._updateChildCompanions(el);
3507
4000
  }
3508
- if (lane && !blockStatus) {
3509
- assignOrMergeLane(el, lane);
3510
- }
3511
4001
  const downstreamBlockStatus = blockStatus || startsBlocking;
3512
4002
  const downstreamLane = blockStatus || isOptimisticBoundary ? undefined : lane;
3513
4003
  const elNotify = statusNotifierOf(el);
@@ -3524,6 +4014,30 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3524
4014
  }
3525
4015
  forEachDependent(el, (sub, link) => {
3526
4016
  sub._time = clock;
4017
+ // A pending mark on a kept-tail link re-derives the subscriber instead of
4018
+ // marking it (A30, #3494 review; fuzzer latest-1 #2141; #3519 review).
4019
+ // Past `_depsTail` lie the committed frame's deps, kept by A30 because
4020
+ // that frame still derives from them while the pass that dropped them is
4021
+ // held (staged, or unchanged and parked). A source going pending there is
4022
+ // a question for the node's NEXT pass, not a fact about its current one:
4023
+ // marked, the node was registered as the flight's reporter and its holder
4024
+ // entangled with the flight (the A15 arm below) on a dep the held frame
4025
+ // never reads — an orphaned fetch held the truth (a hide joined to a
4026
+ // parked action, A34), and a manual flight nobody awaited held a gated
4027
+ // reader hidden forever (fuzzer branches-1 #1105). Skipped, the committed
4028
+ // frame published stale beside its new inputs (`query=1` beside a
4029
+ // `selected` derived from `remote(0)`). Re-derived, the pass decides: it
4030
+ // reads the dep and registers through its own read, or reads a held input
4031
+ // and enters that transaction (A29), or reads neither and is done. Clears
4032
+ // and errors still ride every link. A link inside the prefix carries the
4033
+ // pass's generation (`link()`), so the test is O(1); mid-pass the prefix
4034
+ // is what the pass has read so far, and the heap refuses a recomputing
4035
+ // node — a dep it has yet to reach registers through its own read.
4036
+ if (status === STATUS_PENDING && link._gen !== sub._depGen) {
4037
+ enqueueSub(sub);
4038
+ schedule();
4039
+ return;
4040
+ }
3527
4041
  if (
3528
4042
  (status === STATUS_PENDING &&
3529
4043
  pendingSource &&
@@ -3540,7 +4054,26 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3540
4054
  schedule();
3541
4055
  return;
3542
4056
  }
3543
- if (!downstreamBlockStatus && !sub._transition) queuePendingNode(sub);
4057
+ // A memo another live transaction HOLDS — pending on its work, or
4058
+ // staged by it — made pending by THIS flight cannot reveal before the
4059
+ // flight lands: the two settle as one unit (A15, a shared derivation of
4060
+ // both — #3443). Propagation marks the held memo without recomputing it
4061
+ // (its inputs' values are unchanged), so this is the one moment the
4062
+ // entanglement is known; the memo's stamped re-entry at its next pass
4063
+ // came too late — the holder's own flight landed first and revealed the
4064
+ // inputs beside the stale sum. The stamp alone decides nothing (#3334):
4065
+ // a node the transaction once queued but holds nothing of — a switch's
4066
+ // shared output whose first flight the second write superseded — must
4067
+ // not drag the older flight into the newer reveal. Effects entangle
4068
+ // nothing (A15 shared-hole corollary): their reader registers with the
4069
+ // flight's transaction at queue notification.
4070
+ if (!downstreamBlockStatus)
4071
+ sub._transition
4072
+ ? pendingSource &&
4073
+ !sub._type &&
4074
+ (sub._statusFlags & STATUS_PENDING || sub._pendingValue !== NOT_PENDING) &&
4075
+ globalQueue.initTransition(sub._transition)
4076
+ : queuePendingNode(sub);
3544
4077
  notifyStatus(sub, status, error, downstreamBlockStatus, downstreamLane);
3545
4078
  }
3546
4079
  });
@@ -3570,6 +4103,10 @@ const REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE =
3570
4103
  const REACTIVE_WRITE_IN_OWNED_SCOPE_REFRESH_MESSAGE =
3571
4104
  "[REACTIVE_WRITE_IN_OWNED_SCOPE] Calling refresh() inside an owned scope (component, computation) is not allowed. " +
3572
4105
  "Move the invalidation outside pure computation.";
4106
+ const ASYNC_STORE_SETTER_MESSAGE =
4107
+ "[ASYNC_STORE_SETTER] Store setter callback returned a Promise. A store setter is a synchronous transaction: " +
4108
+ "the draft closes when the callback returns, so writes after an `await` are lost. " +
4109
+ "Move the await into an action() and call the setter from there.";
3573
4110
  let tracking = false;
3574
4111
  /** @internal verdict-module glue */
3575
4112
  function setPendingCheckActive(v) {
@@ -3633,10 +4170,15 @@ function releaseSubtree(owner) {
3633
4170
  function clearSnapshots() {
3634
4171
  if (snapshotSources) {
3635
4172
  for (const source of snapshotSources) {
3636
- delete source._x?._snapshotValue;
4173
+ // The extension is a fixed-shape object with `_snapshotValue`
4174
+ // pre-initialized to undefined (see ext()), and every reader tests
4175
+ // `!== undefined` — assign, don't `delete`: deleting a field pushes the
4176
+ // object to dictionary mode for every later read of every field.
4177
+ const x = source._x;
4178
+ if (x != null) x._snapshotValue = undefined;
3637
4179
  // StoreNode targets share one pre-initialized hidden class (see
3638
- // createStoreProxy) — assign undefined instead of deleting, and only
3639
- // when present so signal-node sources don't grow the field.
4180
+ // createStoreProxy) — same rule, and only when present so signal-node
4181
+ // sources don't grow the field.
3640
4182
  if (source[STORE_SNAPSHOT_PROPS] !== undefined) source[STORE_SNAPSHOT_PROPS] = undefined;
3641
4183
  }
3642
4184
  snapshotSources = null;
@@ -3653,7 +4195,11 @@ function recompute(el, create = false) {
3653
4195
  let devChanged = false;
3654
4196
  if (attrHooks !== null) attrHooks.recomputeStart(el, create);
3655
4197
  if (!create) {
3656
- if (el._transition && (!isEffect || activeTransition) && activeTransition !== el._transition)
4198
+ // A stamped memo re-enters its hold: its value is that transaction's work.
4199
+ // An effect's pass belongs to whatever dirtied it (A15 corollary: effects
4200
+ // don't entangle parallel transactions); it joins its stamp only when the
4201
+ // pass observes the held flight — queue notification (#3407).
4202
+ if (el._transition && !isEffect && activeTransition !== el._transition)
3657
4203
  globalQueue.initTransition(el._transition);
3658
4204
  deleteFromHeap(el, queueFor(el));
3659
4205
  if (el._x !== null) {
@@ -3665,8 +4211,14 @@ function recompute(el, create = false) {
3665
4211
  // work that replaced it. Idempotent with the cleanup-channel close.
3666
4212
  releaseFlightTeardown(el);
3667
4213
  }
3668
- // Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring
3669
- if (el._transition || isEffect === EFFECT_TRACKED) disposeChildren(el);
4214
+ // Tracked effects run after finalizePureQueue, so dispose immediately
4215
+ // instead of deferring. Children built by an uncommitted recompute
4216
+ // (CONFIG_HELD_CHILDREN) die immediately too: no frame ever showed them.
4217
+ // Everything else is the committed frame's and is deferred as zombies
4218
+ // until this node's commit — a transaction-owned node included (#3404):
4219
+ // a parked node's children predate the hold, and tearing them down when
4220
+ // the source lands ran cleanups before the transaction's atomic reveal.
4221
+ if (isEffect === EFFECT_TRACKED || el._config & CONFIG_HELD_CHILDREN) disposeChildren(el);
3670
4222
  else if (el._firstChild !== null || el._disposal !== null) {
3671
4223
  markDisposal(el);
3672
4224
  const x = ext(el);
@@ -3679,8 +4231,11 @@ function recompute(el, create = false) {
3679
4231
  } else clearSignals(el);
3680
4232
  }
3681
4233
  let isOptimisticDirty = !!(el._flags & REACTIVE_OPTIMISTIC_DIRTY);
4234
+ // A derived override (lanes stage, #3479) is override-covered like a written
4235
+ // one: a plain pass over it — its source superseded (A18) — stages the truth
4236
+ // and supersedes the override through the sync twin below.
3682
4237
  const hasOverride =
3683
- (el._config & CONFIG_OPTIMISTIC) !== 0 &&
4238
+ (el._config & (CONFIG_OPTIMISTIC | CONFIG_DERIVED_OVERRIDE)) !== 0 &&
3684
4239
  el._x?._overrideValue !== NOT_PENDING &&
3685
4240
  el._x?._overrideValue !== undefined;
3686
4241
  const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
@@ -3688,8 +4243,8 @@ function recompute(el, create = false) {
3688
4243
  // A conditional can drop its pending source and recover to an unchanged
3689
4244
  // value, leaving blocked dependents outside that source’s settle walk.
3690
4245
  const outgoingError = el._statusFlags & STATUS_ERROR ? el._x?._error : undefined;
3691
- const outgoingPendingSources =
3692
- el._statusFlags & STATUS_PENDING ? el._x?._pendingSources : undefined;
4246
+ const wasPending = (el._statusFlags & STATUS_PENDING) !== 0;
4247
+ const outgoingPendingSources = wasPending ? el._x?._pendingSources : undefined;
3693
4248
  // Pending SOURCE-hood, captured before the compute clears status: a node
3694
4249
  // whose own flight parked dependents self-registers in _pendingSources
3695
4250
  // (notifyStatus, isSource). If this recompute supersedes that flight and
@@ -3705,6 +4260,11 @@ function recompute(el, create = false) {
3705
4260
  // commits (commitPendingNode) — a closed window plus a held value reads as
3706
4261
  // a pending frame to live observers of the verdict (#2990).
3707
4262
  const wasLoading = el._loading;
4263
+ // Creation-time A29 (see enterStagedRead): a pass outside a flush that is
4264
+ // served a live transaction's staged value records the transaction here
4265
+ // and is staged INTO it below — "born held" — instead of committing.
4266
+ const prevStagedEntry = stagedEntry;
4267
+ stagedEntry = null;
3708
4268
  const oldcontext = context;
3709
4269
  context = el;
3710
4270
  el._depsTail = null;
@@ -3730,6 +4290,17 @@ function recompute(el, create = false) {
3730
4290
  // covers creation-time computes and flushes that run inside the window.
3731
4291
  const prevLatestRead = latestReadActive;
3732
4292
  latestReadActive = false;
4293
+ // A memo computes under its OWN lane posture, never the puller's (A31,
4294
+ // #3442): its value is one shared slot every reader sees, so a pull from a
4295
+ // lane-carrying reader (a probe effect on its companion lane pulling a sync
4296
+ // memo) must not run it with that lane's read carve-outs — under a lane, a
4297
+ // pending node on no lane serves its committed value instead of throwing,
4298
+ // and the memo then published a stale "settled" value, dropped its
4299
+ // pending status, and stopped holding its transaction. The branches below
4300
+ // re-establish the posture the memo itself owns (OPT-dirty, or adopted
4301
+ // through its deps). Effects keep the ambient lane: their runs are the
4302
+ // lane's own view.
4303
+ if (!isEffect) currentOptimisticLane = null;
3733
4304
  // Lane posture lives with the engine: OPTIMISTIC_DIRTY is only ever set by
3734
4305
  // engine-driven paths, and _optimisticNodes is only pushed by
3735
4306
  // _optimisticWrite, so the hook is installed whenever either gate holds.
@@ -3740,6 +4311,21 @@ function recompute(el, create = false) {
3740
4311
  // latest()/isPending() pull stages instead of direct-committing (#3009).
3741
4312
  // The predicate lives with the engine (recomputeLane).
3742
4313
  else if (lane === false) isOptimisticDirty = false;
4314
+ } else if (el._config & CONFIG_DERIVED_OVERRIDE) {
4315
+ // Lanes stage (#3479): a pass over a live lane member carrying a derived
4316
+ // override is the lane's pass whatever channel dirtied it (a boundary
4317
+ // reset, an unrelated sync write) — its inputs serve the lane's view, so
4318
+ // its result is the lane's and belongs in the override slot. Run plain,
4319
+ // A18's sync twin below read that re-derived lane view as a differing
4320
+ // truth (a fresh array), superseded the override and demoted the lane;
4321
+ // the lane's next pass then dropped the staged "truth" and left the node
4322
+ // flagged superseded with nothing to serve (fuzzer latest-1 #2481). A
4323
+ // demoted node resolves no lane and stays plain: its pass IS the truth.
4324
+ const lane = GlobalQueue._recomputeLane(el, true);
4325
+ if (lane) {
4326
+ isOptimisticDirty = true;
4327
+ currentOptimisticLane = lane;
4328
+ }
3743
4329
  } else if (activeTransition && !create && activeTransition._optimisticNodes.length) {
3744
4330
  // Lane adoption: parent-deeper-than-owned-child can run before its OPT-dirty
3745
4331
  // child propagates. Walk deps once and inherit the OPT lane so this node
@@ -3787,7 +4373,9 @@ function recompute(el, create = false) {
3787
4373
  // its body gates on is either _statusFlags or lives in the cold
3788
4374
  // extension — no extension, no status to clear. (_x from an unrelated
3789
4375
  // installer just makes clearStatus a cheap re-verified no-op.)
3790
- if (el._statusFlags !== 0 || el._x !== null) clearStatus(el, create);
4376
+ // A node born held keeps STATUS_UNINITIALIZED until its transaction
4377
+ // commits: it has no committed value, and read() holds its readers.
4378
+ if (el._statusFlags !== 0 || el._x !== null) clearStatus(el, create && stagedEntry === null);
3791
4379
  // _optimisticLane is only ever assigned by engine paths (CONFIG_HAS_LANE
3792
4380
  // is their sticky presence mark).
3793
4381
  if (el._config & CONFIG_HAS_LANE && el._x?._optimisticLane) GlobalQueue._laneAsyncSettled(el);
@@ -3821,6 +4409,18 @@ function recompute(el, create = false) {
3821
4409
  // The replacement source is fully propagated now. If no new flight
3822
4410
  // re-owned self, retire the superseded flight and its dependent copies.
3823
4411
  if (notReady && wasPendingSource && !el._x?._inFlight) settlePendingSource(el);
4412
+ // A re-park drops what the earlier pass carried (#3456): a source this
4413
+ // pass no longer reaches — its branch switched, or a fresh flight
4414
+ // replaced the inputs' pending with its own — stays copied onto
4415
+ // dependents that reached it only through here, and its landing walk
4416
+ // stops at this node (nothing left to retire) before it finds them. A
4417
+ // dependent then waits forever on a flight it has no path to. The
4418
+ // re-park twin of the unchanged-value recovery sweep below; dependents
4419
+ // with another path keep the source (retryReaches).
4420
+ if (notReady && outgoingPendingSources)
4421
+ for (const source of outgoingPendingSources)
4422
+ if (source !== el && !el._x?._pendingSources?.has(source))
4423
+ settlePendingSource(el, source);
3824
4424
  if (reaskChanged) GlobalQueue._repollVerdicts(el);
3825
4425
  }
3826
4426
  } finally {
@@ -3837,18 +4437,26 @@ function recompute(el, create = false) {
3837
4437
  el._flags = REACTIVE_NONE | (create ? el._flags & REACTIVE_SNAPSHOT_STALE : 0);
3838
4438
  context = oldcontext;
3839
4439
  }
4440
+ // The cast re-widens: TS narrowed `stagedEntry` to `null` at the reset
4441
+ // above and does not invalidate that across the compute call that
4442
+ // `enterStagedRead` runs under. No emitted code.
4443
+ const bornHeld = stagedEntry;
4444
+ stagedEntry = prevStagedEntry;
3840
4445
  if (!el._x?._error) {
3841
- trimStaleDeps(el);
3842
- // Observe-tier fan-in (HUGE_FAN_IN): with the stale tail trimmed, the dep
3843
- // list IS this pass's distinct sources count it here rather than per
3844
- // link. A begin/end bracket around the pass plus a per-link increment
3845
- // measured -5.8% on createRenderEffects:create1to1 (CodSpeed, dev tier)
3846
- // and cost several points of the shape wins elsewhere; this walk is a
3847
- // fraction of the reads that built the list and keeps no module state,
3848
- // so nested pulls need no save/restore.
4446
+ // Observe-tier fan-in (HUGE_FAN_IN): the validated prefix [_deps.._depsTail]
4447
+ // IS this pass's distinct sources count it here rather than per link. A
4448
+ // begin/end bracket around the pass plus a per-link increment measured
4449
+ // -5.8% on createRenderEffects:create1to1 (CodSpeed, dev tier) and cost
4450
+ // several points of the shape wins elsewhere; this walk is a fraction of
4451
+ // the reads that built the list and keeps no module state, so nested
4452
+ // pulls need no save/restore. (The stale tail is trimmed at the end of
4453
+ // this pass, or at its commit — see recompute's tail.)
3849
4454
  {
3850
4455
  let fanIn = 0;
3851
- for (let d = el._deps; d !== null; d = d._nextDep) fanIn++;
4456
+ for (let d = el._deps; d !== null; d = d._nextDep) {
4457
+ fanIn++;
4458
+ if (d === el._depsTail) break;
4459
+ }
3852
4460
  if (fanIn >= GRAPH_SIZE_WARN_AT) noteFanIn(el, fanIn);
3853
4461
  }
3854
4462
  // INV-11 (#3330): the equality gate compares against the slot this run
@@ -3921,7 +4529,7 @@ function recompute(el, create = false) {
3921
4529
  else if (valueChanged) {
3922
4530
  const prevVisible = hasOverride ? el._x?._overrideValue : undefined;
3923
4531
  if (
3924
- create ||
4532
+ (create && bornHeld === null) ||
3925
4533
  // Plain sync flush (no transition on either side) commits effect
3926
4534
  // values directly — the pending round-trip (queuePendingNode +
3927
4535
  // commitPendingNodes) exists to sequence transition reveals, and
@@ -3931,6 +4539,7 @@ function recompute(el, create = false) {
3931
4539
  // not the stashed queues, so a staged value would hand the immediate
3932
4540
  // apply stale state — see CONFIG_DIRECT_COMMIT.
3933
4541
  (isEffect &&
4542
+ bornHeld === null &&
3934
4543
  (activeTransition !== el._transition ||
3935
4544
  activeTransition === null ||
3936
4545
  el._config & CONFIG_DIRECT_COMMIT)) ||
@@ -3943,26 +4552,50 @@ function recompute(el, create = false) {
3943
4552
  // round-trip is that separation; it cannot be skipped on any path a
3944
4553
  // pull can reach.
3945
4554
  ) {
3946
- el._value = value;
3947
- // Lane-propagated correction: upstream data is fresh, correct the
3948
- // override unconditionally. The direct _value commit is the lane's
3949
- // own reveal schedule; drop any superseded older hold so its queued
3950
- // commit can't clobber the fresh value.
3951
- if (hasOverride && isOptimisticDirty) {
3952
- ext(el)._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
3953
- el._pendingValue = NOT_PENDING;
3954
- }
4555
+ // Lanes stage (#3479): a lane pass on a memo publishes its speculative
4556
+ // result as an OVERRIDE `_value` stays the committed truth, so a
4557
+ // reader off the lane (A17's committed view, #3460) sees a whole
4558
+ // committed frame: the source's shadow and its derivations together,
4559
+ // never a committed shadow beside a speculative memo. The lane's own
4560
+ // readers and untracked reads see the override (A17); the revert
4561
+ // drops it and re-derives (resolveOptimisticNodes). Effects keep the
4562
+ // direct commit — their `_value` is a run result the lane's queues
4563
+ // already sequence. Either way the lane pass drops any superseded
4564
+ // older hold so its queued commit can't clobber the fresh frame: a
4565
+ // hold staged on an earlier, lane-free pass of the SAME transaction
4566
+ // is superseded — left in place, the commit published the older
4567
+ // frame over the fresh one (#3377). A reversion pass (OPT-dirty with
4568
+ // no lane — the override dropped) commits directly: it IS the truth.
4569
+ if (isOptimisticDirty && !isEffect && currentOptimisticLane !== null)
4570
+ GlobalQueue._laneOverride(el, value, currentOptimisticLane);
4571
+ else el._value = value;
4572
+ if (isOptimisticDirty) el._pendingValue = NOT_PENDING;
3955
4573
  } else {
3956
4574
  el._pendingValue = value;
4575
+ if (bornHeld !== null) {
4576
+ // Born held: the pass ran from mainline and derived from this
4577
+ // transaction's staged world (enterStagedRead). Its value is the
4578
+ // transaction's — staged into it directly, stamped, and committed
4579
+ // with it; mainline's batch never sees it. A born-held effect
4580
+ // skips its synchronous first run (effect()) and is replayed by
4581
+ // the commit like a stale reader that showed the committed frame.
4582
+ el._transition = bornHeld;
4583
+ bornHeld._pendingNodes.push(el);
4584
+ if (isEffect) bornHeld._gatedSubs.add(el);
4585
+ }
3957
4586
  // A window landing that gets held re-opens the window until the hold
3958
4587
  // commits — the verdict's held-value branch is window-gated (#2990).
3959
4588
  if (wasLoading) el._loading = true;
3960
- // Transition-held sync recompute is a write path like setSignal/asyncWrite,
4589
+ // A staged sync recompute is a write path like setSignal/asyncWrite,
3961
4590
  // so sync derivations of held sources stay visible to isPending()/latest()
3962
4591
  // (#2831). Both companion writes are transition-scoped (optimistic) and
3963
- // auto-revert/re-derive at commit. Skipped for plain flushes where the
3964
- // pending value commits before effects run.
3965
- if ((activeTransition || el._transition) && GlobalQueue._syncCompanions !== null)
4592
+ // auto-revert/re-derive at commit. Not gated on an active transition:
4593
+ // a plain flush can still become a hold after this recompute — an
4594
+ // async memo downstream pends and the batch is adopted into a
4595
+ // transaction (scheduler.enterTransition) — and nothing re-derives
4596
+ // the companion at adoption, so a memo held that way read
4597
+ // isPending() false while its held source read true (#3413).
4598
+ if (el._config & CONFIG_HAS_COMPANIONS && GlobalQueue._syncCompanions !== null)
3966
4599
  GlobalQueue._syncCompanions(el, value);
3967
4600
  }
3968
4601
  // insertSubs only walks _subs (no scheduling of its own), so a
@@ -4024,6 +4657,54 @@ function recompute(el, create = false) {
4024
4657
  if (wasPendingSource && !(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)))
4025
4658
  settlePendingSource(el);
4026
4659
  }
4660
+ // A REPORTER whose pass stopped reading a source it reported on (a gate
4661
+ // closed) stops counting for the transaction waiting on it (A15 / #3426:
4662
+ // the hold lasts while a live reporter observes the flight). Nothing else
4663
+ // re-judges a parked transaction (see wokenTransitions; the disposal
4664
+ // (#3372) and boundary (#3375) twins of this site), so the writes it held
4665
+ // stayed staged for as long as the flight stayed up — forever, for one
4666
+ // that never lands (fuzzer #3446 P1, spec O3). The event is "this pass
4667
+ // dropped a dep" — deps past `_depsTail` (trimmed below, or kept by A30
4668
+ // for a staged pass), or a pass that read nothing — not "recovered from
4669
+ // pending": a reporter registered by the stale-reader carve-out
4670
+ // (heldFromStale, an INITIALIZED source refetching) displays the committed
4671
+ // value and is never pending (fuzzer case 79). Every parked transaction,
4672
+ // not the reporter's stamp: the transaction waiting on it registered it
4673
+ // without stamping it. One idle pass per parked transaction; done ones
4674
+ // return at re-entry. Effects only — reporters register from render-effect
4675
+ // notification (INV-3).
4676
+ // (`_depsTail` was reset at the top of the pass; TS keeps that narrowing.)
4677
+ const tail = el._depsTail;
4678
+ if (
4679
+ isEffect &&
4680
+ ((wasPending && !(el._statusFlags & STATUS_PENDING)) ||
4681
+ (tail === null ? el._deps !== null : tail._nextDep !== null))
4682
+ )
4683
+ wakeParked();
4684
+ // Dependencies are the committed frame's until it is replaced (A30, #3410; the
4685
+ // deps twin of the held children above): a pass that staged its value
4686
+ // leaves the previous pass's tail linked for `commitPendingNode` to trim,
4687
+ // so a write to a dependency the committed value still derives from
4688
+ // reaches this node — and joins its hold if the stage is transaction-held
4689
+ // by then (a plain flush decides nothing here: the transaction that holds
4690
+ // the pass may open later in the same flush). A pass that published
4691
+ // directly (a first pass, a lane's own reveal) trims now. An errored pass
4692
+ // (a throw, NotReady included, or a comparator throw above) keeps its full
4693
+ // list as before — `_depsTail` marks where it stopped — and the commit
4694
+ // skips it by the same `_error`. An effect's frame is the run its value is
4695
+ // applied by, not the value slot (#3438): a direct-committed pass that
4696
+ // still owes a run (`_modified`) has not replaced what the last run
4697
+ // published — the same flush may stash that run into a transaction it
4698
+ // opens later — so its tail waits for `runEffect` to trim once the run
4699
+ // applies. A pass that changed nothing replaced nothing either (#3469): the
4700
+ // same flush may park with its inputs held, and the committed frame still
4701
+ // derives from the tail — its trim waits on the flush's verdict (heldTrims).
4702
+ // A tracked effect's pass IS its run (it bypasses the heap and runs after
4703
+ // the commit): the frame is replaced, trim now.
4704
+ if (!el._x?._error && el._pendingValue === NOT_PENDING && !(isEffect && el._modified)) {
4705
+ if (create || isOptimisticDirty || isEffect === EFFECT_TRACKED) trimStaleDeps(el);
4706
+ else if (el._depsTail?._nextDep ?? el._deps) heldTrims.push(el);
4707
+ }
4027
4708
  // Attribution hook: fired before the lane restore so `currentOptimisticLane`
4028
4709
  // still reflects THIS run's posture. The facts distinguish an overlay
4029
4710
  // recompute (optimistic lane, transition replay, transition-held commit)
@@ -4048,14 +4729,40 @@ function recompute(el, create = false) {
4048
4729
  // under the override (A17). Revert no longer commits anything, so an
4049
4730
  // unqueued covered hold would leak (INV-7) once the revert clears
4050
4731
  // _transition.
4051
- needsPendingCommit &&
4052
- (!create || el._statusFlags & STATUS_PENDING) &&
4053
- (!el._transition || hasOverride) &&
4054
- queuePendingNode(el);
4055
- el._transition &&
4056
- isEffect &&
4057
- activeTransition !== el._transition &&
4732
+ //
4733
+ // While a pass's result waits on a commit, its children (and `_disposal`)
4734
+ // wait with it, and a re-run may tear them down immediately
4735
+ // (CONFIG_HELD_CHILDREN, #3404). One exception: a transaction-owned effect
4736
+ // recomputed mainline (contested, #3322) published its value directly — a
4737
+ // `_pendingValue` left from an earlier held pass is that transaction's,
4738
+ // not this one's — so this pass's children are the frame's, and any
4739
+ // zombies deferred at the top are superseded on that same frame. Its
4740
+ // commit rides the transaction, not this flush: release them here rather
4741
+ // than let two generations render at once.
4742
+ let held =
4743
+ needsPendingCommit &&
4744
+ (!create || bornHeld !== null || (el._statusFlags & STATUS_PENDING) !== 0);
4745
+ if (held && (!el._transition || hasOverride)) queuePendingNode(el);
4746
+ else if (
4747
+ held &&
4748
+ activeTransition === null &&
4749
+ !(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED))
4750
+ ) {
4751
+ held = false;
4752
+ disposeChildren(el, false, true);
4753
+ }
4754
+ if (held) el._config |= CONFIG_HELD_CHILDREN;
4755
+ else el._config &= ~CONFIG_HELD_CHILDREN;
4756
+ // (A born-held pass IS the transaction's staged view — nothing to refresh.)
4757
+ if (el._transition && isEffect && activeTransition !== el._transition && bornHeld === null) {
4758
+ // The re-run refreshes the transaction's STAGED view (_pendingValue); the
4759
+ // value this pass published in _value belongs to the run that just
4760
+ // finished. Keep that ownership, or the effect phase parks a
4761
+ // mainline-computed value with the transaction (#3412).
4762
+ const owner = el._valueTransition;
4058
4763
  runInTransition(el._transition, () => recompute(el));
4764
+ el._valueTransition = owner;
4765
+ }
4059
4766
  // Missed-wake reschedule (see the finally above): values this pass read
4060
4767
  // before the nested commit are stale, so run again now that the heap will
4061
4768
  // accept the node. Equality gates stop same-value landings from cascading,
@@ -4159,6 +4866,7 @@ function ext(el) {
4159
4866
  _overrideValue: undefined,
4160
4867
  _overrideOwner: undefined,
4161
4868
  _overrideTime: 0,
4869
+ _flushedStaged: NOT_PENDING,
4162
4870
  _overrideStamp: 0,
4163
4871
  _optimisticLane: undefined,
4164
4872
  _pendingSignal: undefined,
@@ -4372,9 +5080,10 @@ function linkFirewallChild(firewall, s) {
4372
5080
  /** Release a firewall child the store no longer addresses (unobserved sweep
4373
5081
  * dropped it from its target's cache): unlink it from the chain so the
4374
5082
  * projection stops retaining it and its last value. The node keeps its own
4375
- * `_nextChild` so a walk that is mid-chain on it still terminates. Nodes in
4376
- * `_companionChildren` stay there companions are permanent by contract
4377
- * and snap through that set, not the chain. */
5083
+ * `_nextChild` so a walk that is mid-chain on it still terminates. It also
5084
+ * leaves `_companionChildren` (#3503): the companions themselves are
5085
+ * permanent on the node, but the node is unreachable through the store, so
5086
+ * the set would only retain it and its last value. */
4378
5087
  function unlinkFirewallChild(node) {
4379
5088
  const n = node;
4380
5089
  const fw = n._firewall;
@@ -4385,6 +5094,11 @@ function unlinkFirewallChild(node) {
4385
5094
  else if (fw._x._child === n) fw._x._child = next;
4386
5095
  if (next !== null) next._prevChild = prev;
4387
5096
  n._prevChild = null;
5097
+ // #3503: the companion set is the last projection-side reference to a
5098
+ // released leaf; leaving it there kept the leaf and its last value alive
5099
+ // for the projection's lifetime after every latest()/isPending() reader
5100
+ // was disposed.
5101
+ fw._x._companionChildren?.delete(n);
4388
5102
  }
4389
5103
  function slotSignal(v, equals, host, key, acc, firewall = null) {
4390
5104
  // Prod and observe boilerplates — see computed(). The store relabels the
@@ -4481,6 +5195,31 @@ function untrack(fn, strictReadLabel) {
4481
5195
  strictRead = prevStrictRead;
4482
5196
  }
4483
5197
  }
5198
+ /**
5199
+ * Set while runtime bookkeeping evaluates a user accessor inside another
5200
+ * node's pass (a loading boundary's `on` key, read from `notify` while the
5201
+ * node that went pending is still recomputing). `context` is that node, but
5202
+ * the read is nobody's: the value is compared, never derived from, so the
5203
+ * untracked-pending re-run link (`read`, `!tracking`) must not be recorded on
5204
+ * it — it made the key's source a dependency of an unrelated async memo
5205
+ * (#3528: `isPending(m2)` through a memo in `on()` linked the memo into `m2`,
5206
+ * a cycle that re-derived `m2` on every pending mark and never converged).
5207
+ */
5208
+ let spectating = false;
5209
+ /**
5210
+ * Evaluates `fn` untracked and without recording a dependency for the
5211
+ * untracked-pending re-run rule on the current `context`. For bookkeeping
5212
+ * reads made mid-propagation on behalf of no node — see `spectating`.
5213
+ */
5214
+ function spectate(fn) {
5215
+ const prev = spectating;
5216
+ spectating = true;
5217
+ try {
5218
+ return untrack(fn);
5219
+ } finally {
5220
+ spectating = prev;
5221
+ }
5222
+ }
4484
5223
  /**
4485
5224
  * Bring a computed to a readable state: lazy/disposed nodes are (re)computed;
4486
5225
  * an isPending() probe (`refresh`) additionally pulls the node fully up to
@@ -4557,15 +5296,262 @@ function installAuthoritativeRead() {
4557
5296
  * already use). An effect the transaction itself computed re-derives at its
4558
5297
  * commit on its own (parked run, or the contested re-derive, #3322) and is
4559
5298
  * not recorded — replaying it too would publish the frame twice.
5299
+ *
5300
+ * Flight twin (the pending-branch carve-out): the reader is served the
5301
+ * node's committed, pre-flight value and now observes that flight — A15:
5302
+ * async work observed by a reader settles as one unit with the writes that
5303
+ * asked it — so it joins the transaction's reporters for the node. The
5304
+ * reporter the transaction recorded when the flight started may be gone (a
5305
+ * keyed remount disposed it, #3374); a completion check that found no live
5306
+ * reporter committed the writes ahead of the answer, tearing the new
5307
+ * reader's frame (`Count: 1` beside `Details: 0`). Joins an entry the
5308
+ * transaction already holds; a flight nobody had observed yet has none (the
5309
+ * reader is its first observer — a conditional that just revealed it, #3458)
5310
+ * and is notified up the reader's own queue chain under that transaction,
5311
+ * the one sanctioned registration site (INV-3): a collecting boundary above
5312
+ * the reader consumes it as it would any pending, an unboundaried reader
5313
+ * opens the entry — and the transaction, judged complete on its other
5314
+ * flights, revealed the inputs beside the reader's pre-flight value
5315
+ * otherwise (`Count: 1 | A: 1` beside `B: 0`). A staged signal or a settled
5316
+ * node registers nothing. Every reporter dies with its reader
5317
+ * (reporterBlocksSource: the read linked it as a dep). The node's own entry
5318
+ * is the only one that can matter: a chain's intermediate memo is re-pulled
5319
+ * by the read (updateIfNecessary's retry) and enters the transaction, so the
5320
+ * reader holds through the normal path; a node with its own flight that is
5321
+ * also pending on an upstream re-ask blocks through that flight until it
5322
+ * lands, and its landing re-runs the reader into the normal path.
5323
+ */
5324
+ /** The replay half of the stale-of-foreign clause (A15 / A26): a stale reader
5325
+ * served the committed value because `txn` holds what it read re-runs at
5326
+ * txn's commit, when the value it was denied becomes the frame — unless its
5327
+ * own last value already came from that transaction. One registration for
5328
+ * the node path (heldFromStale) and the store's backing paths, which have
5329
+ * no node to carry the hold (heldFromReader, the adoption hold view). */
5330
+ function recordStaleReplay(txn, c) {
5331
+ const vt = c._valueTransition;
5332
+ if (vt == null || currentTransition(vt) !== txn) txn._gatedSubs.add(c);
5333
+ }
5334
+ /**
5335
+ * The ownership relation (DESIGN-CONSOLIDATION §6, ruled 2026-09-17): is
5336
+ * `hold` part of the running pass's world? A plain reader's world is the
5337
+ * transaction it runs under, through merges. A lane reader's world is its
5338
+ * lane AND the transition that owns the lane — the one asymmetry between a
5339
+ * lane and a separate transaction (a lane sees what lands from its parent as
5340
+ * its own; a separate transaction would wait for the parent to settle) —
5341
+ * see `ownsLane` in lanes.ts, built on this. One relation for the
5342
+ * stale-of-foreign clause (heldFromStale), the lane arm (readsHeldCommitted)
5343
+ * and the store's backing holds (foreignHold); `serve` has no lane arm of
5344
+ * its own, the lane's extra visibility lives here.
4560
5345
  */
5346
+ function ownsHold(hold) {
5347
+ return (
5348
+ activeTransition !== null && currentTransition(hold) === currentTransition(activeTransition)
5349
+ );
5350
+ }
4561
5351
  function heldFromStale(el, c) {
4562
5352
  const t = el._transition;
4563
- if (t === null || t === activeTransition) return false;
5353
+ if (t === null || ownsHold(t)) return false;
4564
5354
  const txn = currentTransition(t);
4565
- const vt = c._valueTransition;
4566
- if (vt == null || currentTransition(vt) !== txn) txn._gatedSubs.add(c);
5355
+ recordStaleReplay(txn, c);
5356
+ const reporters = txn._asyncReporters.get(el);
5357
+ if (reporters) reporters.add(c);
5358
+ else if (el._statusFlags & STATUS_PENDING)
5359
+ runInTransition(txn, () => c._queue.notify(c, STATUS_PENDING, STATUS_PENDING, el._x._error));
5360
+ return true;
5361
+ }
5362
+ /**
5363
+ * A tracked computation about to be served a live transaction's staged value
5364
+ * derives from that transaction's world, so it enters the transaction and
5365
+ * this pass's result is held with it (A29, #3408). The write side (`setSignal`)
5366
+ * and a stamped node's recompute already enter; a reader that only now
5367
+ * starts reading the held node — a conditional memo whose branch flipped —
5368
+ * was the gap: it published a value derived from the staged world into the
5369
+ * mainline frame. Not for a probe (`isPending(() => x())` observes, it does
5370
+ * not derive) and a no-op for the ambient batch (`_transition` null) or the
5371
+ * transaction already active.
5372
+ */
5373
+ /** The transaction a pass running OUTSIDE a flush was served a staged value
5374
+ * from (A29, creation-time form). Inside a flush the pass enters through
5375
+ * initTransition; outside one — a memo or effect created from mainline code
5376
+ * while a hold is live — entering would leave `activeTransition` and the
5377
+ * batch pointed at the transaction for the rest of the synchronous block,
5378
+ * so an unrelated write made after the mount was held with someone else's
5379
+ * action. The entry is the pass's alone: recompute stages the node into the
5380
+ * transaction (born held) and mainline is never touched. */
5381
+ let stagedEntry = null;
5382
+ function enterStagedRead(el, t = el._transition) {
5383
+ if (!t || t === activeTransition || pendingCheckActive) return;
5384
+ // A companion (the latest() shadow, the isPending() verdict signal) is the
5385
+ // engine's mirror of the flushed world — reading it, or being it, is an
5386
+ // observation, not a derivation from the hold: latest(x) never enters x's
5387
+ // transaction, and the shadow's own pass never enters either (it would
5388
+ // flip activeTransition under the reader that pulled it). (`el` is null for
5389
+ // a store backing served under a hold — no node, the transaction is the
5390
+ // fold's.)
5391
+ if (el?._x?._parentSource || context?._x?._parentSource) return;
5392
+ // Verdict machinery (GlobalQueue._verdictPull: companion creation and the
5393
+ // latest()/isPending() pulls — the latest() shadow is created before it is
5394
+ // marked optimistic, so the bit alone cannot tell) and optimistic nodes
5395
+ // (own lane posture, A31) read staged truth by design; they keep the
5396
+ // entering path.
5397
+ // (`context` is non-null here: every caller selected a value for a reader.)
5398
+ const ctx = context;
5399
+ if (activeTransition === null && !globalQueue._running) {
5400
+ // Verdict pulls are observations, not derivations: a latest() /
5401
+ // isPending() call from mainline must never enter a transaction (it
5402
+ // would capture the rest of the caller's synchronous block).
5403
+ if (GlobalQueue._verdictPull) return;
5404
+ if (
5405
+ ctx._flags & REACTIVE_RECOMPUTING_DEPS &&
5406
+ !(ctx._config & CONFIG_OPTIMISTIC) &&
5407
+ (stagedEntry === null || stagedEntry === t)
5408
+ ) {
5409
+ stagedEntry = t;
5410
+ return;
5411
+ }
5412
+ }
5413
+ globalQueue.initTransition(t);
5414
+ }
5415
+ /**
5416
+ * Rule 1 (value selection), the full arm: does this reader see a STAGED
5417
+ * node's COMMITTED value? One implementation of the rule the fast paths
5418
+ * (readNodeFast, read's fast block) carry as their trivial ternary and that
5419
+ * every slow site — read's tail, the store's backing selection, the lane and
5420
+ * verdict arms — used to restate by hand (docs/DESIGN-CONSOLIDATION.md, move 3b). In order:
5421
+ * - no reader at all (an untracked read) — the committed frame;
5422
+ * - a reader under an optimistic lane the engine says reads committed
5423
+ * (laneReadsCommitted: another lane's hold, #3460);
5424
+ * - nothing staged;
5425
+ * - a children-forbidden reader (createTrackedEffect / onSettled: the frame,
5426
+ * never the graph — A32);
5427
+ * - a stale reader (render effect) of a FOREIGN transaction's staged write —
5428
+ * committed, no entanglement (heldFromStale registers the replay; a node
5429
+ * born held has no committed frame to fall back to, `noCommitted`);
5430
+ * - A17 for HELD truth (#3164, CONFIG_HELD_TRUTH): staged confirming truth —
5431
+ * fold-staged onto an armed family, or entangle-stolen by an awaited
5432
+ * until() — is masked from ordinary readers until its transaction's
5433
+ * reveal, the retaining transaction's own speculative recomputes included
5434
+ * (partial override coverage would otherwise compose override + staged
5435
+ * truth into a state no timeline contains). Authoritative readers
5436
+ * (until()'s predicate) and latest() see the staged truth — the tunnel that
5437
+ * keeps the hold deadlock-free.
5438
+ * False means the reader derives from the staged value and enters its
5439
+ * transaction (enterStagedRead, A29).
5440
+ */
5441
+ function readerSeesCommitted(el, c, owner, noCommitted) {
5442
+ return !!(
5443
+ !c ||
5444
+ (currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
5445
+ el._pendingValue === NOT_PENDING ||
5446
+ c._config & CONFIG_CHILDREN_FORBIDDEN ||
5447
+ (stale && !noCommitted && heldFromStale(el, c)) ||
5448
+ (el._config & CONFIG_HELD_TRUTH &&
5449
+ !latestReadActive &&
5450
+ !(c._config & CONFIG_AUTHORITATIVE_READ))
5451
+ );
5452
+ }
5453
+ /** A28 — set when a node is staged (queuePendingNode) or a held node rewritten
5454
+ * (stashHeldRewrite) OUTSIDE a flush; cleared when the next flush begins. The
5455
+ * read sites test this one module boolean instead of `globalQueue._running`:
5456
+ * inside a flush it is false and the A28 arm costs nothing; outside, only a
5457
+ * tick with unflushed writes pays the staged-node check. */
5458
+ let unflushedStaged = false;
5459
+ function markUnflushedStaged() {
5460
+ unflushedStaged = true;
5461
+ }
5462
+ /** A28 — a write becomes visible at flush. Outside a flush, a node holding an
5463
+ * AMBIENT staged value (no transaction stamp) was written since the last
5464
+ * flush: ambient staging commits at flush end, so nothing else leaves a node
5465
+ * in this state; a stamped value is the flushed held world, which A28 says
5466
+ * latest() serves. Inside a flush the rule does not apply (A28 (4): promoted
5467
+ * within the round). Structural — no marker on the write path. */
5468
+ function unflushed(el) {
5469
+ return unflushedValue(el) !== NOT_PENDING;
5470
+ }
5471
+ /** The value an unflushed node serves — the committed value for an ambient
5472
+ * write, the flushed staged value for a rewrite of a held node — or
5473
+ * NOT_PENDING when nothing is unflushed. Exempt: owned-write nodes (A28 (4):
5474
+ * a write issued inside a recompute is promoted at that recompute's end —
5475
+ * boundary and loading machinery, until()'s internals, signals declared for
5476
+ * in-computation writes) and engine companions (the isPending() verdict
5477
+ * signal, the latest() shadow: the system's own writes, made at the source's
5478
+ * write to mirror it, installing eagerly — A28, A8). */
5479
+ function unflushedValue(el, committed = el._value) {
5480
+ if (
5481
+ globalQueue._running ||
5482
+ el._pendingValue === NOT_PENDING ||
5483
+ el._config & CONFIG_PROMOTED ||
5484
+ el._x?._parentSource
5485
+ )
5486
+ return NOT_PENDING;
5487
+ // Ambient, or adopted by a transaction before any flush carried the staging
5488
+ // (CONFIG_ADOPTED_UNFLUSHED): nothing flushed is staged — the committed
5489
+ // value answers (the caller's notion of committed: a store node's backing).
5490
+ // A held node: unflushed only if rewritten since the last flush (stash).
5491
+ if (el._transition === null || el._config & CONFIG_ADOPTED_UNFLUSHED) return committed;
5492
+ return el._x === null ? NOT_PENDING : el._x._flushedStaged;
5493
+ }
5494
+ /** Held nodes rewritten since the last flush (setSignal); the flush clears
5495
+ * their stash — from then on latest() answers with the rewrite. */
5496
+ const unflushedRewrites = [];
5497
+ /** Nodes written inside a creation-time recompute (CONFIG_PROMOTED). */
5498
+ const promotedWrites = [];
5499
+ /** A28 (5): an optimistic write becomes the ACTIVE override at the flush that
5500
+ * carries it. `_overrideTime` is stamped with `clock` at the write and `clock`
5501
+ * advances after every flush, so "this tick, outside a flush" is unflushed. */
5502
+ function unflushedOverride(el) {
5503
+ // Companions are optimistic signals written by the engine (see unflushed).
5504
+ return !globalQueue._running && el._x?._overrideTime === clock && !el._x?._parentSource;
5505
+ }
5506
+ /** Active optimistic override on an armed node (an armed slot idles at
5507
+ * NOT_PENDING; undefined = unarmed plain node). The writer's own channels —
5508
+ * the draft, `in`/keys inside the setter — compose on this regardless of
5509
+ * flush state. */
5510
+ function hasActiveOverride(el) {
5511
+ const x = el._x;
5512
+ return x !== null && x._overrideValue !== undefined && x._overrideValue !== NOT_PENDING;
5513
+ }
5514
+ /** The override a READER sees: installed, and carried by a flush (A28 (5) —
5515
+ * an optimistic write is a write; until its flush no reader sees it). One
5516
+ * implementation for read()'s override arm, the verdict channels
5517
+ * (latestRead, computePendingState) and the store's selection
5518
+ * (docs/DESIGN-CONSOLIDATION.md, move 3b). */
5519
+ function visibleOverride(el) {
5520
+ return hasActiveOverride(el) && !unflushedOverride(el);
5521
+ }
5522
+ /** A derivation served the committed value because of an unflushed write
5523
+ * (A28) must run again in the flush that carries it — the late-linker case
5524
+ * (#3337's reason to defer the walk): it linked after the write walked. */
5525
+ function markLateLinker(c) {
5526
+ // The pass's own tail re-enqueues on this latch (recompute's finally) —
5527
+ // a direct enqueue here would be wiped by the pass's flag reset.
5528
+ c._flags |= REACTIVE_MISSED_WAKE;
4567
5529
  return true;
4568
5530
  }
5531
+ /** Companion-bearing nodes written outside a flush (setSignal); the flush
5532
+ * that carries their writes re-syncs their companions (A28). */
5533
+ const unflushedCompanions = [];
5534
+ function resyncUnflushedCompanions() {
5535
+ unflushedStaged = false;
5536
+ // Length-guarded: the common flush has nothing here, and must allocate nothing.
5537
+ // Length-guarded: the common flush has nothing here and allocates nothing.
5538
+ if (unflushedRewrites.length !== 0) {
5539
+ for (const el of unflushedRewrites) el._x._flushedStaged = NOT_PENDING;
5540
+ unflushedRewrites.length = 0;
5541
+ }
5542
+ if (promotedWrites.length !== 0) {
5543
+ for (const el of promotedWrites) el._config &= ~CONFIG_PROMOTED;
5544
+ promotedWrites.length = 0;
5545
+ }
5546
+ if (unflushedCompanions.length !== 0) {
5547
+ for (const el of unflushedCompanions)
5548
+ GlobalQueue._syncCompanions(
5549
+ el,
5550
+ el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value
5551
+ );
5552
+ unflushedCompanions.length = 0;
5553
+ }
5554
+ }
4569
5555
  function readNodeFast(el) {
4570
5556
  if (
4571
5557
  latestReadActive ||
@@ -4577,6 +5563,9 @@ function readNodeFast(el) {
4577
5563
  activeTransition !== null ||
4578
5564
  currentOptimisticLane !== null ||
4579
5565
  snapshotCaptureActive ||
5566
+ // A28: a staged node read outside a flush may serve its committed value;
5567
+ // that arm lives on the slow path so this body stays inlinable.
5568
+ (unflushedStaged && el._pendingValue !== NOT_PENDING) ||
4580
5569
  strictRead
4581
5570
  )
4582
5571
  return READ_SLOW;
@@ -4598,7 +5587,7 @@ function readNodeFast(el) {
4598
5587
  c._config & CONFIG_CHILDREN_FORBIDDEN ||
4599
5588
  (stale && heldFromStale(el, c))
4600
5589
  ? el._value
4601
- : el._pendingValue;
5590
+ : (enterStagedRead(el), el._pendingValue);
4602
5591
  }
4603
5592
  function read(el) {
4604
5593
  // Handle latest() mode: read from _latestValueComputed
@@ -4627,6 +5616,7 @@ function read(el) {
4627
5616
  activeTransition === null &&
4628
5617
  currentOptimisticLane === null &&
4629
5618
  !snapshotCaptureActive &&
5619
+ (!unflushedStaged || el._pendingValue === NOT_PENDING) && // A28, see readNodeFast
4630
5620
  !strictRead
4631
5621
  ) {
4632
5622
  if (c && tracking) link(el, c);
@@ -4637,7 +5627,7 @@ function read(el) {
4637
5627
  c._config & CONFIG_CHILDREN_FORBIDDEN ||
4638
5628
  (stale && heldFromStale(el, c))
4639
5629
  ? el._value
4640
- : el._pendingValue;
5630
+ : (enterStagedRead(el), el._pendingValue);
4641
5631
  }
4642
5632
  // The dev component-body safeguard (#2897) must not fire inside an
4643
5633
  // isPending() probe: its plain Error would be swallowed by the probe's
@@ -4724,7 +5714,10 @@ function read(el) {
4724
5714
  // lane (#3276); that check rides laneSuspends so floor bundles don't
4725
5715
  // pay for it.
4726
5716
  if (currentOptimisticLane === null || GlobalQueue._laneSuspends(owner)) {
4727
- if (!tracking && el !== c) link(el, c);
5717
+ // An untracked read of a pending node still re-runs its reader when
5718
+ // the node settles — unless nobody is reading (`spectating`, #3528)
5719
+ // or the reader is the node itself.
5720
+ if (!tracking && !spectating && el !== c) link(el, c);
4728
5721
  throw owner._x?._error;
4729
5722
  }
4730
5723
  } else if (!c && owner._statusFlags & STATUS_UNINITIALIZED) {
@@ -4760,7 +5753,48 @@ function read(el) {
4760
5753
  ownerName: c?._name,
4761
5754
  nodeName: owner?._name
4762
5755
  });
4763
- if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
5756
+ const value = serve(el, c, owner, el._value);
5757
+ if (
5758
+ !c &&
5759
+ owner === el &&
5760
+ typeof computed._fn === "function" &&
5761
+ el._config & CONFIG_AUTO_DISPOSE &&
5762
+ !(owner._statusFlags & STATUS_PENDING) &&
5763
+ !el._subs &&
5764
+ // An untracked read served a visible override returned before this
5765
+ // sweep registration when the arm was inline; keep that.
5766
+ !visibleOverride(el)
5767
+ ) {
5768
+ // Deferred, not inline (#3078): an inline unobserved() here made untracked
5769
+ // reads destructive — dispose on this read, full revival recompute on the
5770
+ // next — so consecutive reads could answer differently with no write in
5771
+ // between (the revival samples the ambient transition/lane context).
5772
+ // The sweep at flush finalization re-validates and reclaims; schedule()
5773
+ // guarantees that flush happens even if nothing else is queued.
5774
+ dormantNodes.add(el);
5775
+ schedule();
5776
+ }
5777
+ return value;
5778
+ }
5779
+ /**
5780
+ * Rule 1, the one slow implementation (DESIGN-CONSOLIDATION move 3b, step
5781
+ * 6c): the value a reader `c` (null = untracked, no pass) is served from
5782
+ * `el`, whose committed value is `committed` — the node's own `_value` for
5783
+ * a signal or memo, the BACKING for a store property node (single-home rule,
5784
+ * O6: committed truth lives in the backing and a node's `_value` is never
5785
+ * served for one). Called by read()'s slow tail and by the store's untracked
5786
+ * node path (nodeValue); the fast paths (readNodeFast, read's fast block)
5787
+ * keep their trivial ternary by design (perf, see the doc). Arms, in order:
5788
+ * - the override (A17), routed through the engine for a tracked reader under
5789
+ * a lane or a supersession (A18), an authoritative reader marked instead;
5790
+ * - the lane entanglement gate (committed, recorded for replay);
5791
+ * - a node born held has nothing for an untracked reader (A19 exception 1);
5792
+ * - an unflushed write serves committed and re-runs the reader in the
5793
+ * carrying flush (A28);
5794
+ * - readerSeesCommitted, else the staged value and the transaction (A29).
5795
+ */
5796
+ function serve(el, c, owner, committed) {
5797
+ if (hasActiveOverride(el)) {
4764
5798
  // A17: the override IS the value for every reader — except an authoritative
4765
5799
  // reader (until()'s predicate carries CONFIG_AUTHORITATIVE_READ): it must
4766
5800
  // observe independently-arriving truth, and serving it the caller's own
@@ -4771,13 +5805,18 @@ function read(el) {
4771
5805
  // authoritative — optimism never lives there); the sticky mark makes the
4772
5806
  // A17-silent "landing equals override" paths notify this node's subs so
4773
5807
  // the reader re-runs when truth arrives.
4774
- if (!(c && c._config & CONFIG_AUTHORITATIVE_READ)) {
4775
- // A18 supersession (#3331): the node's own source answered with a
4776
- // DIFFERENT value. The optimism is over for the graph — a tracked
4777
- // reader sees the staged truth while the override remains the
4778
- // DISPLAYED value for untracked reads (and for a stale reader of some
4779
- // other transaction). The selection lives with the engine.
4780
- if (c && el._config & CONFIG_OVERRIDE_SUPERSEDED) return GlobalQueue._supersededRead(el);
5808
+ // A28 (5): an optimistic write written this tick, outside a flush, is not
5809
+ // yet the active override fall through to the normal selection (the
5810
+ // authoritative mark below still applies: until() must wake on landing).
5811
+ if (!(c && c._config & CONFIG_AUTHORITATIVE_READ) && !unflushedOverride(el)) {
5812
+ // A tracked read of an override is the engine's selection (a lane or a
5813
+ // supersession implies the engine): a render effect OFF the override's
5814
+ // held lane sees the committed value (#3460, lanes mirror transitions);
5815
+ // a node whose own source answered with a DIFFERENT value hands a
5816
+ // tracked reader the staged truth (A18 supersession, #3331). Untracked
5817
+ // reads display the override.
5818
+ if (c && el._config & (CONFIG_HAS_LANE | CONFIG_OVERRIDE_SUPERSEDED))
5819
+ return GlobalQueue._overrideRead(el, c);
4781
5820
  return unwrapOverride(el._x?._overrideValue);
4782
5821
  }
4783
5822
  el._config |= CONFIG_AUTHORITATIVE_OBSERVED;
@@ -4795,7 +5834,7 @@ function read(el) {
4795
5834
  c !== null &&
4796
5835
  GlobalQueue._gatedRead(el, owner, c)
4797
5836
  ) {
4798
- return el._value;
5837
+ return committed;
4799
5838
  }
4800
5839
  // In optimistic lane context, return _value for optimistic/lane-assigned signals
4801
5840
  // and for regular signals in stale mode (render effects). Non-stale readers (user
@@ -4803,58 +5842,40 @@ function read(el) {
4803
5842
  // (The lane-context clause lives with the engine.) Children-forbidden readers
4804
5843
  // (createTrackedEffect / onSettled callbacks) get committed visibility — see
4805
5844
  // readNodeFast (#3006).
4806
- const value =
4807
- !c ||
4808
- (currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
4809
- el._pendingValue === NOT_PENDING ||
4810
- c._config & CONFIG_CHILDREN_FORBIDDEN ||
4811
- (stale && heldFromStale(el, c)) ||
4812
- // A17 for HELD truth (#3164, see CONFIG_HELD_TRUTH): staged confirming
4813
- // truth fold-staged onto an armed family, or entangle-stolen by an
4814
- // awaited until() is masked from ordinary readers until its
4815
- // transaction's reveal; the retaining transaction's own speculative
4816
- // recomputes included (partial override coverage would otherwise
4817
- // compose override + staged truth into a state no timeline contains).
4818
- // Authoritative readers (until()'s predicate) and latest() see the
4819
- // staged truth the tunnel that keeps the hold deadlock-free.
4820
- (el._config & CONFIG_HELD_TRUTH &&
4821
- !latestReadActive &&
4822
- !(c._config & CONFIG_AUTHORITATIVE_READ))
4823
- ? el._value
4824
- : el._pendingValue;
5845
+ // A node born held (recompute) has a staged value and no committed one: a
5846
+ // stale reader cannot fall back to the committed frame, so it takes the
5847
+ // staged value and enters like a tracked reader (A29); an untracked reader
5848
+ // has nothing to serve and holds (A19 exception 1).
5849
+ const noCommitted =
5850
+ el._pendingValue !== NOT_PENDING && (el._statusFlags & STATUS_UNINITIALIZED) !== 0;
5851
+ if (noCommitted && !c) throw new NotReadyError(null);
5852
+ const u = c && unflushedStaged ? unflushedValue(el, committed) : NOT_PENDING;
5853
+ if (u !== NOT_PENDING) {
5854
+ markLateLinker(c);
5855
+ if (pendingCheckActive) GlobalQueue._recordFresh(el, u);
5856
+ return u;
5857
+ }
5858
+ const value = readerSeesCommitted(el, c, owner, noCommitted)
5859
+ ? committed
5860
+ : (enterStagedRead(el), el._pendingValue);
4825
5861
  // Record that this isPending() probe observed the fresh pending value, so
4826
5862
  // the probe doesn't pair "pending" with the new value (#2831).
4827
5863
  if (pendingCheckActive) GlobalQueue._recordFresh(el, value);
4828
- if (
4829
- !c &&
4830
- owner === el &&
4831
- typeof computed._fn === "function" &&
4832
- el._config & CONFIG_AUTO_DISPOSE &&
4833
- !(owner._statusFlags & STATUS_PENDING) &&
4834
- !el._subs
4835
- ) {
4836
- // Deferred, not inline (#3078): an inline unobserved() here made untracked
4837
- // reads destructive — dispose on this read, full revival recompute on the
4838
- // next — so consecutive reads could answer differently with no write in
4839
- // between (the revival samples the ambient transition/lane context).
4840
- // The sweep at flush finalization re-validates and reclaims; schedule()
4841
- // guarantees that flush happens even if nothing else is queued.
4842
- dormantNodes.add(el);
4843
- schedule();
4844
- }
4845
5864
  return value;
4846
5865
  }
4847
5866
  /**
4848
5867
  * Store-rewrite setter guard: the rewrite parks writes in a pending backing
4849
5868
  * (no setSignal at write time), so the owned-scope write protection must
4850
- * fire at the setter entry instead. Mirrors setSignal's guard condition
5869
+ * fire at the setter entry instead. Exactly setSignal's guard condition
4851
5870
  * minus the node-specific exemptions (ownedWrite/firewall), which don't
4852
- * apply to plain store setters.
5871
+ * apply to plain store setters. Roots are NOT exempt (#3500): a root body is
5872
+ * tree construction — every dev component body, every context Provider, the
5873
+ * top of `render()`, and the whole SSR pass run directly under one — and a
5874
+ * write there re-runs what already read the old value (or on the server,
5875
+ * can't). Same rule as setSignal, which never exempted roots.
4853
5876
  */
4854
5877
  function devGuardStoreSetterWrite() {
4855
- // Roots are not owned computation scopes — setters inside createRoot bodies
4856
- // are legal (legacy parity; the guard targets computed/effect bodies).
4857
- if (context && !context._root && !(context._config & CONFIG_CHILDREN_FORBIDDEN)) {
5878
+ if (context && !(context._config & CONFIG_CHILDREN_FORBIDDEN)) {
4858
5879
  emitDiagnostic({
4859
5880
  code: "REACTIVE_WRITE_IN_OWNED_SCOPE",
4860
5881
  kind: "write",
@@ -4869,12 +5890,59 @@ function devGuardStoreSetterWrite() {
4869
5890
  throw new Error(ownedScopeWriteMessage(context));
4870
5891
  }
4871
5892
  }
5893
+ /**
5894
+ * Store setter result guard: the callback's return has one meaning — a
5895
+ * replacement root to adopt — and a thenable can never be that. It is the
5896
+ * signature of `setStore(async d => …)` (or a sync arrow whose helper is
5897
+ * async): only the writes before the first `await` were in the transaction;
5898
+ * the rest land on a closed draft and vanish. Setters are synchronous
5899
+ * transactions; async orchestration is `action()`'s job. Store-specific —
5900
+ * a signal may legitimately hold a promise, so its setter has no such rule.
5901
+ */
5902
+ function devGuardStoreSetterResult(result) {
5903
+ if (result == null) return;
5904
+ if (
5905
+ (typeof result === "object" || typeof result === "function") &&
5906
+ typeof result.then === "function"
5907
+ ) {
5908
+ emitDiagnostic({
5909
+ code: "ASYNC_STORE_SETTER",
5910
+ kind: "write",
5911
+ severity: "error",
5912
+ message: ASYNC_STORE_SETTER_MESSAGE,
5913
+ ownerId: context?.id,
5914
+ ownerName: context?._name,
5915
+ data: { operation: "setStore" }
5916
+ });
5917
+ throw new Error(ASYNC_STORE_SETTER_MESSAGE);
5918
+ }
5919
+ }
4872
5920
  function ownedScopeWriteMessage(owner) {
4873
5921
  const name = owner._name;
4874
5922
  return name
4875
5923
  ? `${REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE} (in ${name})`
4876
5924
  : REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE;
4877
5925
  }
5926
+ /** A28 — a rewrite of a HELD node outside a flush keeps the staged value the
5927
+ * last flush left, for latest()/verdicts, until the next flush carries it
5928
+ * (stash, cleared at that flush). Cold: only stamped nodes reach here. */
5929
+ function stashHeldRewrite(el) {
5930
+ if (globalQueue._running) return;
5931
+ const x = ext(el);
5932
+ if (x._flushedStaged === NOT_PENDING) {
5933
+ x._flushedStaged = el._pendingValue;
5934
+ unflushedRewrites.push(el);
5935
+ unflushedStaged = true;
5936
+ }
5937
+ }
5938
+ /** A28 (4) — written inside a recompute that runs OUTSIDE a flush (a
5939
+ * creation-time compute): promoted at that pass's end, visible to the rest of
5940
+ * the block. Cold: only contextual writes reach here. */
5941
+ function notePromotedWrite(el) {
5942
+ if (globalQueue._running || el._config & CONFIG_PROMOTED) return;
5943
+ el._config |= CONFIG_PROMOTED;
5944
+ promotedWrites.push(el);
5945
+ }
4878
5946
  function setSignal(el, v) {
4879
5947
  if (
4880
5948
  !(el._config & CONFIG_OWNED_WRITE) &&
@@ -4894,8 +5962,22 @@ function setSignal(el, v) {
4894
5962
  });
4895
5963
  throw new Error(ownedScopeWriteMessage(context));
4896
5964
  }
4897
- if (el._transition && activeTransition !== el._transition)
4898
- globalQueue.initTransition(el._transition);
5965
+ // A write to a held node is a second proposal on a contested node (A34, #3494):
5966
+ // the writer's tick reveals with the hold, the same value or another. Inside
5967
+ // a flush the round enters now; from mainline the entry waits for the next
5968
+ // flush's start (batchJoins) — entering here left activeTransition set for
5969
+ // the rest of the caller's block, so a memo created after the write became
5970
+ // the transaction's instead of mainline's (A28, A29). Before the equality
5971
+ // gate below: repeating the held value proposes it too — and that repeat
5972
+ // leaves through the gate, so the join schedules its own flush here; left
5973
+ // for the next flush to find, it adopted an unrelated tick (#3519 review).
5974
+ if (el._transition && activeTransition !== el._transition) {
5975
+ if (globalQueue._running) globalQueue.initTransition(el._transition);
5976
+ else {
5977
+ batchJoins.push(el._transition); // dupes: a bare return in initTransition
5978
+ schedule();
5979
+ }
5980
+ }
4899
5981
  // The optimistic write path lives with the engine: only optimisticSignal /
4900
5982
  // optimisticComputed callers and optimistic store nodes carry an
4901
5983
  // _overrideValue slot (flagged by CONFIG_OPTIMISTIC — a masked read of the
@@ -4919,15 +6001,25 @@ function setSignal(el, v) {
4919
6001
  if (attrHooks !== null) attrHooks.write(el, currentValue, v);
4920
6002
  const wasStaged = el._pendingValue !== NOT_PENDING;
4921
6003
  if (!wasStaged) queuePendingNode(el);
6004
+ // A28 arms, gated on the loads the write already pays for (a plain ambient
6005
+ // rewrite outside a flush — the write-loop shape — costs `_transition` and
6006
+ // `context` here and nothing else; the arms themselves are cold helpers so
6007
+ // setSignal stays within every setter's inlining budget, ~300 B bytecode).
6008
+ else if (el._transition !== null) stashHeldRewrite(el);
4922
6009
  el._pendingValue = v;
6010
+ if (context !== null) notePromotedWrite(el);
4923
6011
  // syncCompanions only pokes _pendingSignal/_latestValueComputed — with
4924
6012
  // neither companion present the call is a guaranteed no-op (companions are
4925
6013
  // only ever created, never removed, and creating one installs the hook and
4926
6014
  // sets CONFIG_HAS_COMPANIONS — one masked read replaces two optional-field
4927
6015
  // probes on every write).
4928
- el._config & CONFIG_HAS_COMPANIONS &&
4929
- GlobalQueue._syncCompanions !== null &&
6016
+ if (el._config & CONFIG_HAS_COMPANIONS && GlobalQueue._syncCompanions !== null) {
4930
6017
  GlobalQueue._syncCompanions(el, v);
6018
+ // A28 (2): the verdict computed here is the PRE-flush one (an unflushed
6019
+ // write is not pending); the flush that carries the write re-syncs so
6020
+ // the companions mirror the flushed world. Only companion nodes pay.
6021
+ if (!globalQueue._running) unflushedCompanions.push(el);
6022
+ }
4931
6023
  // _time is a computed-only slot (§12e): writing it on a signal would fork
4932
6024
  // the lean shape. Every read site is computed-typed.
4933
6025
  if (el._fn !== undefined) el._time = clock;
@@ -5091,131 +6183,153 @@ function markRefresh(node) {
5091
6183
  }
5092
6184
 
5093
6185
  export {
5094
- context as $,
5095
- findLane as A,
5096
- stale as B,
6186
+ insertIntoHeap as $,
6187
+ laneHeld as A,
6188
+ CONFIG_AUTHORITATIVE_OBSERVED as B,
5097
6189
  ContextNotFoundError as C,
5098
- queuePendingNode as D,
6190
+ findLane as D,
5099
6191
  EFFECT_RENDER as E,
5100
- latestReadActive as F,
6192
+ sourceObserved as F,
5101
6193
  GlobalQueue as G,
5102
- REACTIVE_MANUAL_WRITE as H,
5103
- currentOptimisticLane as I,
5104
- resolveLane as J,
5105
- REACTIVE_OPTIMISTIC_DIRTY as K,
5106
- assignOrMergeLane as L,
5107
- LANE_RUN as M,
6194
+ resolveLane as H,
6195
+ stale as I,
6196
+ readsHeldCommitted as J,
6197
+ enterStagedRead as K,
6198
+ currentTransition as L,
6199
+ queuePendingNode as M,
5108
6200
  NoOwnerError as N,
5109
6201
  OVERRIDE_UNDEFINED as O,
5110
- attrHooks as P,
5111
- EFFECT_USER as Q,
6202
+ latestReadActive as P,
6203
+ REACTIVE_MANUAL_WRITE as Q,
5112
6204
  REACTIVE_DIRTY as R,
5113
6205
  STATUS_UNINITIALIZED as S,
5114
- REACTIVE_DISPOSED as T,
5115
- insertIntoHeap as U,
5116
- queueFor as V,
5117
- REACTIVE_ZOMBIE as W,
5118
- markHeap as X,
5119
- pendingCheckActive as Y,
5120
- setPendingCheckActive as Z,
5121
- prepareComputed as _,
6206
+ currentOptimisticLane as T,
6207
+ REACTIVE_OPTIMISTIC_DIRTY as U,
6208
+ assignOrMergeLane as V,
6209
+ LANE_RUN as W,
6210
+ attrHooks as X,
6211
+ EFFECT_USER as Y,
6212
+ REACTIVE_DISPOSED as Z,
6213
+ dispose as _,
5122
6214
  setSignal as a,
5123
- isEqual as a$,
5124
- tracking as a0,
5125
- link as a1,
5126
- currentTransition as a2,
5127
- enqueueSub as a3,
5128
- setLatestReadActive as a4,
5129
- setContextInternal as a5,
5130
- optimisticComputed as a6,
5131
- CONFIG_HAS_COMPANIONS as a7,
5132
- CONFIG_CHILD_COMPANIONS as a8,
5133
- setStrictRead as a9,
5134
- enterActionStep as aA,
5135
- exitActionStep as aB,
5136
- registerGraph as aC,
5137
- runWithOwner as aD,
5138
- setMemo as aE,
5139
- $REFRESH as aF,
5140
- installAuthoritativeRead as aG,
5141
- markRefresh as aH,
5142
- createRoot as aI,
5143
- getObserver as aJ,
5144
- dispose as aK,
5145
- CONFIG_DIRECT_COMMIT as aL,
5146
- CONFIG_AUTHORITATIVE_READ as aM,
5147
- entangleConfirmingTransitions as aN,
5148
- TimeoutError as aO,
5149
- untrack as aP,
5150
- Queue as aQ,
5151
- CONFIG_FRESH_READ as aR,
5152
- forEachDependent as aS,
5153
- statusNotifierOf as aT,
5154
- shiftAffectsMarks as aU,
5155
- SUPPORTS_PROXY as aV,
5156
- setSlotUnobserved as aW,
5157
- devGuardStoreSetterWrite as aX,
5158
- projectionWriteActive as aY,
5159
- setProjectionWriteActive as aZ,
5160
- DEV as a_,
5161
- strictRead as aa,
5162
- optimisticSignal as ab,
5163
- activeAffectsMarks as ac,
5164
- STATUS_ERROR as ad,
5165
- REACTIVE_RECOMPUTING_DEPS as ae,
5166
- setEffectStatusNotify as af,
5167
- unwrapStatusError as ag,
5168
- haltReactivity as ah,
5169
- StatusError as ai,
5170
- createEffectNode as aj,
5171
- recompute as ak,
5172
- reportDiagnostic as al,
5173
- emitDiagnostic as am,
5174
- computed as an,
5175
- staleValues as ao,
5176
- CONFIG_AUTO_DISPOSE as ap,
5177
- CONFIG_CHILDREN_FORBIDDEN as aq,
5178
- EFFECT_TRACKED as ar,
5179
- setEffectCallback as as,
5180
- setTrackedQueueCallback as at,
5181
- _hitUnhandledAsync as au,
5182
- resetUnhandledAsync as av,
5183
- setOrigin as aw,
5184
- isThenable as ax,
5185
- actionStepDepth as ay,
5186
- flush as az,
6215
+ TimeoutError as a$,
6216
+ queueFor as a0,
6217
+ visibleOverride as a1,
6218
+ unflushedValue as a2,
6219
+ context as a3,
6220
+ REACTIVE_RECOMPUTING_DEPS as a4,
6221
+ markLateLinker as a5,
6222
+ prepareComputed as a6,
6223
+ tracking as a7,
6224
+ link as a8,
6225
+ enqueueSub as a9,
6226
+ staleValues as aA,
6227
+ CONFIG_AUTO_DISPOSE as aB,
6228
+ CONFIG_CHILDREN_FORBIDDEN as aC,
6229
+ EFFECT_TRACKED as aD,
6230
+ setEffectCallback as aE,
6231
+ setTrackedQueueCallback as aF,
6232
+ _hitUnhandledAsync as aG,
6233
+ resetUnhandledAsync as aH,
6234
+ setOrigin as aI,
6235
+ isThenable as aJ,
6236
+ actionStepDepth as aK,
6237
+ flush as aL,
6238
+ enterActionStep as aM,
6239
+ exitActionStep as aN,
6240
+ ROOT_ERROR_HOOK as aO,
6241
+ registerGraph as aP,
6242
+ runWithOwner as aQ,
6243
+ setMemo as aR,
6244
+ dirtyQueue as aS,
6245
+ $REFRESH as aT,
6246
+ installAuthoritativeRead as aU,
6247
+ markRefresh as aV,
6248
+ createRoot as aW,
6249
+ getObserver as aX,
6250
+ CONFIG_DIRECT_COMMIT as aY,
6251
+ CONFIG_AUTHORITATIVE_READ as aZ,
6252
+ entangleConfirmingTransitions as a_,
6253
+ setLatestReadActive as aa,
6254
+ setContextInternal as ab,
6255
+ optimisticComputed as ac,
6256
+ CONFIG_HAS_COMPANIONS as ad,
6257
+ unflushed as ae,
6258
+ CONFIG_ADOPTED_UNFLUSHED as af,
6259
+ CONFIG_CHILD_COMPANIONS as ag,
6260
+ runAsTransitionBatch as ah,
6261
+ unflushedCompanions as ai,
6262
+ setPendingCheckActive as aj,
6263
+ setStrictRead as ak,
6264
+ strictRead as al,
6265
+ optimisticSignal as am,
6266
+ activeAffectsMarks as an,
6267
+ pendingCheckActive as ao,
6268
+ STATUS_ERROR as ap,
6269
+ setEffectStatusNotify as aq,
6270
+ unwrapStatusError as ar,
6271
+ haltReactivity as as,
6272
+ StatusError as at,
6273
+ trimStaleDeps as au,
6274
+ createEffectNode as av,
6275
+ recompute as aw,
6276
+ reportDiagnostic as ax,
6277
+ emitDiagnostic as ay,
6278
+ computed as az,
5187
6279
  NOT_PENDING as b,
5188
- readNodeFast as b0,
5189
- READ_SLOW as b1,
5190
- throwPendingUntrackedRead as b2,
5191
- warnStrictReadUntracked as b3,
5192
- unlinkFirewallChild as b4,
5193
- CONFIG_OWNED_WRITE as b5,
5194
- CONFIG_OPTIMISTIC as b6,
5195
- slotSignal as b7,
5196
- CONFIG_HELD_TRUTH as b8,
5197
- setStoreCommitHook as b9,
5198
- suppressComputedRecompute as ba,
5199
- handleAsync as bb,
5200
- createTransition as bc,
5201
- runAsTransitionBatch as bd,
5202
- createOwner as be,
5203
- OBSERVE as bf,
5204
- clearSnapshots as bg,
5205
- enforceLoadingBoundary as bh,
5206
- getNextChildId as bi,
5207
- isDisposed as bj,
5208
- markSnapshotScope as bk,
5209
- peekNextChildId as bl,
5210
- releaseSnapshotScope as bm,
5211
- resetErrorHalt as bn,
5212
- setSnapshotCapture as bo,
5213
- setAttributionHooks as bp,
5214
- ownerPath as bq,
5215
- isSuppressed as br,
5216
- anyExcluded as bs,
5217
- isExcluded as bt,
5218
- GRAPH_SIZE_WARN_AT as bu,
6280
+ untrack as b0,
6281
+ Queue as b1,
6282
+ CONFIG_FRESH_READ as b2,
6283
+ forEachDependent as b3,
6284
+ statusNotifierOf as b4,
6285
+ shiftAffectsMarks as b5,
6286
+ SUPPORTS_PROXY as b6,
6287
+ setSlotUnobserved as b7,
6288
+ devGuardStoreSetterWrite as b8,
6289
+ devGuardStoreSetterResult as b9,
6290
+ clearSnapshots as bA,
6291
+ enforceLoadingBoundary as bB,
6292
+ getNextChildId as bC,
6293
+ isDisposed as bD,
6294
+ markSnapshotScope as bE,
6295
+ ownerPath as bF,
6296
+ peekNextChildId as bG,
6297
+ releaseSnapshotScope as bH,
6298
+ resetErrorHalt as bI,
6299
+ setSnapshotCapture as bJ,
6300
+ setAttributionHooks as bK,
6301
+ isSuppressed as bL,
6302
+ recordSubject as bM,
6303
+ anyExcluded as bN,
6304
+ isExcluded as bO,
6305
+ GRAPH_SIZE_WARN_AT as bP,
6306
+ subjectOf as bQ,
6307
+ projectionWriteActive as ba,
6308
+ setProjectionWriteActive as bb,
6309
+ DEV as bc,
6310
+ isEqual as bd,
6311
+ readNodeFast as be,
6312
+ READ_SLOW as bf,
6313
+ throwPendingUntrackedRead as bg,
6314
+ warnStrictReadUntracked as bh,
6315
+ unlinkFirewallChild as bi,
6316
+ CONFIG_OWNED_WRITE as bj,
6317
+ ownsHold as bk,
6318
+ deferSlotRelease as bl,
6319
+ slotSignal as bm,
6320
+ serve as bn,
6321
+ recordStaleReplay as bo,
6322
+ setStoreCommitHook as bp,
6323
+ suppressComputedRecompute as bq,
6324
+ handleAsync as br,
6325
+ createTransition as bs,
6326
+ CONFIG_HELD_TRUTH as bt,
6327
+ createOwner as bu,
6328
+ spectate as bv,
6329
+ transitions as bw,
6330
+ wakeParked as bx,
6331
+ REACTIVE_ZOMBIE as by,
6332
+ OBSERVE as bz,
5219
6333
  cleanup as c,
5220
6334
  REACTIVE_CHECK as d,
5221
6335
  resolveTransition as e,
@@ -5230,14 +6344,14 @@ export {
5230
6344
  schedule as n,
5231
6345
  origin as o,
5232
6346
  STATUS_PENDING as p,
5233
- CONFIG_OVERRIDE_SUPERSEDED as q,
6347
+ CONFIG_DERIVED_OVERRIDE as q,
5234
6348
  read as r,
5235
6349
  signal as s,
5236
- hasActiveOverride as t,
6350
+ CONFIG_OPTIMISTIC as t,
5237
6351
  unwrapOverride as u,
5238
- NotReadyError as v,
5239
- activeLanes as w,
5240
- signalLanes as x,
5241
- laneHeld as y,
5242
- CONFIG_AUTHORITATIVE_OBSERVED as z
6352
+ CONFIG_OVERRIDE_SUPERSEDED as v,
6353
+ hasActiveOverride as w,
6354
+ NotReadyError as x,
6355
+ activeLanes as y,
6356
+ signalLanes as z
5243
6357
  };