@solidjs/signals 2.0.0-rc.3 → 2.0.0-rc.5

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 (71) hide show
  1. package/dist/dev.js +2494 -271
  2. package/dist/node.cjs +3691 -1595
  3. package/dist/prod/affects.js +13 -12
  4. package/dist/prod/boundaries.js +43 -35
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +137 -106
  7. package/dist/prod/core/constants.js +55 -1
  8. package/dist/prod/core/core.js +354 -247
  9. package/dist/prod/core/effect.js +47 -50
  10. package/dist/prod/core/error.js +13 -1
  11. package/dist/prod/core/external.js +4 -4
  12. package/dist/prod/core/graph.js +88 -52
  13. package/dist/prod/core/heap.js +38 -38
  14. package/dist/prod/core/lanes.js +34 -34
  15. package/dist/prod/core/optimistic.js +61 -58
  16. package/dist/prod/core/owner.js +38 -38
  17. package/dist/prod/core/scheduler.js +401 -174
  18. package/dist/prod/core/verdict.js +132 -65
  19. package/dist/prod/index.js +7 -3
  20. package/dist/prod/map.js +106 -106
  21. package/dist/prod/signals.js +253 -25
  22. package/dist/prod/store/index.js +2 -0
  23. package/dist/prod/store/next/optimistic.js +314 -121
  24. package/dist/prod/store/next/patch-hooks.js +13 -0
  25. package/dist/prod/store/next/patch.js +614 -0
  26. package/dist/prod/store/next/projection.js +23 -19
  27. package/dist/prod/store/next/reconcile.js +307 -120
  28. package/dist/prod/store/next/store.js +440 -117
  29. package/dist/prod/store/next/target.js +13 -4
  30. package/dist/prod/store/store.js +5 -5
  31. package/dist/types/core/async.d.ts +2 -0
  32. package/dist/types/core/attribution.d.ts +9 -4
  33. package/dist/types/core/constants.d.ts +54 -0
  34. package/dist/types/core/core.d.ts +34 -21
  35. package/dist/types/core/dev.d.ts +8 -0
  36. package/dist/types/core/error.d.ts +9 -0
  37. package/dist/types/core/graph.d.ts +22 -0
  38. package/dist/types/core/index.d.ts +2 -2
  39. package/dist/types/core/scheduler.d.ts +46 -0
  40. package/dist/types/core/types.d.ts +12 -0
  41. package/dist/types/index.d.ts +3 -3
  42. package/dist/types/signals.d.ts +108 -0
  43. package/dist/types/store/index.d.ts +2 -0
  44. package/dist/types/store/next/optimistic.d.ts +13 -10
  45. package/dist/types/store/next/patch-hooks.d.ts +41 -0
  46. package/dist/types/store/next/patch.d.ts +91 -0
  47. package/dist/types/store/next/projection.d.ts +1 -1
  48. package/dist/types/store/next/reconcile.d.ts +14 -0
  49. package/dist/types/store/next/store.d.ts +52 -2
  50. package/dist/types/store/next/target.d.ts +73 -8
  51. package/dist/types-cjs/core/async.d.cts +2 -0
  52. package/dist/types-cjs/core/attribution.d.cts +9 -4
  53. package/dist/types-cjs/core/constants.d.cts +54 -0
  54. package/dist/types-cjs/core/core.d.cts +34 -21
  55. package/dist/types-cjs/core/dev.d.cts +8 -0
  56. package/dist/types-cjs/core/error.d.cts +9 -0
  57. package/dist/types-cjs/core/graph.d.cts +22 -0
  58. package/dist/types-cjs/core/index.d.cts +2 -2
  59. package/dist/types-cjs/core/scheduler.d.cts +46 -0
  60. package/dist/types-cjs/core/types.d.cts +12 -0
  61. package/dist/types-cjs/index.d.cts +3 -3
  62. package/dist/types-cjs/signals.d.cts +108 -0
  63. package/dist/types-cjs/store/index.d.cts +2 -0
  64. package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
  65. package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
  66. package/dist/types-cjs/store/next/patch.d.cts +91 -0
  67. package/dist/types-cjs/store/next/projection.d.cts +1 -1
  68. package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
  69. package/dist/types-cjs/store/next/store.d.cts +52 -2
  70. package/dist/types-cjs/store/next/target.d.cts +73 -8
  71. package/package.json +2 -2
package/dist/dev.js CHANGED
@@ -42,6 +42,18 @@ class StatusError extends Error {
42
42
  function unwrapStatusError(error) {
43
43
  return error instanceof StatusError ? error.cause : error;
44
44
  }
45
+ /**
46
+ * Rejection value of `until(fn, { timeout })` when the predicate does not turn
47
+ * truthy within the window. Inside an `action()`, the rejection is thrown back
48
+ * in at the `yield` point — catchable there, or the action fails and its
49
+ * optimistic state reverts.
50
+ */
51
+ class TimeoutError extends Error {
52
+ constructor(message = "Timed out waiting for condition") {
53
+ super(message);
54
+ this.name = "TimeoutError";
55
+ }
56
+ }
45
57
  class NoOwnerError extends Error {
46
58
  constructor() {
47
59
  super("Context can only be accessed under a reactive root.");
@@ -117,6 +129,60 @@ const CONFIG_CHILD_COMPANIONS = 1 << 11;
117
129
  * moved into the cold extension (§12), and an unconditional `_x` deref per
118
130
  * marked node measurably taxed the propagation hot path (diamond -22%). */
119
131
  const CONFIG_FW_CHILDREN = 1 << 12;
132
+ /** Authoritative-view reader (`until()`): while this node computes, reads
133
+ * dodge active optimistic OVERRIDES only — the predicate must observe
134
+ * arriving truth, never the caller's own tentative writes (which would
135
+ * trivially satisfy it). Everything else reads normally, INCLUDING
136
+ * transition-staged `_pendingValue`: staged data is authoritative (optimism
137
+ * lives only in override slots), and a hold that refused staged reads would
138
+ * deadlock on data the open transaction itself is holding (a refresh the
139
+ * action issued lands staged and cannot commit until the hold releases).
140
+ * read() checks the bit on the reading computation (`context`) directly — no
141
+ * ambient flag — so a shared computed the predicate pulls recomputes as
142
+ * itself (no bit) under the normal view, and its cache never forks. */
143
+ const CONFIG_AUTHORITATIVE_READ = 1 << 13;
144
+ /** Sticky mark: an authoritative-view reader read this node PAST an active
145
+ * override. The ack shape — an authoritative arrival EQUAL to the override —
146
+ * rides paths that are deliberately silent under A17 (every ordinary reader
147
+ * sees the override, so an equal landing changes nothing for them). A marked
148
+ * node notifies those readers on such paths anyway, so the landed truth is
149
+ * seen without re-firing ordinary subscribers. Never cleared — only nodes an
150
+ * until() predicate observed mid-override pay. */
151
+ const CONFIG_AUTHORITATIVE_OBSERVED = 1 << 14;
152
+ /** Promise-delivery effect (resolve()/until()): commits its computed value
153
+ * directly even when recomputing under its own held transition. These
154
+ * effects deliver applies on a microtask (#2930) instead of the stashed
155
+ * effect queues, so the value must ride the same immediate schedule — a
156
+ * staged value with an immediate apply delivers stale state (resolve) or
157
+ * deadlocks the hold (until). Safe because the node is a private leaf: no
158
+ * subscriber reads an effect's value, only its own apply does. */
159
+ const CONFIG_DIRECT_COMMIT = 1 << 15;
160
+ /** Fresh-pull reader (awaitable `refresh()`'s waiter effect): a read of a
161
+ * dirty source recomputes it inline even when the height gate defers to the
162
+ * flush. Closes the same-flush ordering race where a waiter created
163
+ * alongside a refresh() mark read the PRE-re-ask value as settled and
164
+ * delivered stale; with the pull, the waiter either parks on the re-ask's
165
+ * pending window (async — woken by the settle walk, which runs on every
166
+ * landing including equal-value ones) or serves its sync answer. resolve()
167
+ * deliberately keeps that race — its contract is "first settled value"
168
+ * (#2930), not "next quiescent state". */
169
+ const CONFIG_FRESH_READ = 1 << 16;
170
+ /** HELD truth (#3164): this node's staged `_pendingValue` is confirming
171
+ * truth riding a transaction that retains optimism, revealed only at that
172
+ * transaction's settle. Two arming sites, one meaning: the store fold
173
+ * (a landing staged into the retaining transaction) and until()'s
174
+ * flip-entanglement (a foreign carrier's staged write, stolen when it
175
+ * flipped the awaited predicate truthy). Until the reveal, ordinary
176
+ * readers — lane and speculative recomputes included — keep committed:
177
+ * the staging notified subscribers as a plain write, so without the mask
178
+ * a mid-hold recompute composes live optimism with the confirming truth,
179
+ * a frame no timeline contains (GabbeV's union tear). Authoritative
180
+ * readers (until()'s predicate) and latest() tunnel through — the
181
+ * exemption that keeps holds deadlock-free. Override-covered nodes never
182
+ * arm: the override is their display and its revert their notification
183
+ * (A17). Cleared at commit (the commit IS the reveal); subscribers masked
184
+ * during the hold are woken by finalizePureQueue's post-revert pass. */
185
+ const CONFIG_HELD_TRUTH = 1 << 17;
120
186
  const STATUS_PENDING = 1 << 0;
121
187
  const STATUS_ERROR = 1 << 1;
122
188
  const STATUS_UNINITIALIZED = 1 << 2;
@@ -600,6 +666,18 @@ const engineHooks = {
600
666
  const totalMs = now() - frame.start;
601
667
  if (frames.length > 0) frames[frames.length - 1].childMs += totalMs;
602
668
  const selfMs = Math.max(0, totalMs - frame.childMs);
669
+ // Effect-output honesty: effects run with `_equals: false`, so core
670
+ // reports EVERY effect recompute as changed — which made effect waste
671
+ // invisible to costs() (and compiled JSX bindings are effects: the
672
+ // fan-out waste a naive selected-row produces is all effects). The
673
+ // engine re-derives the fact from its own snapshot: an identical
674
+ // committed compute output is an unchanged run. `undefined` outputs are
675
+ // exempt — a side-effect-only compute's work IS its effect phase, and
676
+ // identity of `undefined` proves nothing.
677
+ if (changed && frame.causes !== null && el._type) {
678
+ const committed = el._pendingValue !== NOT_PENDING ? el._pendingValue : el._value;
679
+ if (committed !== undefined && committed === frame.prevValue) changed = false;
680
+ }
603
681
  // Unstable-output check: memos only, non-create, plain runs with a
604
682
  // committed change. The fresh value sits in `_pendingValue` for held
605
683
  // plain-flush memo commits and in `_value` for direct ones. Overlay runs
@@ -708,11 +786,17 @@ const hooks = {};
708
786
  const diagnosticListeners = new Set();
709
787
  const diagnosticCaptures = new Set();
710
788
  let diagnosticSequence = 0;
789
+ let consoleFooter;
790
+ const footeredCodes = new Set();
711
791
  const diagnostics = {
712
792
  subscribe(listener) {
713
793
  diagnosticListeners.add(listener);
714
794
  return () => diagnosticListeners.delete(listener);
715
795
  },
796
+ setConsoleFooter(footer) {
797
+ consoleFooter = footer;
798
+ footeredCodes.clear();
799
+ },
716
800
  capture() {
717
801
  const events = [];
718
802
  diagnosticCaptures.add(events);
@@ -752,6 +836,13 @@ function emitDiagnostic(event) {
752
836
  };
753
837
  for (const listener of diagnosticListeners) listener(entry);
754
838
  for (const capture of diagnosticCaptures) capture.push(entry);
839
+ if (consoleFooter && !footeredCodes.has(entry.code)) {
840
+ footeredCodes.add(entry.code);
841
+ const footer = consoleFooter(entry);
842
+ // Call sites console.warn/error their message after emitDiagnostic
843
+ // returns; a microtask lands the footer right below that report.
844
+ if (footer) queueMicrotask(() => console.warn(footer));
845
+ }
755
846
  return entry;
756
847
  }
757
848
  /**
@@ -1097,7 +1188,7 @@ function cancelZombieRecompute(el) {
1097
1188
  }
1098
1189
  let clock = 0;
1099
1190
  let activeTransition = null;
1100
- let scheduled = false;
1191
+ let scheduled$1 = false;
1101
1192
  let halted = false;
1102
1193
  let haltNotified = false;
1103
1194
  let syncDepth = 0;
@@ -1207,6 +1298,27 @@ function mergeTransitionState(target, outgoing) {
1207
1298
  outgoing._affectsNodes.length = 0;
1208
1299
  }
1209
1300
  for (const store of outgoing._optimisticStores) target._optimisticStores.add(store);
1301
+ // Patch-channel stash (store/next/patch.ts): entries held for the outgoing
1302
+ // transition must ride the merge like every other per-transition
1303
+ // collection — releaseBatch only reads the COMMITTING transition's stash,
1304
+ // so a stranded sidecar would silently drop its patches. Move (don't
1305
+ // copy), same aliasing rule as the collections above. The field is an
1306
+ // expando so this module stays free of patch imports (pay-for-use).
1307
+ const heldPatches = outgoing._heldPatches;
1308
+ if (heldPatches !== undefined) {
1309
+ outgoing._heldPatches = undefined;
1310
+ let dest = target._heldPatches;
1311
+ if (dest !== undefined) dest.push(...heldPatches);
1312
+ else dest = target._heldPatches = heldPatches;
1313
+ // Retarget the entries' coalescing stamps to the surviving stash
1314
+ // (opaque backref contract with store/next/patch.ts): without this a
1315
+ // post-merge emission misses the stamp and pushes a SECOND entry —
1316
+ // the record's patch applies twice at commit (re-audit 5, P1-2).
1317
+ for (let i = 0; i < heldPatches.length; i++) {
1318
+ const pc = heldPatches[i].pc;
1319
+ if (pc !== undefined && pc.qe === heldPatches[i]) pc.qa = dest;
1320
+ }
1321
+ }
1210
1322
  for (const [source, reporters] of outgoing._asyncReporters) {
1211
1323
  let targetReporters = target._asyncReporters.get(source);
1212
1324
  if (!targetReporters) target._asyncReporters.set(source, (targetReporters = new Set()));
@@ -1214,13 +1326,114 @@ function mergeTransitionState(target, outgoing) {
1214
1326
  }
1215
1327
  for (const sub of outgoing._gatedSubs) target._gatedSubs.add(sub);
1216
1328
  }
1329
+ /**
1330
+ * Flip-entanglement (#3164 follow-up): `until()` is a declaration of
1331
+ * relatedness — the predicate names the condition that confirms the awaiting
1332
+ * transaction. When the predicate settles truthy, every live foreign
1333
+ * transition whose staged write it read IS the confirming event by the
1334
+ * user's own definition, so it merges into the awaiting transaction and
1335
+ * reveals at the joint settle — the cross-primitive twin of the family fold
1336
+ * (a landing on an optimism-carrying family joins the retaining
1337
+ * transaction). Non-flipping updates never pass through here: falsy
1338
+ * evaluations don't entangle, so unrelated traffic on the watched sources
1339
+ * reveals freely on its own schedule.
1340
+ *
1341
+ * Runs inside the predicate's compute (pure phase) — the confirming
1342
+ * transition's stamps are still live and its commit decision hasn't run, so
1343
+ * the merge lands before any reveal. Only the tree-shaken graphs that call
1344
+ * `until()` retain this.
1345
+ */
1346
+ function entangleConfirmingTransitions(obs, target) {
1347
+ target = currentTransition(target);
1348
+ if (target._done === true) return;
1349
+ // The confirming evidence is a dep whose value is STAGED (pending,
1350
+ // uncommitted) at flip evaluation — committed deps are public already and
1351
+ // carry nothing to entangle. A staged dep lives in one of two carriers: a
1352
+ // stamped transition, or the queue's current batch (ambient registrations
1353
+ // don't stamp; "ambient work IS a transaction" — the batch is the
1354
+ // carrier). The entangle STEALS the carrier's staged cargo — its pending
1355
+ // nodes move (re-stamped) into the awaiting transaction and reveal at its
1356
+ // settle — but never the carrier itself: its async reporters, actions,
1357
+ // and stashes are its own future (a live stream's flight must not chain
1358
+ // the awaiting transaction to landings that haven't happened; a merged
1359
+ // reporter deadlocked exactly that way).
1360
+ let stole = false;
1361
+ for (let l = obs._deps; l !== null; l = l._nextDep) {
1362
+ const dep = l._dep;
1363
+ if (dep._pendingValue !== NOT_PENDING) {
1364
+ const stamp = dep._transition;
1365
+ const t = stamp != null ? currentTransition(stamp) : null;
1366
+ // Skip the awaiting transaction's own cargo (t === target: a
1367
+ // fold-staged landing or a write the action itself issued — hold and
1368
+ // reveal already correct) and dead carriers. Ambient-batch staging
1369
+ // (t === null) must leave the batch NOW — it commits at this flush's
1370
+ // end, which would reveal the confirmation under the live optimism
1371
+ // it just confirmed.
1372
+ const carrier =
1373
+ t === null
1374
+ ? currentBatch._pendingNodes
1375
+ : t !== target && t._done !== true
1376
+ ? t._pendingNodes
1377
+ : null;
1378
+ if (carrier !== null) stole = stealEntangledCargo(carrier, target) || stole;
1379
+ }
1380
+ if (l === obs._depsTail) break;
1381
+ }
1382
+ // The steal never activates the awaiting transaction: the predicate can
1383
+ // flip inside another transaction's finalize heap, and adopting the queue
1384
+ // batch there hands the stolen cargo to that finalize's commit sweep — a
1385
+ // premature reveal at a foreign settle. Subscribers that computed against
1386
+ // the pre-steal world were re-dirtied by the steal itself, so this
1387
+ // flush's applies paint the masked (mid-hold) view; the cargo commits at
1388
+ // the awaiting transaction's own settle.
1389
+ }
1390
+ /** Move a confirming carrier's staged nodes into the awaiting transaction:
1391
+ * re-stamp and arm the held-truth mask (override-covered nodes skip it —
1392
+ * the override already hides their staged value per A17, and is usually
1393
+ * the very optimism this confirmation settles); the mask's commit
1394
+ * registers the settle-side post-revert wake. The carrier's array is
1395
+ * emptied so its own commit point commits none of the stolen cargo.
1396
+ *
1397
+ * EFFECT subs of stolen nodes re-run: any that recomputed against the
1398
+ * staging BEFORE the steal (the carrier's landing notified them as a plain
1399
+ * write) hold a private torn result — override composed with confirming
1400
+ * truth — that the next paint gate (stash-point lane run, a foreign
1401
+ * flush's completion drain) would show. Re-running them under the mask
1402
+ * re-derives the mid-hold view in this same heap pass. PURE computeds are
1403
+ * deliberately NOT re-run: a torn staged value of theirs is itself stolen
1404
+ * cargo — masked at read, so ordinary readers already serve their
1405
+ * committed value — while re-running them would re-derive the OLD world
1406
+ * and re-stage it over the held truth. The reveal re-notifies
1407
+ * (commitPendingNodes), which is when they re-derive for real. */
1408
+ function stealEntangledCargo(carrier, target) {
1409
+ if (carrier === target._pendingNodes || carrier.length === 0) return false;
1410
+ for (let i = 0; i < carrier.length; i++) {
1411
+ const node = carrier[i];
1412
+ node._transition = target;
1413
+ target._pendingNodes.push(node);
1414
+ // Override-covered nodes stay silent AND unmasked: the override is the
1415
+ // display (A17 — its staging never notified, its revert will), so their
1416
+ // subs saw nothing and re-running one would break the silence with a
1417
+ // duplicate fire of an unchanged view.
1418
+ if (!hasActiveOverride$1(node)) {
1419
+ node._config |= CONFIG_HELD_TRUTH;
1420
+ for (let s = node._subs; s !== null; s = s._nextSub) {
1421
+ const sub = s._sub;
1422
+ if (sub._type && !(sub._config & CONFIG_AUTHORITATIVE_READ)) enqueueSub(sub);
1423
+ }
1424
+ }
1425
+ }
1426
+ carrier.length = 0;
1427
+ transitions.add(target);
1428
+ return true;
1429
+ }
1217
1430
  function schedule() {
1218
1431
  if (halted) {
1219
1432
  notifyHalted();
1220
1433
  return;
1221
1434
  }
1222
- if (scheduled) return;
1223
- scheduled = true;
1435
+ if (scheduled$1) return;
1436
+ scheduled$1 = true;
1224
1437
  if (!syncDepth && !globalQueue._running && !projectionWriteActive) queueMicrotask(flush);
1225
1438
  }
1226
1439
  /**
@@ -1403,11 +1616,19 @@ class GlobalQueue extends Queue {
1403
1616
  static _transitionBlocked = null;
1404
1617
  static _cleanupLanes = null;
1405
1618
  static _runLaneEffects = null;
1619
+ /** Patch-channel optimistic drain (next/patch.ts): optimistic emissions
1620
+ * apply at lane-effect timing — visible in flight, unlike the regular
1621
+ * effect queues an action stashes. Injected; null when unused. */
1622
+ static _drainPatchOptimistic = null;
1406
1623
  static _gatedRead = null;
1407
1624
  static _laneSuspends = null;
1408
1625
  static _laneReadsCommitted = null;
1409
1626
  static _recomputeLane = null;
1410
1627
  static _laneAsyncPending = null;
1628
+ /** Authoritative-view reader wakeup (until()): installed at first until() call.
1629
+ * Call sites are gated by CONFIG_AUTHORITATIVE_OBSERVED, which only until()'s
1630
+ * carve-out read can set, so `!` invocations are safe once the gate holds. */
1631
+ static _notifyAuthoritativeObservers = null;
1411
1632
  static _laneAsyncSettled = null;
1412
1633
  static _trackOptimisticStore = null;
1413
1634
  flush() {
@@ -1415,6 +1636,10 @@ class GlobalQueue extends Queue {
1415
1636
  this._running = true;
1416
1637
  try {
1417
1638
  if (true) devCheckFlushStart();
1639
+ // Before runHeap for the same reason as the fast drain above; late
1640
+ // subscribers (an effect reading a swept memo this flush) revive it,
1641
+ // which is the pay-for-use contract.
1642
+ sweepDormant();
1418
1643
  runHeap(dirtyQueue, GlobalQueue._update);
1419
1644
  if (activeTransition) {
1420
1645
  const isComplete = transitionComplete(activeTransition);
@@ -1449,7 +1674,7 @@ class GlobalQueue extends Queue {
1449
1674
  // A kept ambient batch may hold pending nodes (#2916): stay
1450
1675
  // scheduled so the outer drain loop commits them via the plain
1451
1676
  // flush path instead of leaving them until the next natural flush.
1452
- scheduled = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
1677
+ scheduled$1 = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
1453
1678
  reassignPendingTransition(stashedTransition._pendingNodes);
1454
1679
  activeTransition = null;
1455
1680
  finalizePureQueue(null, true);
@@ -1489,7 +1714,7 @@ class GlobalQueue extends Queue {
1489
1714
  }
1490
1715
  clock++;
1491
1716
  // Check if finalization added items to the heap (from optimistic reversion)
1492
- scheduled = dirtyQueue._max >= dirtyQueue._min;
1717
+ scheduled$1 = dirtyQueue._max >= dirtyQueue._min;
1493
1718
  // Run lane effects first (for ready lanes), then regular effects
1494
1719
  activeLanes.size && GlobalQueue._runLaneEffects(EFFECT_RENDER);
1495
1720
  this.run(EFFECT_RENDER);
@@ -1506,7 +1731,7 @@ class GlobalQueue extends Queue {
1506
1731
  }
1507
1732
  if (
1508
1733
  true &&
1509
- !scheduled &&
1734
+ !scheduled$1 &&
1510
1735
  !activeTransition &&
1511
1736
  transitions.size === 0 &&
1512
1737
  activeLanes.size === 0
@@ -1547,8 +1772,17 @@ class GlobalQueue extends Queue {
1547
1772
  return false;
1548
1773
  }
1549
1774
  initTransition(transition) {
1550
- if (transition) transition = currentTransition(transition);
1551
- if (transition && transition === activeTransition) return;
1775
+ if (transition) {
1776
+ transition = currentTransition(transition);
1777
+ // A finished transaction cannot be re-entered: its state is committed
1778
+ // or reverted, so "rejoining" it (A26) is meaningless and re-activating
1779
+ // it spins the drain loop (#3140). The refusal must be a bare return —
1780
+ // redirecting the caller to a fresh batch would re-arm the loop with a
1781
+ // new transaction identity each pass. Stamps are cleared at commit, so
1782
+ // this is a belt for paths that hand over a chased-dead reference
1783
+ // (merged chains, async settles racing completion).
1784
+ if (transition._done === true || transition === activeTransition) return;
1785
+ }
1552
1786
  if (!transition && activeTransition && activeTransition._time === clock) return;
1553
1787
  if (!activeTransition) {
1554
1788
  activeTransition = transition ?? createBatch();
@@ -1592,11 +1826,25 @@ class GlobalQueue extends Queue {
1592
1826
  for (const lane of activeLanes) {
1593
1827
  if (!lane._transition) lane._transition = activeTransition;
1594
1828
  }
1829
+ // A transaction's ambient window is one flush. Entering must therefore
1830
+ // guarantee a flush: a transaction opened with no writes (an action whose
1831
+ // first statements only await) otherwise leaves activeTransition and the
1832
+ // adopted batch armed across the async gap, and the next unrelated work
1833
+ // to arrive — an optimistic store's authoritative landing, a plain async
1834
+ // settle — is adopted into a transaction it has nothing to do with
1835
+ // (#3141). The scheduled flush parks the incomplete transaction through
1836
+ // the normal machinery and detaches the ambient slots first.
1837
+ schedule();
1595
1838
  }
1596
1839
  }
1597
1840
  function queuePendingNode(node) {
1841
+ lastStagedNodeName = node._name ?? null;
1598
1842
  currentBatch._pendingNodes.push(node);
1599
1843
  }
1844
+ // Dev-only attribution for the flush loop guard (#3140): when the guard
1845
+ // trips, naming what the loop kept chewing on lets the app author attribute
1846
+ // the runaway without patching dist.
1847
+ let lastStagedNodeName = null;
1600
1848
  // Sticky: flips true on the first refresh() ever (the only setter of
1601
1849
  // REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
1602
1850
  // clear entirely in apps that never refresh.
@@ -1692,13 +1940,48 @@ let storeCommitHook = null;
1692
1940
  function setStoreCommitHook(fn) {
1693
1941
  storeCommitHook = fn;
1694
1942
  }
1943
+ /** Patch-channel release hook (next/patch.ts): transition-stamped patch
1944
+ * emissions are released when THEIR batch commits. Transitions never
1945
+ * abort: failed actions still commit (only optimistic overrides revert),
1946
+ * and merged-away transitions hand their stash to the survivor
1947
+ * (mergeTransitionState) — every stash drains exactly once. Injected like
1948
+ * storeCommitHook to stay tree-shakeable. */
1949
+ let patchCommitHook = null;
1950
+ function setPatchCommitHook(fn) {
1951
+ patchCommitHook = fn;
1952
+ }
1953
+ /** Held truth committed this finalize, awaiting its post-revert wake (see
1954
+ * finalizePureQueue): the commit IS the reveal, but subscribers must not
1955
+ * re-derive until the settling transaction's optimistic overrides have
1956
+ * reverted — a commit-time wake recomputes them in the window where
1957
+ * confirming truth is committed and the override still displays, a torn
1958
+ * frame no timeline contains. */
1959
+ const heldRevealed = [];
1695
1960
  function commitPendingNodes() {
1696
1961
  const pendingNodes = currentBatch._pendingNodes;
1697
1962
  for (let i = 0; i < pendingNodes.length; i++) {
1698
- commitPendingNode(pendingNodes[i]);
1963
+ const node = pendingNodes[i];
1964
+ commitPendingNode(node);
1965
+ // The stamp dies with the commit (#3143) — symmetric with
1966
+ // resolveOptimisticNodes clearing optimistic stamps. A stamp outliving
1967
+ // its transaction let any later write (even a value-equal no-op, which
1968
+ // re-opens before the equality bail) resurrect the finished transaction;
1969
+ // a boundary flag rewritten every finalize pass then spun the drain loop
1970
+ // forever (#3140). The held-truth mark dies the same death — the commit
1971
+ // IS the reveal — but its wake defers to the post-revert pass: ordinary
1972
+ // subscribers were masked to committed all hold (some re-derived against
1973
+ // that old view and cached it), and commits are otherwise silent
1974
+ // (staging already notified), so without a wake they'd hold the old
1975
+ // world forever.
1976
+ node._transition = null;
1977
+ if (node._config & CONFIG_HELD_TRUTH) {
1978
+ node._config &= ~CONFIG_HELD_TRUTH;
1979
+ heldRevealed.push(node);
1980
+ }
1699
1981
  }
1700
1982
  pendingNodes.length = 0;
1701
1983
  storeCommitHook?.();
1984
+ patchCommitHook?.(currentBatch);
1702
1985
  }
1703
1986
  function finalizePureQueue(completingTransition = null, incomplete = false) {
1704
1987
  // For incomplete transitions, skip pending resolution and optimistic reversion
@@ -1746,6 +2029,22 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
1746
2029
  // completing transition scopes the clear to its own layer keys (#2899).
1747
2030
  if (batch._optimisticStores.size)
1748
2031
  GlobalQueue._clearOptimisticStores(batch._optimisticStores, completingTransition);
2032
+ // Held-truth reveal wake (#3164), post-revert by construction: this
2033
+ // finalize committed confirming truth whose subscribers were masked all
2034
+ // hold — some re-derived against the committed view (the staging, or a
2035
+ // confirming carrier's landing, notified them as a plain write) and
2036
+ // cached it, and stash-restored applies may carry those torn values.
2037
+ // Waking and recomputing HERE — after _resolveOptimistic and the store
2038
+ // clears above — means every apply paints the settled view; a wake at
2039
+ // commit time would recompute them in the window where truth is
2040
+ // committed but the settling transaction's overrides still display.
2041
+ if (heldRevealed.length !== 0) {
2042
+ while (heldRevealed.length) insertSubs(heldRevealed.pop());
2043
+ if (dirtyQueue._max >= dirtyQueue._min) {
2044
+ runHeap(dirtyQueue, GlobalQueue._update);
2045
+ commitPendingNodes();
2046
+ }
2047
+ }
1749
2048
  sweepTransientStoreNodes();
1750
2049
  // Lanes only enter activeLanes through the engine's getOrCreateLane.
1751
2050
  if (activeLanes.size) GlobalQueue._cleanupLanes(completingTransition);
@@ -1825,8 +2124,21 @@ function flush(fn) {
1825
2124
  let count = 0;
1826
2125
  // `flush()` is an explicit drain point, so it must also process an active
1827
2126
  // transition even if no microtask was scheduled for it yet.
1828
- while (scheduled || activeTransition) {
1829
- if (++count === 1e5) throw new Error("Potential Infinite Loop Detected.");
2127
+ while (scheduled$1 || activeTransition) {
2128
+ if (++count === 1e5) {
2129
+ // Attribution beats a bare guard (#3140): say what kept the loop alive.
2130
+ // A completed transition being re-activated reads `done=true` here —
2131
+ // the corpse-revival signature — while application-driven runaways
2132
+ // (#2843) usually show staged work naming the culprit node.
2133
+ const t = activeTransition;
2134
+ throw new Error(
2135
+ `Potential Infinite Loop Detected. Kept alive by ${scheduled$1 ? "scheduled work" : "an active transition"}${
2136
+ t
2137
+ ? `; transition: done=${t._done === true}, pending=${t._pendingNodes.length}, optimistic=${t._optimisticNodes.length}, asyncReporters=${t._asyncReporters.size}`
2138
+ : ""
2139
+ }${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
2140
+ );
2141
+ }
1830
2142
  globalQueue.flush();
1831
2143
  }
1832
2144
  }
@@ -1888,6 +2200,29 @@ function runInTransition(transition, fn) {
1888
2200
  activeTransition = prevTransition;
1889
2201
  }
1890
2202
  }
2203
+ /** Run `fn` with `transition` as BOTH the ambient transaction and the
2204
+ * registration batch, restoring both after. runInTransition alone is not
2205
+ * enough for code that WRITES on behalf of a transaction from inside someone
2206
+ * else's window (optimistic replay re-arming a still-open action's edits
2207
+ * during a landing commit, #3123): registrations route through the queue's
2208
+ * batch pointer, and a bare activeTransition swap leaves them in the ambient
2209
+ * batch — a plain batch "completes" at the next flush and reverts optimistic
2210
+ * registrations that were supposed to live with the transaction.
2211
+ * initTransition is the wrong tool here: it MERGES the currently ambient
2212
+ * transaction into the target, entangling whatever the interrupted window
2213
+ * belonged to. */
2214
+ function runAsTransitionBatch(transition, fn) {
2215
+ const prevTransition = activeTransition;
2216
+ const prevBatch = globalQueue._batch;
2217
+ try {
2218
+ activeTransition = currentTransition(transition);
2219
+ currentBatch = globalQueue._batch = activeTransition;
2220
+ return fn();
2221
+ } finally {
2222
+ activeTransition = prevTransition;
2223
+ currentBatch = globalQueue._batch = prevBatch;
2224
+ }
2225
+ }
1891
2226
 
1892
2227
  /** The queue a node belongs to, picked from its own zombie flag. */
1893
2228
  function queueFor(n) {
@@ -2363,7 +2698,7 @@ function unlinkSubs(link) {
2363
2698
  // transition holding it) is an observer — tearing down would orphan
2364
2699
  // the work and re-execute it on the next read. The settle path runs
2365
2700
  // this same last-one-out check when that observer releases (the
2366
- // untracked-read dispose in core.ts guards on pending identically).
2701
+ // untracked-read dormancy sweep guards on pending identically).
2367
2702
  const c = dep;
2368
2703
  c._fn &&
2369
2704
  c._config & CONFIG_AUTO_DISPOSE &&
@@ -2403,6 +2738,46 @@ function unobserved(el) {
2403
2738
  clearDeps(el);
2404
2739
  disposeChildren(el, true);
2405
2740
  }
2741
+ /**
2742
+ * Deferred dormancy for never-observed auto-dispose computeds (#3078).
2743
+ *
2744
+ * An untracked top-level read of a subscriber-less observation-lifecycle memo
2745
+ * used to call unobserved() inline at the end of read(). That kept the leak
2746
+ * closed (the compute links the memo into its deps' sub lists — without a
2747
+ * teardown point a never-observed memo is retained by its sources forever;
2748
+ * upstream alien-signals has exactly this retention), but it made reads
2749
+ * destructive: each read disposed the node, the next read revived it with a
2750
+ * full recompute in whatever ambient transition/lane context happened to be
2751
+ * current, so consecutive reads could return different answers with no write
2752
+ * in between.
2753
+ *
2754
+ * Instead, reads queue the node here and the scheduler sweeps at the top of
2755
+ * the next flush (before runHeap, so a same-tick dirtying is reclaimed
2756
+ * instead of recomputed). Reads become idempotent within a tick (the node
2757
+ * stays alive and serves its cache, uniform with observed memos) while
2758
+ * reclamation still happens within one microtask — the enqueue site arms
2759
+ * schedule(), so a flush is guaranteed even when no other work is queued.
2760
+ */
2761
+ const dormantNodes = new Set();
2762
+ function sweepDormant() {
2763
+ if (dormantNodes.size === 0) return;
2764
+ for (const el of dormantNodes) {
2765
+ // Re-validate at sweep time: the node may have gained a subscriber (its
2766
+ // lifecycle is the unlinkSubs cascade now), gone pending (in-flight async
2767
+ // is an observer; the settle path re-runs last-one-out), lost its
2768
+ // AUTO_DISPOSE bit (owner teardown strips it, #3024), or already been
2769
+ // torn down.
2770
+ if (
2771
+ !el._subs &&
2772
+ el._config & CONFIG_AUTO_DISPOSE &&
2773
+ !(el._statusFlags & STATUS_PENDING) &&
2774
+ !(el._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
2775
+ ) {
2776
+ unobserved(el);
2777
+ }
2778
+ }
2779
+ dormantNodes.clear();
2780
+ }
2406
2781
  // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
2407
2782
  function link(dep, sub, pendingObserver = false) {
2408
2783
  // Repeat touches within one pass AND-combine `_pendingObserver`: a probe
@@ -2616,11 +2991,11 @@ function settlePendingSource(el) {
2616
2991
  const errored = node._statusFlags & STATUS_ERROR;
2617
2992
  if (remaining) {
2618
2993
  if (!errored) setPendingError(node, remaining);
2619
- updateCompanions !== null && updateCompanions(node);
2994
+ updateCompanions?.(node);
2620
2995
  } else {
2621
2996
  node._statusFlags &= ~STATUS_PENDING;
2622
2997
  if (!errored) setPendingError(node);
2623
- updateCompanions !== null && updateCompanions(node);
2998
+ updateCompanions?.(node);
2624
2999
  if (node._x?._blocked) {
2625
3000
  enqueueSub(node);
2626
3001
  scheduled = true;
@@ -2643,6 +3018,14 @@ function settlePendingSource(el) {
2643
3018
  function isThenable(value) {
2644
3019
  return value != null && typeof value === "object" && typeof value.then === "function";
2645
3020
  }
3021
+ /** Fire and clear a node's iterator-flight cancellation hook (#3122). */
3022
+ function releaseFlightTeardown(el) {
3023
+ const teardown = el._x?._flightTeardown;
3024
+ if (teardown != null) {
3025
+ el._x._flightTeardown = null;
3026
+ teardown();
3027
+ }
3028
+ }
2646
3029
  function handleAsync(el, result, setter) {
2647
3030
  let iterator = false;
2648
3031
  let thenable = false;
@@ -2680,6 +3063,11 @@ function handleAsync(el, result, setter) {
2680
3063
  });
2681
3064
  throw new Error(message);
2682
3065
  }
3066
+ // Flight replacement relies on recompute's supersede release for iterator
3067
+ // teardown (#3122): every handleAsync call — including the projection
3068
+ // self-registration — runs during a recompute of `el`, which has already
3069
+ // fired _flightTeardown. A future non-recompute registration path must
3070
+ // release it here before overwriting _inFlight.
2683
3071
  ext(el)._inFlight = result;
2684
3072
  let syncValue;
2685
3073
  // Settle-time transition re-entry. The loading rail is invisible to
@@ -2783,10 +3171,21 @@ function handleAsync(el, result, setter) {
2783
3171
  // only notified when the hold is visible to them: under an active
2784
3172
  // override every reader sees the override (A17), so waking subs would
2785
3173
  // re-show an unchanged view — the revert is the notification point.
2786
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, value);
3174
+ GlobalQueue._syncCompanions?.(el, value);
2787
3175
  if (!hasActiveOverride$1(el)) {
2788
3176
  if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
2789
3177
  insertSubs(el);
3178
+ } else if (el._config & CONFIG_AUTHORITATIVE_OBSERVED) {
3179
+ // A17 silence is stated over ordinary readers; an authoritative-view
3180
+ // reader (until()'s predicate) observed this node PAST its override
3181
+ // and is waiting for exactly this staged truth. Without the wake the
3182
+ // hold deadlocks: the landing waits on the transaction, the
3183
+ // transaction on the action, the action on an until() that was never
3184
+ // re-notified (#3164). Same selective wake as the equal-landing
3185
+ // branch in recompute(). Optional call: the bit implies the
3186
+ // optimistic engine WAS consulted, but the hook only installs with
3187
+ // it — a bare-core build must not crash here.
3188
+ GlobalQueue._notifyAuthoritativeObservers?.(el);
2790
3189
  }
2791
3190
  el._time = clock;
2792
3191
  } else if (lane) {
@@ -2801,7 +3200,7 @@ function handleAsync(el, result, setter) {
2801
3200
  // The latest() shadow write gives latest() effects independent lanes; the
2802
3201
  // _pendingSignal update is a no-op repeat of the clearStatus() call above
2803
3202
  // (computePendingState doesn't read _value).
2804
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, value);
3203
+ GlobalQueue._syncCompanions?.(el, value);
2805
3204
  insertSubs(el, true);
2806
3205
  }
2807
3206
  } catch (e) {
@@ -2883,6 +3282,11 @@ function handleAsync(el, result, setter) {
2883
3282
  } catch {}
2884
3283
  };
2885
3284
  registerClose ? registerClose(close) : cleanup(close);
3285
+ // Flight-identity cancellation (#3122): the registration above is the
3286
+ // owner-death backstop, but its disposal list can be zombie-deferred
3287
+ // until the SUPERSEDING flight settles. The teardown slot fires at the
3288
+ // _inFlight release sites so supersede stops this stream immediately.
3289
+ ext(el)._flightTeardown = close;
2886
3290
  // Release check before each next pull: an unobserved lazy node must tear
2887
3291
  // down (its close above runs via disposal, closing the iterator) instead
2888
3292
  // of pumping the stream forever with zero subscribers (#2935).
@@ -3084,7 +3488,8 @@ function clearStatus(el, clearUninitialized = false) {
3084
3488
  GlobalQueue._updateChildCompanions !== null
3085
3489
  )
3086
3490
  GlobalQueue._updateChildCompanions(el);
3087
- if (el._x?._notifyStatus) el._x._notifyStatus.call(el);
3491
+ const notify = statusNotifierOf(el);
3492
+ if (notify) notify.call(el);
3088
3493
  }
3089
3494
  function notifyStatus(el, status, error, blockStatus, lane) {
3090
3495
  // Wrap regular errors to track source node
@@ -3113,7 +3518,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3113
3518
  status | (status !== STATUS_ERROR ? el._statusFlags & STATUS_UNINITIALIZED : 0);
3114
3519
  ext(el)._error = error;
3115
3520
  }
3116
- GlobalQueue._updatePendingSignal !== null && GlobalQueue._updatePendingSignal(el);
3521
+ GlobalQueue._updatePendingSignal?.(el);
3117
3522
  if (
3118
3523
  el._x?._child &&
3119
3524
  el._config & CONFIG_CHILD_COMPANIONS &&
@@ -3126,14 +3531,15 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3126
3531
  }
3127
3532
  const downstreamBlockStatus = blockStatus || startsBlocking;
3128
3533
  const downstreamLane = blockStatus || isOptimisticBoundary ? undefined : lane;
3129
- if (el._x?._notifyStatus) {
3534
+ const elNotify = statusNotifierOf(el);
3535
+ if (elNotify) {
3130
3536
  if (blockStatus && status === STATUS_PENDING) {
3131
3537
  return;
3132
3538
  }
3133
3539
  if (downstreamBlockStatus) {
3134
- el._x._notifyStatus.call(el, status, error);
3540
+ elNotify.call(el, status, error);
3135
3541
  } else {
3136
- el._x._notifyStatus.call(el);
3542
+ elNotify.call(el);
3137
3543
  }
3138
3544
  return;
3139
3545
  }
@@ -3257,7 +3663,15 @@ function recompute(el, create = false) {
3257
3663
  if (el._transition && (!isEffect || activeTransition) && activeTransition !== el._transition)
3258
3664
  globalQueue.initTransition(el._transition);
3259
3665
  deleteFromHeap(el, queueFor(el));
3260
- if (el._x !== null) el._x._inFlight = null;
3666
+ if (el._x !== null) {
3667
+ el._x._inFlight = null;
3668
+ // Supersede is where an iterator flight dies (#3122): close it now.
3669
+ // Its cleanup(close) registration may sit in a zombie-deferred
3670
+ // disposal list that a held transition only drains when the
3671
+ // SUPERSEDING flight settles — cancellation must not wait for the
3672
+ // work that replaced it. Idempotent with the cleanup-channel close.
3673
+ releaseFlightTeardown(el);
3674
+ }
3261
3675
  // Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring
3262
3676
  if (el._transition || isEffect === EFFECT_TRACKED) disposeChildren(el);
3263
3677
  else if (el._firstChild !== null || el._disposal !== null) {
@@ -3281,6 +3695,14 @@ function recompute(el, create = false) {
3281
3695
  // recovers to an unchanged value, dependents still holding this object must
3282
3696
  // be swept (settleErroredDependents, #2949).
3283
3697
  const outgoingError = el._statusFlags & STATUS_ERROR ? el._x?._error : undefined;
3698
+ // Pending SOURCE-hood, captured before the compute clears status: a node
3699
+ // whose own flight parked dependents self-registers in _pendingSources
3700
+ // (notifyStatus, isSource). If this recompute supersedes that flight and
3701
+ // settles synchronously, those dependents settle HERE — asyncWrite's
3702
+ // settlePendingSource walk never runs for a landing that was preempted
3703
+ // (#3181).
3704
+ const wasPendingSource =
3705
+ (el._statusFlags & STATUS_PENDING) !== 0 && el._x?._pendingSources?.has(el) === true;
3284
3706
  // Re-ask classification lives in the verdict module; capture the flag before
3285
3707
  // the recompute wipes _flags below.
3286
3708
  const hadReask = (el._flags & REACTIVE_REASK) !== 0;
@@ -3455,7 +3877,14 @@ function recompute(el, create = false) {
3455
3877
  // values directly — the pending round-trip (queuePendingNode +
3456
3878
  // commitPendingNodes) exists to sequence transition reveals, and
3457
3879
  // paying it per effect on the plain path is pure overhead.
3458
- (isEffect && (activeTransition !== el._transition || activeTransition === null)) ||
3880
+ // DIRECT_COMMIT effects (resolve/until) commit directly even under
3881
+ // their own held transition: their applies deliver on a microtask,
3882
+ // not the stashed queues, so a staged value would hand the immediate
3883
+ // apply stale state — see CONFIG_DIRECT_COMMIT.
3884
+ (isEffect &&
3885
+ (activeTransition !== el._transition ||
3886
+ activeTransition === null ||
3887
+ el._config & CONFIG_DIRECT_COMMIT)) ||
3459
3888
  isOptimisticDirty
3460
3889
  // NOTE (stage-3, 2026-08-21): a quiet-world MEMO direct-commit was
3461
3890
  // attempted here and REVERTED — memo staging is load-bearing beyond
@@ -3501,6 +3930,12 @@ function recompute(el, create = false) {
3501
3930
  if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
3502
3931
  el._pendingValue = value;
3503
3932
  if (wasLoading) el._loading = true; // see the held branch above (#2990)
3933
+ // A authoritative-view reader (until()) observed this node past its
3934
+ // override — and "authoritative arrival equal to the override" is
3935
+ // exactly the acknowledgment it waits for. Wake those readers only;
3936
+ // A17 silence holds for every ordinary subscriber. (Hook installed by
3937
+ // until(), the only setter of the gating bit.)
3938
+ if (el._config & CONFIG_AUTHORITATIVE_OBSERVED) GlobalQueue._notifyAuthoritativeObservers(el);
3504
3939
  } else if (el._height != oldHeight) {
3505
3940
  for (let s = el._subs; s !== null; s = s._nextSub) {
3506
3941
  insertIntoHeapHeight(s._sub, queueFor(s._sub));
@@ -3513,6 +3948,17 @@ function recompute(el, create = false) {
3513
3948
  // (el._x?._error re-set), so this only runs on a genuinely clean recovery.
3514
3949
  if (outgoingError !== undefined && !valueChanged && !el._x?._error)
3515
3950
  settleErroredDependents(el, outgoingError);
3951
+ // Pending twin of the sweep above (#3181): a flight superseded by this
3952
+ // synchronous settle leaves every registered dependent still flagged
3953
+ // STATUS_PENDING with a pending source that will never land — asyncWrite
3954
+ // owns the walk only for flights that actually land. An unchanged value
3955
+ // is the dangerous shape (a projection reconciling in place: a memo over
3956
+ // it re-throws its cached NotReadyError forever, and every reader that
3957
+ // suspended through that memo re-parks on the dead source), but the walk
3958
+ // runs on the changed shape too, exactly as the landing path does —
3959
+ // insertSubs notifies value SUBSCRIBERS, not pending REGISTRANTS, and
3960
+ // the two sets only partially overlap.
3961
+ if (wasPendingSource && !(el._statusFlags & STATUS_PENDING)) settlePendingSource(el);
3516
3962
  }
3517
3963
  // Attribution hook: fired before the lane restore so `currentOptimisticLane`
3518
3964
  // still reflects THIS run's posture. The facts distinguish an overlay
@@ -3561,7 +4007,9 @@ function updateIfNecessary(el) {
3561
4007
  // (_depsTail/_depGen) is live, and a nested recompute would corrupt it.
3562
4008
  // A mid-pass mark stays latched for recompute's own tail to reschedule
3563
4009
  // (#3037); readers meanwhile serve the values the pass has so far.
3564
- if (el._flags & REACTIVE_RECOMPUTING_DEPS) return;
4010
+ // Never recompute a DISPOSED node either: recompute rewrites _flags and
4011
+ // would resurrect it (#2983) — readers serve its last value.
4012
+ if (el._flags & (REACTIVE_RECOMPUTING_DEPS | REACTIVE_DISPOSED)) return;
3565
4013
  if (el._flags & REACTIVE_CHECK) {
3566
4014
  for (let d = el._deps; d; d = d._nextDep) {
3567
4015
  const dep1 = d._dep;
@@ -3597,7 +4045,7 @@ function computed(fn, options) {
3597
4045
  (options?.sync ? CONFIG_SYNC : 0) |
3598
4046
  (options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0) |
3599
4047
  (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
3600
- _equals: options?.equals != null ? options.equals : isEqual,
4048
+ _equals: options?.equals ?? isEqual,
3601
4049
  _disposal: null,
3602
4050
  _queue: context?._queue ?? globalQueue,
3603
4051
  _context: context?._context ?? defaultContext,
@@ -3649,6 +4097,7 @@ function ext(el) {
3649
4097
  _parentSource: undefined,
3650
4098
  _affectsCount: 0,
3651
4099
  _inFlight: null,
4100
+ _flightTeardown: null,
3652
4101
  _error: undefined,
3653
4102
  _blocked: undefined,
3654
4103
  _pendingSources: undefined,
@@ -3668,7 +4117,7 @@ function ext(el) {
3668
4117
  * mode (recompute is called explicitly by `effect()`), so we hardcode the lazy bits and skip
3669
4118
  * the auto-dispose CONFIG bit (effect() previously cleared it post-construction).
3670
4119
  */
3671
- function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
4120
+ function createEffectNode(fn, effectFn, errorFn, type, options) {
3672
4121
  const transparent = options?.transparent ?? false;
3673
4122
  const self = {
3674
4123
  id: inheritId(options, transparent, context),
@@ -3676,6 +4125,7 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
3676
4125
  (transparent ? CONFIG_TRANSPARENT : 0) |
3677
4126
  (options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
3678
4127
  (options?.sync ? CONFIG_SYNC : 0) |
4128
+ (options?._extraConfig ?? 0) |
3679
4129
  (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
3680
4130
  _equals: false,
3681
4131
  _disposal: null,
@@ -3712,12 +4162,35 @@ function createEffectNode(fn, effectFn, errorFn, type, notifyStatus, options) {
3712
4162
  _x: null
3713
4163
  };
3714
4164
  self._name = options?.name ?? "effect";
3715
- // Boundary effects carry a status channel; most effects never touch _x.
3716
- if (notifyStatus !== undefined) ext(self)._notifyStatus = notifyStatus;
4165
+ // Effects dispatch status through the SHARED notifier (statusNotifierOf,
4166
+ // keyed off _type) storing it per node forced a full NodeExtension
4167
+ // allocation on EVERY effect at creation (an alloc + 19 field stores,
4168
+ // +23% effect creation, caught by the creation benches). Only genuinely
4169
+ // per-node channels (boundaries) live on _x.
3717
4170
  if (options?.unobserved) ext(self)._unobserved = options.unobserved;
3718
4171
  setupComputedNode(self, lazyOptions);
3719
4172
  return self;
3720
4173
  }
4174
+ /**
4175
+ * The shared status notifier for effect nodes, installed once by effect.ts
4176
+ * at module evaluation (`this`-dispatched — one function serves every
4177
+ * effect, so nodes never store it). Boundary computeds keep their own
4178
+ * per-node channel on `_x._notifyStatus`, which takes precedence.
4179
+ */
4180
+ let effectStatusNotify = null;
4181
+ function setEffectStatusNotify(fn) {
4182
+ effectStatusNotify = fn;
4183
+ }
4184
+ /** Resolve a node's status notifier: an own `_x` channel (boundaries) wins;
4185
+ * effect nodes (`_type` — EFFECT_PURE is 0, and only effect literals carry
4186
+ * the field) fall back to the shared notifier. Presence doubles as the
4187
+ * "display consumer" membership test in the status walks, exactly as the
4188
+ * per-node field did when every effect carried one. */
4189
+ function statusNotifierOf(el) {
4190
+ const own = el._x?._notifyStatus;
4191
+ if (own !== undefined) return own;
4192
+ return el._type ? (effectStatusNotify ?? undefined) : undefined;
4193
+ }
3721
4194
  const lazyOptions = { lazy: true };
3722
4195
  function setupComputedNode(self, options) {
3723
4196
  self._prevHeap = self;
@@ -3757,7 +4230,7 @@ function setupComputedNode(self, options) {
3757
4230
  }
3758
4231
  function signal(v, options, firewall = null) {
3759
4232
  const s = {
3760
- _equals: options?.equals != null ? options.equals : isEqual,
4233
+ _equals: options?.equals ?? isEqual,
3761
4234
  _config:
3762
4235
  (options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
3763
4236
  (options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0),
@@ -3894,6 +4367,35 @@ const READ_SLOW = Symbol("read-slow");
3894
4367
  * snapshot / transition / lane / dev-strictRead state all take the full
3895
4368
  * resolution. Anything slow returns READ_SLOW; the caller then calls read().
3896
4369
  */
4370
+ /**
4371
+ * Wake only authoritative-view readers (until() predicates) subscribed to `el`.
4372
+ * The A17-silent ack paths — an authoritative arrival equal to the active
4373
+ * override — use this so the predicate re-evaluates without re-firing
4374
+ * ordinary subscribers whose visible (override) value did not change.
4375
+ * Pay-for-use: reached through GlobalQueue._notifyAuthoritativeObservers,
4376
+ * installed at first until() call — apps that never use until() shake it.
4377
+ */
4378
+ function notifyAuthoritativeObservers(el) {
4379
+ for (let s = el._subs; s !== null; s = s._nextSub) {
4380
+ const sub = s._sub;
4381
+ if (!(sub._config & CONFIG_AUTHORITATIVE_READ)) continue;
4382
+ // Missed-wake latch (#3037), same contract as insertSubs: the reader may
4383
+ // itself have pulled this recompute (updateIfNecessary from its own
4384
+ // read), and the heap refuses RECOMPUTING nodes — latch so recompute's
4385
+ // tail reschedules it with the staged value visible.
4386
+ if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
4387
+ sub._flags |= REACTIVE_MISSED_WAKE;
4388
+ enqueueSub(sub);
4389
+ }
4390
+ schedule();
4391
+ }
4392
+ /** Installs the until() machinery hook. Idempotent; called by until() before
4393
+ * any authoritative-view read happens (same late-binding contract as the
4394
+ * optimistic engine). */
4395
+ function installAuthoritativeRead() {
4396
+ if (GlobalQueue._notifyAuthoritativeObservers === null)
4397
+ GlobalQueue._notifyAuthoritativeObservers = notifyAuthoritativeObservers;
4398
+ }
3897
4399
  function readNodeFast(el) {
3898
4400
  if (
3899
4401
  latestReadActive ||
@@ -3973,6 +4475,13 @@ function read(el) {
3973
4475
  markHeap(elQueue);
3974
4476
  updateIfNecessary(owner);
3975
4477
  }
4478
+ // Fresh-pull readers (awaitable refresh's waiter) recompute a dirty
4479
+ // source inline even when the height gate defers to the flush: the
4480
+ // waiter must park on the re-ask's window (or serve its sync answer),
4481
+ // never read the PRE-re-ask value as settled. Self-guarded: a clean
4482
+ // node no-ops and updateIfNecessary refuses disposed nodes (#2983) —
4483
+ // a dead target serves its last value, which is already quiescent.
4484
+ else if (c._config & CONFIG_FRESH_READ) updateIfNecessary(owner);
3976
4485
  const height = owner._height;
3977
4486
  // parent check is shallow, might need to be recursive
3978
4487
  if (height >= c._height && el._parent !== c) {
@@ -4051,8 +4560,18 @@ function read(el) {
4051
4560
  nodeName: owner?._name
4052
4561
  });
4053
4562
  if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
4054
- // A17: the override IS the value for every reader.
4055
- return unwrapOverride(el._x?._overrideValue);
4563
+ // A17: the override IS the value for every reader — except an authoritative
4564
+ // reader (until()'s predicate carries CONFIG_AUTHORITATIVE_READ): it must
4565
+ // observe independently-arriving truth, and serving it the caller's own
4566
+ // tentative write would trivially satisfy the predicate. The bit is checked
4567
+ // on the reading computation itself, so a shared computed the predicate
4568
+ // pulls recomputes as ITSELF (context = the memo, no bit) under the normal
4569
+ // view. Fall through to normal value selection (staged `_pendingValue` is
4570
+ // authoritative — optimism never lives there); the sticky mark makes the
4571
+ // A17-silent "landing equals override" paths notify this node's subs so
4572
+ // the reader re-runs when truth arrives.
4573
+ if (!(c && c._config & CONFIG_AUTHORITATIVE_READ)) return unwrapOverride(el._x?._overrideValue);
4574
+ el._config |= CONFIG_AUTHORITATIVE_OBSERVED;
4056
4575
  }
4057
4576
  // Entanglement gate: a reader recomputing under an optimistic lane that reads
4058
4577
  // a pending mid-transition write sees the committed value. Projection-store
@@ -4080,7 +4599,18 @@ function read(el) {
4080
4599
  (currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
4081
4600
  el._pendingValue === NOT_PENDING ||
4082
4601
  c._config & CONFIG_CHILDREN_FORBIDDEN ||
4083
- (stale && el._transition && activeTransition !== el._transition)
4602
+ (stale && el._transition && activeTransition !== el._transition) ||
4603
+ // A17 for HELD truth (#3164, see CONFIG_HELD_TRUTH): staged confirming
4604
+ // truth — fold-staged onto an armed family, or entangle-stolen by an
4605
+ // awaited until() — is masked from ordinary readers until its
4606
+ // transaction's reveal; the retaining transaction's own speculative
4607
+ // recomputes included (partial override coverage would otherwise
4608
+ // compose override + staged truth into a state no timeline contains).
4609
+ // Authoritative readers (until()'s predicate) and latest() see the
4610
+ // staged truth — the tunnel that keeps the hold deadlock-free.
4611
+ (el._config & CONFIG_HELD_TRUTH &&
4612
+ !latestReadActive &&
4613
+ !(c._config & CONFIG_AUTHORITATIVE_READ))
4084
4614
  ? el._value
4085
4615
  : el._pendingValue;
4086
4616
  // Record that this isPending() probe observed the fresh pending value, so
@@ -4094,7 +4624,14 @@ function read(el) {
4094
4624
  !(owner._statusFlags & STATUS_PENDING) &&
4095
4625
  !el._subs
4096
4626
  ) {
4097
- unobserved(el);
4627
+ // Deferred, not inline (#3078): an inline unobserved() here made untracked
4628
+ // reads destructive — dispose on this read, full revival recompute on the
4629
+ // next — so consecutive reads could answer differently with no write in
4630
+ // between (the revival samples the ambient transition/lane context).
4631
+ // The sweep at flush finalization re-validates and reclaims; schedule()
4632
+ // guarantees that flush happens even if nothing else is queued.
4633
+ dormantNodes.add(el);
4634
+ schedule();
4098
4635
  }
4099
4636
  return value;
4100
4637
  }
@@ -4118,9 +4655,17 @@ function devGuardStoreSetterWrite() {
4118
4655
  ownerName: context._name,
4119
4656
  data: { operation: "setStore" }
4120
4657
  });
4121
- throw new Error(REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE);
4658
+ // the owner name reaches the THROWN message too, not just the
4659
+ // diagnostics channel apps don't subscribe to by default (#3157)
4660
+ throw new Error(ownedScopeWriteMessage(context));
4122
4661
  }
4123
4662
  }
4663
+ function ownedScopeWriteMessage(owner) {
4664
+ const name = owner._name;
4665
+ return name
4666
+ ? `${REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE} (in ${name})`
4667
+ : REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE;
4668
+ }
4124
4669
  function setSignal(el, v) {
4125
4670
  if (
4126
4671
  !(el._config & CONFIG_OWNED_WRITE) &&
@@ -4138,7 +4683,7 @@ function setSignal(el, v) {
4138
4683
  nodeName: el._name,
4139
4684
  data: { operation: "setSignal" }
4140
4685
  });
4141
- throw new Error(REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE);
4686
+ throw new Error(ownedScopeWriteMessage(context));
4142
4687
  }
4143
4688
  if (el._transition && activeTransition !== el._transition)
4144
4689
  globalQueue.initTransition(el._transition);
@@ -4264,41 +4809,13 @@ function staleValues(fn, set = true) {
4264
4809
  }
4265
4810
  }
4266
4811
  /**
4267
- * Invalidates one reactive source, forcing it to re-execute even if its inputs
4268
- * haven't changed.
4269
- *
4270
- * Pass either a Solid-created accessor or a projected store created from
4271
- * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
4272
- * write-like invalidation operation: it does not read the target's value, and
4273
- * refreshing a plain signal accessor is a no-op.
4274
- *
4275
- * Use it to invalidate cached async values (e.g. force a re-fetch) without
4276
- * tearing the consumer down.
4277
- *
4278
- * @example
4279
- * ```ts
4280
- * const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
4281
- *
4282
- * // Re-fetch on demand
4283
- * <button onClick={() => refresh(user)}>Reload</button>
4284
- * ```
4812
+ * Core marking half of `refresh()` (the public wrapper lives in signals.ts
4813
+ * it validates the target, marks through here, then builds the quiescence
4814
+ * promise on the resolve()/until() effect machinery). Flags the node's next
4815
+ * recompute as a quiet re-ask and schedules it; no-ops for non-derived or
4816
+ * disposed targets and for same-tick manual writes.
4285
4817
  */
4286
- function refresh(target) {
4287
- const node = target?.[$REFRESH];
4288
- if (!node) {
4289
- {
4290
- const message =
4291
- "[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
4292
- "Pass the original source target, not a wrapper function or derived property read.";
4293
- emitDiagnostic({
4294
- code: "INVALID_REFRESH_TARGET",
4295
- kind: "write",
4296
- severity: "error",
4297
- message
4298
- });
4299
- throw new Error(message);
4300
- }
4301
- }
4818
+ function markRefresh(node) {
4302
4819
  if (
4303
4820
  context &&
4304
4821
  !((node._config ?? 0) & CONFIG_OWNED_WRITE) &&
@@ -4617,6 +5134,9 @@ function runLaneEffects(type) {
4617
5134
  runQueue(effects, type);
4618
5135
  }
4619
5136
  }
5137
+ // Optimistic patch applications ride the same visibility slot as lane
5138
+ // effects (in-flight DOM updates); no-op unless patches registered.
5139
+ if (type === EFFECT_RENDER) GlobalQueue._drainPatchOptimistic?.();
4620
5140
  }
4621
5141
  function cleanupCompletedLanes(completingTransition) {
4622
5142
  for (const lane of activeLanes) {
@@ -5107,7 +5627,22 @@ function latestRead(el) {
5107
5627
  !(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
5108
5628
  ) {
5109
5629
  markHeap(queue);
5110
- prepareComputed(pendingComputed, true);
5630
+ // Suspend probe collection during the pull (mirrors pendingCheckRead's
5631
+ // prepare): a probe through latest() answers for the SHADOW — the
5632
+ // read() dispatch collects it deliberately, so the verdict reflects
5633
+ // async still in flight for the latest view, not the parent's held
5634
+ // write. A stale shadow recomputing HERE ran its `read(parent)` with
5635
+ // the probe still live and collected the parent too, so the verdict
5636
+ // depended on whether anything had pulled the shadow current earlier
5637
+ // in the tick (#3104: reading latest(m) flipped a later
5638
+ // latest(() => isPending(x)) from true to false).
5639
+ const prevCheck = pendingCheckActive;
5640
+ setPendingCheckActive(false);
5641
+ try {
5642
+ prepareComputed(pendingComputed, true);
5643
+ } finally {
5644
+ setPendingCheckActive(prevCheck);
5645
+ }
5111
5646
  }
5112
5647
  value = read(pendingComputed);
5113
5648
  } catch (e) {
@@ -5137,12 +5672,42 @@ function latestRead(el) {
5137
5672
  return pendingComputed._pendingValue;
5138
5673
  return value;
5139
5674
  }
5675
+ /**
5676
+ * A latest() shadow that is uninitialized only because it was CREATED during
5677
+ * an active flight — its parent source already has a committed value, so
5678
+ * latest() serves that as the visible value and a tracked reader has
5679
+ * something to pair a verdict with (#3166). Same parent resolution as
5680
+ * computePendingState. The pending signal companion also carries
5681
+ * `_parentSource` but is a plain signal (no `_fn`) that never goes pending,
5682
+ * so the `_fn` check is belt-and-braces for this call site.
5683
+ */
5684
+ function latestShadowWithInitializedParent(owner) {
5685
+ if (typeof owner._fn !== "function") return false;
5686
+ const parentNode = owner._x?._parentSource;
5687
+ if (parentNode === undefined) return false;
5688
+ const parent = parentNode._firewall || parentNode;
5689
+ return !(parent._statusFlags & STATUS_UNINITIALIZED);
5690
+ }
5140
5691
  /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
5141
5692
  function pendingCheckRead(el, c, owner, firewall) {
5142
5693
  setPendingCheckActive(false);
5143
5694
  if (typeof el._fn === "function") prepareComputed(el, true);
5144
5695
  const ownerStatus = owner._statusFlags;
5145
- if (c && ownerStatus & STATUS_PENDING && ownerStatus & STATUS_UNINITIALIZED) {
5696
+ if (
5697
+ c &&
5698
+ ownerStatus & STATUS_PENDING &&
5699
+ ownerStatus & STATUS_UNINITIALIZED &&
5700
+ // The suspend-throw is for a genuinely-first-load source: the tracked
5701
+ // reader has nothing to pair a verdict with, so it parks on the source.
5702
+ // A latest() SHADOW created lazily mid-flight is born uninitialized even
5703
+ // though its parent has a committed value latest() will serve — throwing
5704
+ // here (swallowed by latestRead's fallback) dropped the shadow from the
5705
+ // probe, so a tracked latest(isPending()) probe created during a
5706
+ // new-question flight cached `false` for that whole flight (#3166).
5707
+ // Defer to the PARENT's initialization state and fall through to normal
5708
+ // collection; the plain pending throw downstream still links the reader.
5709
+ !latestShadowWithInitializedParent(owner)
5710
+ ) {
5146
5711
  if (tracking && el !== c) link(el, c);
5147
5712
  setPendingCheckActive(true);
5148
5713
  throw owner._x?._error;
@@ -5161,8 +5726,22 @@ function pendingCheckRead(el, c, owner, firewall) {
5161
5726
  */
5162
5727
  function heldAwaitingAsync(el) {
5163
5728
  const et = el._transition;
5164
- const t = et ? currentTransition(et) : null;
5729
+ const t = et ? currentTransition(et) : activeTransition;
5165
5730
  if (!t || t._done) return false;
5731
+ // A plain staged write (a signal/store leaf — no _fn) held while an action
5732
+ // is still running is an INPUT to a computation still in flight (#3078):
5733
+ // the pairing rule must not suppress the verdict, or a memo recomputing
5734
+ // mid-action reads the staged value, gets told "not pending", and
5735
+ // disagrees with a direct isPending() probe for the whole action window.
5736
+ // A computed's staged value is the opposite case — a LANDED answer
5737
+ // awaiting reveal — where the pairing rule stands even inside an open
5738
+ // action (#2831: a reader that saw the new value must not also see
5739
+ // pending); still-computing answers are covered by the reporter scan.
5740
+ if (t._actions.length && !el._fn) return true;
5741
+ // A node not yet stamped with a transition only qualifies through the
5742
+ // action check above; the reporter scan below is for transition-held
5743
+ // writes whose source async is still computing.
5744
+ if (!et) return false;
5166
5745
  for (const [source, reporters] of t._asyncReporters) {
5167
5746
  if (
5168
5747
  reporters.size &&
@@ -5209,6 +5788,17 @@ function isPending(fn) {
5209
5788
  });
5210
5789
  const collectPending = () => {
5211
5790
  setPendingCheckActive(false);
5791
+ // Companion reads are mode-neutral plumbing: under an outer latest()
5792
+ // (isPending inside a latest window — #3104's memo shape) leaving latest
5793
+ // mode active dispatched these reads through latestRead, which built a
5794
+ // SHADOW OF THE PENDING SIGNAL itself. The next updatePendingSignal then
5795
+ // wrote that companion-on-companion from inside a recompute
5796
+ // (syncCompanions → setSignal on a shadow created without ownedWrite)
5797
+ // and halted dev with the owned-scope write guard. The creation paths
5798
+ // (getLatestValueComputed / getPendingSignal) already suspend both
5799
+ // modes; this read site must too.
5800
+ const prevLatest = latestReadActive;
5801
+ setLatestReadActive(false);
5212
5802
  const prevStrictRead = strictRead;
5213
5803
  setStrictRead(false);
5214
5804
  try {
@@ -5220,6 +5810,7 @@ function isPending(fn) {
5220
5810
  });
5221
5811
  } finally {
5222
5812
  setStrictRead(prevStrictRead);
5813
+ setLatestReadActive(prevLatest);
5223
5814
  setPendingCheckActive(true);
5224
5815
  }
5225
5816
  // A "not pending" verdict that exists only because this reader saw the
@@ -5279,7 +5870,6 @@ function effect(compute, effect, error, options) {
5279
5870
  effect,
5280
5871
  error,
5281
5872
  isUser ? EFFECT_USER : EFFECT_RENDER,
5282
- notifyEffectStatus,
5283
5873
  options
5284
5874
  );
5285
5875
  recompute(node, true);
@@ -5452,17 +6042,9 @@ function trackedEffect(fn, options) {
5452
6042
  node._config = (node._config & ~CONFIG_AUTO_DISPOSE) | CONFIG_CHILDREN_FORBIDDEN;
5453
6043
  node._modified = true;
5454
6044
  node._type = EFFECT_TRACKED;
5455
- ext(node)._notifyStatus = (status, error) => {
5456
- const actualStatus = status !== undefined ? status : node._statusFlags;
5457
- if (actualStatus & STATUS_ERROR) {
5458
- node._queue.notify(node, STATUS_PENDING, 0);
5459
- const err = error !== undefined ? error : node._x?._error;
5460
- if (!node._queue.notify(node, STATUS_ERROR, STATUS_ERROR)) {
5461
- haltReactivity(unwrapStatusError(err));
5462
- throw err;
5463
- }
5464
- }
5465
- };
6045
+ // Status dispatch rides the SHARED notifier (statusNotifierOf keys off
6046
+ // _type): its error arm is behavior-identical to the closure that used to
6047
+ // live here, without the per-node NodeExtension allocation.
5466
6048
  node._run = run;
5467
6049
  node._queue.enqueue(EFFECT_USER, run);
5468
6050
  if (!node._parent) {
@@ -5480,6 +6062,10 @@ function trackedEffect(fn, options) {
5480
6062
  console.warn(message);
5481
6063
  }
5482
6064
  }
6065
+ // Install the shared effect status notifier (statusNotifierOf serves it to
6066
+ // every effect node) — module-scope: any bundle that creates effects
6067
+ // evaluates this module.
6068
+ setEffectStatusNotify(notifyEffectStatus);
5483
6069
 
5484
6070
  const ACTION_CALLED_IN_OWNED_SCOPE_MESSAGE =
5485
6071
  "[ACTION_CALLED_IN_OWNED_SCOPE] Calling an action inside an owned scope (component, computation) is not allowed. " +
@@ -5948,8 +6534,256 @@ function resolve(fn) {
5948
6534
  rej(err);
5949
6535
  dispose();
5950
6536
  },
5951
- { user: true }
6537
+ // DIRECT_COMMIT: a source settling INTO the held transaction (e.g. a
6538
+ // refresh this action issued) stages its landing; the effect's own
6539
+ // recompute must not stage too, or the microtask apply reads the
6540
+ // stale mainline value and resolves with old data.
6541
+ { user: true, _extraConfig: CONFIG_DIRECT_COMMIT }
6542
+ );
6543
+ });
6544
+ });
6545
+ }
6546
+ /**
6547
+ * Invalidates one reactive source, forcing it to re-execute even if its inputs
6548
+ * haven't changed, and returns a promise for the target's NEXT QUIESCENT
6549
+ * STATE — the re-ask (and anything that supersedes it) has settled.
6550
+ *
6551
+ * Pass either a Solid-created accessor or a projected store created from
6552
+ * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
6553
+ * write-like invalidation operation: it does not read the target's value, and
6554
+ * refreshing a plain signal accessor is a no-op that resolves immediately.
6555
+ *
6556
+ * The returned promise is safe to ignore (fire-and-forget refresh is
6557
+ * unchanged, and a failed refetch will not surface an unhandled rejection).
6558
+ * Awaiting it gives imperative flows the settle point without a reactive
6559
+ * read:
6560
+ * - Accessor targets resolve with the settled value; store targets resolve
6561
+ * with the store node passed (reads through it are fresh after the await).
6562
+ * - A failed re-ask rejects with the error (inside an action's generator,
6563
+ * `yield refresh(x)` throws back at the yield point and the action reverts
6564
+ * like any other failure).
6565
+ * - Semantics are quiescence, not flight identity: if another refresh (or
6566
+ * any invalidation) supersedes this one mid-flight, the promise waits for
6567
+ * — and delivers — whatever finally lands.
6568
+ * - Inside an action, truth landing into the held transaction is STAGED;
6569
+ * the promise still settles then (matching `resolve()`/`until()`, #2930)
6570
+ * and delivers the staged value — the caller's own optimistic override is
6571
+ * never the delivered value.
6572
+ * - The re-ask itself stays verdict-quiet exactly as before: `isPending`
6573
+ * does not flip for a bare refresh (pair with `affects()` for a visible
6574
+ * pending window).
6575
+ *
6576
+ * @example
6577
+ * ```ts
6578
+ * const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
6579
+ *
6580
+ * // Fire-and-forget re-fetch
6581
+ * <button onClick={() => refresh(user)}>Reload</button>;
6582
+ *
6583
+ * // Imperative settle point
6584
+ * const fresh = await refresh(user);
6585
+ * ```
6586
+ */
6587
+ function refresh(target) {
6588
+ const node = target?.[$REFRESH];
6589
+ if (!node) {
6590
+ {
6591
+ const message =
6592
+ "[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
6593
+ "Pass the original source target, not a wrapper function or derived property read.";
6594
+ emitDiagnostic({
6595
+ code: "INVALID_REFRESH_TARGET",
6596
+ kind: "write",
6597
+ severity: "error",
6598
+ message
6599
+ });
6600
+ throw new Error(message);
6601
+ }
6602
+ }
6603
+ // Mark now, watch on a microtask. The waiter is resolve()'s machinery with
6604
+ // two extra reader bits, but it must NOT compute at call time (effects
6605
+ // recompute eagerly on creation): same-tick refreshes coalesce into ONE
6606
+ // re-ask only because every mark lands before anything pulls, and eager
6607
+ // per-call pulls turned three refreshes into three fetches. Deferred, the
6608
+ // waiter's first read sees the coalesced state: FRESH_READ pulls the node
6609
+ // through recompute if it is still dirty (self-deduping — a clean node
6610
+ // no-ops, so N waiters cost one pull; this also closes the race where a
6611
+ // waiter reads the PRE-re-ask value as settled and delivers stale), after
6612
+ // which the read either parks on the re-ask's pending window (async — the
6613
+ // settle walk re-runs it on every landing, equal-value and
6614
+ // staged-under-hold included, and a rejection arrives through the effect's
6615
+ // error channel) or serves the sync answer. AUTHORITATIVE_READ keeps an
6616
+ // action's own optimistic override out of the delivered value. resolve()'s
6617
+ // own eager compute is untouched: created after a refresh it still settles
6618
+ // stale-while-revalidate (#2930) — its contract is "first settled value",
6619
+ // not "next quiescent state".
6620
+ markRefresh(node);
6621
+ const promise = new Promise((res, rej) => {
6622
+ queueMicrotask(() => {
6623
+ // No createRoot: the microtask has no ambient owner, so the effect is
6624
+ // naturally detached, and settle disposes the node directly — the root
6625
+ // added ~560B of otherwise-shakeable machinery for nothing but the
6626
+ // dev-mode NO_OWNER_EFFECT warning, so dev keeps a root husk purely to
6627
+ // stay quiet. The waiter swaps in its microtask queue during its own
6628
+ // first compute (before the initial apply enqueue), replacing the
6629
+ // root-owner plumbing.
6630
+ // Typed as the effect node, not Owner: the capture runs inside the
6631
+ // effect's own compute, where the ambient owner IS the effect —
6632
+ // exactly what dispose() takes.
6633
+ let waiter = null;
6634
+ const make = () =>
6635
+ effect(
6636
+ () => {
6637
+ if (waiter === null) {
6638
+ waiter = getOwner();
6639
+ const queue = new MicrotaskQueue();
6640
+ queue._parent = waiter._queue;
6641
+ waiter._queue = queue;
6642
+ }
6643
+ return read(node);
6644
+ },
6645
+ value => {
6646
+ res(typeof target === "function" ? value : target);
6647
+ dispose(waiter);
6648
+ },
6649
+ err => {
6650
+ rej(err);
6651
+ dispose(waiter);
6652
+ },
6653
+ {
6654
+ user: true,
6655
+ _extraConfig: CONFIG_DIRECT_COMMIT | CONFIG_AUTHORITATIVE_READ | CONFIG_FRESH_READ
6656
+ }
6657
+ );
6658
+ createRoot(make);
6659
+ });
6660
+ });
6661
+ // Fire-and-forget refresh must not turn a failed refetch into an unhandled
6662
+ // rejection; awaiting callers attach their own handlers to `promise`.
6663
+ promise.catch(() => {});
6664
+ return promise;
6665
+ }
6666
+ /**
6667
+ * Awaits a reactive predicate and resolves the first time it settles *truthy*,
6668
+ * with that (narrowed) value. Falsy results and pending async reads both mean
6669
+ * "not yet": the subscription stays live and re-evaluates as sources change.
6670
+ * If the predicate settles with an error — a throw, or an async source that
6671
+ * rejects — the promise rejects with it, as do timeout and abort.
6672
+ *
6673
+ * Where {@link resolve} answers "what is this value" (first settled value,
6674
+ * whatever it is), `until` answers "when does the world confirm this
6675
+ * condition". The difference matters inside an `action()`: `yield until(...)`
6676
+ * holds the action's transaction — and any optimistic state riding it — open
6677
+ * until the condition is independently true.
6678
+ *
6679
+ * To make that sound, `until`'s predicate reads the AUTHORITATIVE view — and
6680
+ * this is the one read-semantics difference from `resolve`, which reads the
6681
+ * normal (transaction's own) view where overrides are visible:
6682
+ *
6683
+ * - **Optimistic overrides are invisible** to the predicate. Your own
6684
+ * tentative write can never satisfy your own ack, even on the
6685
+ * single-primitive shape where the optimistic store IS the live-fed store.
6686
+ * (Derived computeds serve their normal cached values — express the
6687
+ * condition over sources of truth, not derived views of the overlay.)
6688
+ * - **Everything else reads normally, including uncommitted transition-staged
6689
+ * data.** Real data is real wherever it currently lives. This is
6690
+ * load-bearing, not a loophole: truth that arrives *into* the open
6691
+ * transaction (a `refresh()` this action issued, an entangled landing)
6692
+ * stages and cannot commit until the hold releases — a predicate that
6693
+ * refused staged reads would deadlock on the very data it is waiting for.
6694
+ *
6695
+ * This is the acknowledgment mechanism for mutations confirmed on a live data
6696
+ * channel (sockets, subscriptions, live queries) rather than by the mutation's
6697
+ * own response: correlate by a client-generated id or version in the predicate,
6698
+ * and let truth arrive however it arrives — push, refetch, or another tab.
6699
+ *
6700
+ * Failure composes with action semantics: a rejection is thrown back into the
6701
+ * generator at the `yield` point — catchable there, or the action fails and
6702
+ * its optimistic state reverts.
6703
+ *
6704
+ * Must be called *outside* a tracking scope.
6705
+ *
6706
+ * @example
6707
+ * ```ts
6708
+ * const send = action(async function* (text: string) {
6709
+ * const clientId = crypto.randomUUID();
6710
+ * setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
6711
+ * await socket.send({ clientId, text }); // fire-and-forget transport
6712
+ * // Hold until the live source echoes the write (authoritative view —
6713
+ * // the optimistic row above cannot satisfy this):
6714
+ * yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
6715
+ * });
6716
+ * ```
6717
+ *
6718
+ * @param fn a reactive predicate over authoritative state
6719
+ * @param options optional `timeout` (ms) and abort `signal`
6720
+ */
6721
+ function until(fn, options) {
6722
+ if (getObserver()) {
6723
+ throw new Error(
6724
+ "Cannot call until inside a reactive scope; await it from an action or another imperative scope."
6725
+ );
6726
+ }
6727
+ // Late-bind the wakeup hook for the A17-silent ack paths (pay-for-use:
6728
+ // apps that never call until() never retain it).
6729
+ installAuthoritativeRead();
6730
+ // Flip-entanglement (#3164 follow-up): the transaction this until() holds
6731
+ // open (the action's, when yielded from one). The predicate is the user's
6732
+ // declaration of what confirms it — when a foreign transition's staged
6733
+ // write flips it truthy, that transition merges here and reveals at the
6734
+ // joint settle instead of painting the confirmation under live optimism.
6735
+ const awaiting = activeTransition;
6736
+ return new Promise((res, rej) => {
6737
+ const signal = options?.signal;
6738
+ if (signal?.aborted) return rej(signal.reason);
6739
+ createRoot(dispose => {
6740
+ // Same delivery contract as resolve() (#2930): effect applies ride a
6741
+ // microtask so the promise can settle while the transaction the caller
6742
+ // yielded it into is still open — that transaction being open is the
6743
+ // entire point of the hold.
6744
+ const owner = getOwner();
6745
+ const queue = new MicrotaskQueue();
6746
+ queue._parent = owner._queue;
6747
+ owner._queue = queue;
6748
+ let timer;
6749
+ let onAbort;
6750
+ const settle = fire => {
6751
+ if (timer !== undefined) clearTimeout(timer);
6752
+ if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
6753
+ fire();
6754
+ dispose();
6755
+ };
6756
+ effect(
6757
+ awaiting === null
6758
+ ? fn
6759
+ : () => {
6760
+ const value = fn();
6761
+ // Runs inside the compute (pure phase): the confirming
6762
+ // transition's stamps are live and its commit hasn't run, so
6763
+ // the merge lands before any reveal. Falsy evaluations skip —
6764
+ // non-flipping updates were never named as the confirmation.
6765
+ if (value) entangleConfirmingTransitions(getObserver(), awaiting);
6766
+ return value;
6767
+ },
6768
+ value => {
6769
+ // Falsy is "not yet": keep the subscription live and wait for the
6770
+ // next evaluation. Only a truthy settled value resolves.
6771
+ if (value) settle(() => res(value));
6772
+ },
6773
+ err => settle(() => rej(err)),
6774
+ // AUTHORITATIVE_READ: overrides invisible to the predicate.
6775
+ // DIRECT_COMMIT: truth that stages into the held transaction (a
6776
+ // refresh the action issued) must flow through to the microtask
6777
+ // apply — a staged effect value would deadlock the hold on data
6778
+ // the hold itself is keeping uncommitted.
6779
+ { user: true, _extraConfig: CONFIG_AUTHORITATIVE_READ | CONFIG_DIRECT_COMMIT }
5952
6780
  );
6781
+ if (options?.timeout !== undefined)
6782
+ timer = setTimeout(() => settle(() => rej(new TimeoutError())), options.timeout);
6783
+ if (signal !== undefined) {
6784
+ onAbort = () => settle(() => rej(signal.reason));
6785
+ signal.addEventListener("abort", onAbort, { once: true });
6786
+ }
5953
6787
  });
5954
6788
  });
5955
6789
  }
@@ -6100,6 +6934,15 @@ let optHooks = null;
6100
6934
  function setOptHooks(h) {
6101
6935
  optHooks = h;
6102
6936
  }
6937
+ /** Sticky descendants flag walk (§6d): reconcile's keyed pruning descends
6938
+ * only where subscriptions exist at/below. Nodes AND patches count. */
6939
+ function markDescendants(target) {
6940
+ let t = target;
6941
+ while (t && !t.d) {
6942
+ t.d = true;
6943
+ t = t.u;
6944
+ }
6945
+ }
6103
6946
 
6104
6947
  /**
6105
6948
  * Brand symbols used internally by the store proxy / projection plumbing.
@@ -6448,8 +7291,9 @@ function notifyMarkBoundaries(node) {
6448
7291
  visited.add(sub);
6449
7292
  // Display consumers (render effects, boundary computeds) act on the
6450
7293
  // notification; descent stops there, exactly like the status rails.
6451
- if (sub._x?._notifyStatus) {
6452
- sub._x._notifyStatus.call(sub, STATUS_PENDING, error);
7294
+ const notify = statusNotifierOf(sub);
7295
+ if (notify) {
7296
+ notify.call(sub, STATUS_PENDING, error);
6453
7297
  return;
6454
7298
  }
6455
7299
  forEachDependent(sub, visit);
@@ -6557,6 +7401,15 @@ function affects(target, key) {
6557
7401
  }
6558
7402
  }
6559
7403
 
7404
+ let patchHooks = null;
7405
+ let rowHooks = null;
7406
+ function installPatchHooks(hooks) {
7407
+ patchHooks = hooks;
7408
+ }
7409
+ function installRowHooks(hooks) {
7410
+ rowHooks = hooks;
7411
+ }
7412
+
6560
7413
  /**
6561
7414
  * Store rewrite — increment 2: plain deep stores with pending-backing writes.
6562
7415
  * Contract: INTERNALS-STORE-STATE.md.
@@ -6585,8 +7438,13 @@ function affects(target, key) {
6585
7438
  * headroom for future fields. The prototype is reset to `Object.prototype`
6586
7439
  * so proxy-forwarded semantics (getPrototypeOf, constructor) are exactly a
6587
7440
  * plain object's. Array targets keep the bare-`[]` path — they must carry
6588
- * the array exotic class for `Array.isArray(proxy)`, and arrays store named
6589
- * fields off-object where this cliff does not apply. */
7441
+ * the array exotic class for `Array.isArray(proxy)`.
7442
+ *
7443
+ * ARRAY SHAPE RULE: arrays normalize their named properties to dictionary
7444
+ * mode as the count grows (V8 13.x: counts ≡ 0 mod 3 from 18 up), so the
7445
+ * target's named field count is capped at 20 — write-side patch-channel
7446
+ * state lives inside the single `pc` extension (see target.ts), never as
7447
+ * new named fields here. */
6590
7448
  function TargetShape() {
6591
7449
  this.v = undefined;
6592
7450
  this.ch = undefined;
@@ -6607,9 +7465,15 @@ function TargetShape() {
6607
7465
  this.s = undefined;
6608
7466
  this.ovl = undefined;
6609
7467
  this.del = undefined;
6610
- this.wk = undefined;
7468
+ this.pc = undefined;
7469
+ this.hv = undefined;
7470
+ this.ht = undefined;
6611
7471
  }
6612
7472
  TargetShape.prototype = Object.prototype;
7473
+ /** Lazily allocate the patch-channel extension (one literal shape). */
7474
+ function pcOf(t) {
7475
+ return t.pc ?? (t.pc = { sp: null, p: null, ro: null, wk: null, qa: null, qe: null });
7476
+ }
6613
7477
  function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
6614
7478
  // The proxy target carries the array exotic class when the value is an
6615
7479
  // array, so Array.isArray(proxy) is true; the fields live on it directly.
@@ -6626,6 +7490,7 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
6626
7490
  t.h = null;
6627
7491
  t.k = null;
6628
7492
  t.dk = null;
7493
+ t.pc = null;
6629
7494
  t.u = parent;
6630
7495
  t.pk = parentKey;
6631
7496
  t.px = null;
@@ -6638,7 +7503,8 @@ function createTarget(value, parent, parentKey, fam = parent?.fam ?? null) {
6638
7503
  t.s = false;
6639
7504
  t.ovl = false;
6640
7505
  t.del = null;
6641
- t.wk = null;
7506
+ t.hv = null;
7507
+ t.ht = null;
6642
7508
  t.px = new Proxy(t, traps);
6643
7509
  // Legacy interop: shared machinery (affects walks, wrap dedupe) reads the
6644
7510
  // proxy off looked-up targets as a field.
@@ -6819,13 +7685,6 @@ function getDeepNode(target) {
6819
7685
  function bumpDeep(t) {
6820
7686
  if (t.dk !== null) setSignal(t.dk, 1);
6821
7687
  }
6822
- function markDescendants(target) {
6823
- let t = target;
6824
- while (t && !t.d) {
6825
- t.d = true;
6826
- t = t.u;
6827
- }
6828
- }
6829
7688
  // ---------------------------------------------------------------------------
6830
7689
  // pending backing + fold (the single mutation point)
6831
7690
  /** target → committed backing at batch start (the fold diff's old side). */
@@ -6850,6 +7709,12 @@ function cloneRaw(source, t) {
6850
7709
  ? Object.defineProperties([], descs)
6851
7710
  : Object.create(Object.getPrototypeOf(source), descs);
6852
7711
  }
7712
+ /** Scanned plainness for patch admission (patchableRaw): runs the one-time
7713
+ * accessor scan if it hasn't happened yet — the sticky `a` flag alone is not
7714
+ * trustworthy before a scan (it starts false and is discovered lazily). */
7715
+ function targetIsPlain(target) {
7716
+ return target.sc ? !target.a : scanAccessorsOnce(target);
7717
+ }
6853
7718
  /** One-time own-accessor scan (Annex-B probes, no descriptor allocation);
6854
7719
  * returns true when the container is plain data (overlay-safe). */
6855
7720
  function scanAccessorsOnce(target) {
@@ -6892,6 +7757,28 @@ function materializePB(target) {
6892
7757
  }
6893
7758
  function ensurePB(target) {
6894
7759
  let pb = target.pb;
7760
+ // Truth-staged backing hand-off (#3164 fold): a TENTATIVE draft opening on
7761
+ // a target whose pending backing is truth-staged (a landing folded into a
7762
+ // retaining transaction — it carries a foldBatches stamp) must not share
7763
+ // the container. Tentative writes would pollute staged truth, and the
7764
+ // tentative discard (notifyOptimisticWrites nulls pb) would destroy the
7765
+ // landing. Park the staged backing and open a fresh draft seeded from the
7766
+ // optimistic view below; the tentative discard restores it. The
7767
+ // tentativePBs guard scopes this to draft OPEN: the draft's own backing
7768
+ // (foldBatches-stamped by its first write when an action's transition is
7769
+ // ambient) must not be parked by its own later writes.
7770
+ if (
7771
+ pb !== null &&
7772
+ !tentativePBs.has(pb) &&
7773
+ target.fam?.opt === true &&
7774
+ !projectionWriteActive &&
7775
+ !getWriteOverride() &&
7776
+ foldBatches.has(target)
7777
+ ) {
7778
+ stagedTruthPB.set(target, pb);
7779
+ pb = target.pb = null;
7780
+ }
7781
+ if (activeTransition !== null) foldBatches.set(target, activeTransition);
6895
7782
  if (pb === null) {
6896
7783
  // Prototype-chain overlay (#3044): plain-data non-array containers
6897
7784
  // outside projection/optimistic families open drafts in O(1) — own keys
@@ -6913,6 +7800,7 @@ function ensurePB(target) {
6913
7800
  // seed from committed truth — seeding overrides there would fold a lane
6914
7801
  // value into the committed home ("authority wins at reveal" would break).
6915
7802
  if (target.fam?.opt && !projectionWriteActive && !getWriteOverride()) {
7803
+ tentativePBs.add(pb);
6916
7804
  const nodes = target.n;
6917
7805
  if (nodes !== null) {
6918
7806
  for (const key of Reflect.ownKeys(nodes)) {
@@ -6934,6 +7822,25 @@ function ensurePB(target) {
6934
7822
  }
6935
7823
  return pb;
6936
7824
  }
7825
+ /** Sentinel holder for `t.ht`: a latest()-pull staged this adoption outside
7826
+ * any transition — the hold lasts until the fold commit (drainFolds). */
7827
+ const PLAIN_HOLD = Symbol("plainHold");
7828
+ /** True while a latest() read is pulling the projection computed up to date
7829
+ * (see the get trap): adoptions landing during the pull are speculative
7830
+ * against the un-flushed batch and stage a held view. (Not injectable — the
7831
+ * derived createStore overload retains projection machinery in every store
7832
+ * bundle, see treeshake.test.ts.) */
7833
+ let latestPullActive = false;
7834
+ /** Resolve the held committed view (#3074): answers the masked old backing
7835
+ * while the hold is live, and lazily clears a hold whose transition has
7836
+ * committed (transitions merge — resolve through currentTransition, same as
7837
+ * foldHeld's node stamps). */
7838
+ function heldMaskView(t) {
7839
+ const ht = t.ht;
7840
+ if (ht === null) return null;
7841
+ if (ht !== PLAIN_HOLD && currentTransition(ht)?._done === true) return (t.ht = t.hv = null);
7842
+ return t.hv;
7843
+ }
6937
7844
  /**
6938
7845
  * Adoption (2026-08-16c): the incoming object becomes the committed backing
6939
7846
  * IMMEDIATELY — reconcile is eagerly visible to every reader (shipped
@@ -6949,6 +7856,20 @@ function adoptPB(target, incoming, eager = false) {
6949
7856
  if (!eager) {
6950
7857
  queueFold(target); // records the pre-batch old before we swap
6951
7858
  target.adopted = true;
7859
+ // #3074/#3075: a projection recompute deriving from uncommitted inputs
7860
+ // swaps the backing SPECULATIVELY — committed-visibility readers must
7861
+ // keep the pre-hold view until the hold resolves (a source held by a
7862
+ // live transition, or a latest()-pull ahead of the flush). Post-await
7863
+ // landings (write-override) stay immediately visible — landed truth —
7864
+ // and clear any hold; optimistic families ride the lane machinery.
7865
+ if (target.fam?.opt !== true) {
7866
+ if (getWriteOverride()) {
7867
+ target.ht = target.hv = null;
7868
+ } else if (activeTransition !== null || latestPullActive) {
7869
+ if (heldMaskView(target) === null) target.hv = target.v;
7870
+ target.ht = activeTransition ?? PLAIN_HOLD;
7871
+ }
7872
+ }
6952
7873
  }
6953
7874
  target.pb = null;
6954
7875
  // Overlay and accessor-scan state describe the OUTGOING backing — a
@@ -6960,24 +7881,54 @@ function adoptPB(target, incoming, eager = false) {
6960
7881
  // draft rescans once (#3044 audit follow-up).
6961
7882
  target.ovl = false;
6962
7883
  target.del = null;
6963
- target.wk = null; // adoption supersedes any staged trap writes
6964
7884
  target.sc = false;
6965
7885
  target.a = false;
7886
+ if (target.pc !== null) target.pc.wk = null; // adoption supersedes staged trap writes
6966
7887
  target.v = incoming;
6967
7888
  target.ch = incoming[$TARGET] !== undefined;
6968
7889
  (target.fam?.map ?? storeNextLookup).set(incoming, target);
6969
7890
  }
7891
+ /** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
7892
+ * array length write implicitly deleted indices) — consumers full-scan. */
7893
+ const WK_ALL = new Set();
7894
+ const plainProto = o => {
7895
+ const p = Object.getPrototypeOf(o);
7896
+ return p === Object.prototype || p === Array.prototype || p === null;
7897
+ };
6970
7898
  function queueFold(target) {
6971
7899
  if (foldOlds.has(target)) return;
6972
- if (foldOlds.size === 0) {
6973
- if (!hookInstalled) {
6974
- hookInstalled = true;
6975
- setStoreCommitHook(drainFolds);
6976
- }
6977
- schedule(); // once per batch drain clears the map
6978
- }
7900
+ if (!hookInstalled) {
7901
+ hookInstalled = true;
7902
+ setStoreCommitHook(drainFolds);
7903
+ }
7904
+ // Always arm — "map non-empty ⇒ drain scheduled" is NOT an invariant: a
7905
+ // held re-queue, or an incomplete-transition flush (which skips
7906
+ // commitPendingNodes entirely), leaves entries behind after `scheduled`
7907
+ // was consumed. A size-gated arm then strands every LATER fold — queued
7908
+ // silently, never drained, committed base frozen at stale state while its
7909
+ // nodes commit (#3089). schedule() early-returns when already armed.
7910
+ schedule();
6979
7911
  foldOlds.set(target, target.v);
6980
7912
  }
7913
+ /** Fold write-attribution (#3089): a draft written while a transition is
7914
+ * active belongs to that transition — its fold must not commit before the
7915
+ * transition settles. Observed keys already defer through the held check in
7916
+ * drainFolds (their nodes carry _pendingValue); this write-time stamp is the
7917
+ * equivalent hold for UNOBSERVED keys, which have no node to consult.
7918
+ * Refreshed on every write; resolved through currentTransition at drain
7919
+ * (transitions merge — same rule as heldMaskView). */
7920
+ const foldBatches = new WeakMap();
7921
+ /** Parked truth-staged pending backings (#3164 fold): a tentative draft that
7922
+ * opens while a folded landing's backing is live moves the staged container
7923
+ * here (see ensurePB); the tentative discard in notifyOptimisticWrites
7924
+ * restores it in place of the usual null. */
7925
+ const stagedTruthPB = new WeakMap();
7926
+ /** Backings opened by TENTATIVE drafts (optimistic user setters): ensurePB's
7927
+ * truth-park must not fire against the draft's own container on its second
7928
+ * and later writes (the first write stamps foldBatches whenever an action's
7929
+ * transition is ambient). Entries die with their draft — tentative backings
7930
+ * are consumed at setter exit. */
7931
+ const tentativePBs = new WeakSet();
6981
7932
  /** Committed-time privatization for parent-chain slot updates (path copying). */
6982
7933
  function privatizeCommitted(target) {
6983
7934
  if (ownedRaw.has(target.v)) return;
@@ -6997,7 +7948,27 @@ function drainFolds() {
6997
7948
  const entries = [...foldOlds];
6998
7949
  foldOlds.clear();
6999
7950
  for (const [t, old] of entries) {
7951
+ // A latest()-pull staging holds only until the fold commit: this flush
7952
+ // is committing the batch the pull ran ahead of. Transition holds stay —
7953
+ // they clear when their transition is done (heldMaskView).
7954
+ if (t.ht === PLAIN_HOLD) t.ht = t.hv = null;
7955
+ // Eager (write-override) family folds swap pb -> v at notifyWrites'
7956
+ // tail: by the time this drain runs they carry no pb, and their
7957
+ // structural ops must emit at the fold-commit site below (the clone
7958
+ // branch never sees them). Re-audit blocker 4.
7959
+ const foldedEager = t.pb === null;
7000
7960
  if (t.pb !== null) {
7961
+ // #3089: a fold written under a still-running transition defers to
7962
+ // that transition's settle (the write-time stamp covers unobserved
7963
+ // keys; observed keys also hit the pending-node held check below).
7964
+ const fb = foldBatches.get(t);
7965
+ if (fb !== undefined) {
7966
+ if (currentTransition(fb)._done === false) {
7967
+ foldOlds.set(t, old);
7968
+ continue;
7969
+ }
7970
+ foldBatches.delete(t);
7971
+ }
7001
7972
  // Setter path: nodes were setSignal'd at setter exit (write-time
7002
7973
  // notification — transitions/holds ride core machinery). Commit the
7003
7974
  // backing only for keys whose nodes have committed; a still-pending
@@ -7009,9 +7980,14 @@ function drainFolds() {
7009
7980
  // Only written keys can hold (their nodes took the setSignal); the
7010
7981
  // wk bound keeps this O(written) — see notifyWrites. Same fallback
7011
7982
  // rules as the notify (WK_ALL / accessors / non-plain prototypes).
7012
- const wkh = t.wk;
7983
+ const wkh = t.pc !== null ? t.pc.wk : null;
7013
7984
  const keys =
7014
- wkh === null || wkh === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb)
7985
+ wkh === null ||
7986
+ wkh === WK_ALL ||
7987
+ t.a === true ||
7988
+ // Overlay pbs chain to the COMMITTED object (#3044) — plainness is
7989
+ // the committed container's prototype, not the overlay's.
7990
+ !plainProto(t.ovl ? t.v : pb)
7015
7991
  ? Reflect.ownKeys(nodes)
7016
7992
  : wkh;
7017
7993
  for (const key of keys) {
@@ -7049,15 +8025,70 @@ function drainFolds() {
7049
8025
  (t.fam?.map ?? storeNextLookup).delete(pb);
7050
8026
  t.pb = null;
7051
8027
  t.ovl = false;
7052
- t.wk = null; // written-keys window closes with the fold commit
8028
+ if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
7053
8029
  } else {
8030
+ // Setter-channel structural ops: a fold that changes an array's shape
8031
+ // (push/splice/permutation through the setter — the reconcile walk
8032
+ // never queues here) is a structural visibility transition for any
8033
+ // registered list driver. Identity-keyed; aligned folds emit nothing.
8034
+ // Family targets defer to their own adoption emission (fam reconcile).
8035
+ // Arrays always fold on this clone branch (overlay is non-array only).
8036
+ // Family setter drafts (writable projection push/splice through the
8037
+ // masked setter) fold on this branch too and the fold IS their
8038
+ // visibility moment — emit unless the structure already rode another
8039
+ // channel: adoption folds (reconcile walk emitted ops) and
8040
+ // optimistic families (lane-timed override channel). Re-audit
8041
+ // blocker 4.
8042
+ if (
8043
+ t.pc !== null &&
8044
+ t.pc.ro !== null &&
8045
+ !t.adopted &&
8046
+ t.fam?.opt !== true &&
8047
+ Array.isArray(pb) &&
8048
+ Array.isArray(t.v)
8049
+ )
8050
+ rowHooks.emitSetterRowOps(t, t.v, pb);
7054
8051
  t.v = pb;
7055
8052
  t.ch = false; // pb is always a plain clone
7056
8053
  t.pb = null;
7057
- t.wk = null; // written-keys window closes with the fold commit
8054
+ if (t.pc !== null) t.pc.wk = null; // written-keys window closes with the fold commit
8055
+ }
8056
+ }
8057
+ if (t.v === old) {
8058
+ // A no-op adoption (A -> B -> A before flush) still consumed its walk:
8059
+ // clear the flag or every later setter row-op gate (!t.adopted) stays
8060
+ // failed and a driven family list freezes (re-audit 5, P1-1).
8061
+ t.adopted = false;
8062
+ continue;
8063
+ }
8064
+ // Patch channel (fold-commit site): family targets emit HERE — the fold
8065
+ // IS their visibility moment (held folds re-queued above emit when they
8066
+ // actually commit) — and so do PLAIN fold-adopted targets (setter-
8067
+ // returned root replacements, chained-store swaps: adoptions WITHOUT a
8068
+ // reconcile walk, so no walk-site emission ever happened — re-audit 2,
8069
+ // P1-2). Plain eager targets emitted at their walk/setter sites already.
8070
+ if (t.pc !== null && (t.fam !== null || t.adopted)) {
8071
+ // Structural ops for folds whose structure rode no other channel:
8072
+ // eager-folded family SETTER drafts (write-override swaps pb -> v at
8073
+ // notifyWrites' tail — the clone branch never sees them; adoption
8074
+ // folds re-emitting would double the walk's ops) and PLAIN fold
8075
+ // adoptions (no walk at all). Optimistic families ride the override
8076
+ // channel (lane-timed ops + revert RESYNC) — never re-emit here.
8077
+ if (
8078
+ t.pc.ro !== null &&
8079
+ t.fam?.opt !== true &&
8080
+ (t.fam !== null ? foldedEager && !t.adopted : t.adopted) &&
8081
+ Array.isArray(t.v) &&
8082
+ Array.isArray(old)
8083
+ )
8084
+ rowHooks.emitSetterRowOps(t, old, t.v);
8085
+ if (t.pc.p !== null) {
8086
+ // Accessor demotion at the fold-commit seam is DEV-ONLY (see the
8087
+ // reconcile seam note: prod never pays per-adoption scans).
8088
+ if (!targetIsPlain(t)) patchHooks.demoteToEffects(t);
8089
+ else patchHooks.emitPatchLocal(t, t.v, old);
7058
8090
  }
7059
8091
  }
7060
- if (t.v === old) continue; // adopted then re-adopted back, or no-op
7061
8092
  // Path copying (CAS: see the eager-fold twin above).
7062
8093
  if (t.u && t.u.v[t.pk] === old) {
7063
8094
  privatizeCommitted(t.u);
@@ -7078,17 +8109,6 @@ function drainFolds() {
7078
8109
  * "pending home = the node when a node exists"). Unobserved keys stay in the
7079
8110
  * pending backing and fold directly at commit.
7080
8111
  */
7081
- /** Sentinel for `t.wk`: the written-keys bound is unusable this batch (an
7082
- * array length write implicitly deleted indices) — consumers full-scan. */
7083
- const WK_ALL = new Set();
7084
- /** Plain-prototype check for the written-keys bound: prototype getters on
7085
- * class instances can derive from ANY field, so only plain-data containers
7086
- * may bound the notify to written keys. Overlay pbs chain to the COMMITTED
7087
- * object (#3044), so overlay plainness is judged on the committed proto. */
7088
- const plainProto = o => {
7089
- const p = Object.getPrototypeOf(o);
7090
- return p === Object.prototype || p === Array.prototype || p === null;
7091
- };
7092
8112
  function notifyWrites(t) {
7093
8113
  let pb = t.pb;
7094
8114
  if (pb === null) return;
@@ -7143,8 +8163,15 @@ function notifyWrites(t) {
7143
8163
  // not a full scan). Falls back to the full node scan when the bound can't
7144
8164
  // hold: no trap granularity (wk null), an array length write (WK_ALL —
7145
8165
  // implicit index deletes), accessors on the record (t.a — a getter node's
7146
- // value can change when ANY key is written), or a non-plain prototype.
7147
- const wk0 = t.wk;
8166
+ // value can change when ANY key is written), or a non-plain prototype
8167
+ // (class instances: prototype getters derive from arbitrary fields).
8168
+ const wk0 = t.pc !== null ? t.pc.wk : null;
8169
+ // Overlay pbs chain to the COMMITTED object (#3044): a prototype-overlay
8170
+ // draft is plain data on its own layer, but its getPrototypeOf is the
8171
+ // committed container — judge plainness by the COMMITTED prototype or the
8172
+ // bound never engages for overlay writes (every plain-object setter batch
8173
+ // would full-scan: the exact selection-map workload wk exists for; jf
8174
+ // `select` regressed 2x on this).
7148
8175
  const writtenKeys = wk0 === WK_ALL || t.a === true || !plainProto(t.ovl ? t.v : pb) ? null : wk0;
7149
8176
  if (nodes !== null) {
7150
8177
  const keys = writtenKeys ?? Reflect.ownKeys(nodes);
@@ -7177,15 +8204,18 @@ function notifyWrites(t) {
7177
8204
  }
7178
8205
  const has = t.h;
7179
8206
  if (has !== null) {
7180
- for (const key of Reflect.ownKeys(has))
7181
- setSignal(has[key], key in pb && !(t.del !== null && t.del.has(key)));
8207
+ const keys = writtenKeys ?? Reflect.ownKeys(has);
8208
+ for (const key of keys) {
8209
+ const node = has[key];
8210
+ if (node !== undefined) setSignal(node, key in pb && !(t.del !== null && t.del.has(key)));
8211
+ }
7182
8212
  }
7183
8213
  // Deep-witness (dk): setter writes must notify a deep() subscriber even on
7184
- // keys with no node. O(pb keys) equality only when a witness exists.
8214
+ // keys with no node. O(written/pb keys) equality only when a witness exists.
7185
8215
  if (t.dk !== null) {
7186
8216
  if (t.del !== null && t.del.size !== 0) bumpDeep(t);
7187
8217
  else
7188
- for (const key of Reflect.ownKeys(pb)) {
8218
+ for (const key of writtenKeys ?? Reflect.ownKeys(pb)) {
7189
8219
  const nv = pb[key];
7190
8220
  const ov = old[key];
7191
8221
  if (nv !== null && typeof nv === "object" ? !targetsEqual(ov, nv) : !isEqual(ov, nv)) {
@@ -7215,6 +8245,13 @@ function notifyWrites(t) {
7215
8245
  }
7216
8246
  if (changed) setSignal(t.k, v => v + 1);
7217
8247
  }
8248
+ // Patch channel (setter site): a committed write transitions this record —
8249
+ // queue its patches and bubble to ancestors (targeted nested writes must
8250
+ // reach the row patch, §4b). One number compare when no patches exist.
8251
+ // Family targets skip this site: their visibility moment is the FOLD
8252
+ // commit (drainFolds emits), not the recompute/draft write.
8253
+ if (t.fam === null && patchHooks !== null && patchHooks.hasPatches())
8254
+ patchHooks.emitPatch(t, pb, old);
7218
8255
  // Projection backing folds split by channel (two pinned contracts):
7219
8256
  // - sync-derive drafts (recompute body): NEVER eager — a downstream async
7220
8257
  // hold can form LATER in the same flush and the leaf must stay at stale
@@ -7225,10 +8262,18 @@ function notifyWrites(t) {
7225
8262
  // IMMEDIATE — landed truth shows to untracked readers even while a
7226
8263
  // downstream consumer's own async still holds the effect-level reveal
7227
8264
  // (spec-async "verdicts never inherit consumers' in-flight state").
7228
- if (t.fam !== null && t.pb !== null && getWriteOverride()) {
8265
+ // EXCEPT under an active transaction (#3164 fold): a landing riding a
8266
+ // retaining transaction (the optimistic module's aroundWrite binds it)
8267
+ // stages instead — ensurePB stamped foldBatches, so the backing commits
8268
+ // with the transaction and the reveal is atomic at settle. The pinned
8269
+ // immediate-commit contract is stated over the no-transaction microtask
8270
+ // posture, which `activeTransition === null` is exactly.
8271
+ if (t.fam !== null && t.pb !== null && getWriteOverride() && activeTransition === null) {
8272
+ // Landed truth (post-await write-override): immediately visible to every
8273
+ // reader — any staged held view is superseded.
8274
+ if (t.ht !== null) t.ht = t.hv = null;
7229
8275
  const oldBacking = t.v;
7230
8276
  t.pb = null;
7231
- t.wk = null; // written-keys window closes with the eager fold
7232
8277
  t.v = pb;
7233
8278
  t.ch = false;
7234
8279
  if (t.u && t.u.v[t.pk] === oldBacking) {
@@ -7433,6 +8478,17 @@ function inOwnerContext() {
7433
8478
  const eff = c._root ? c._parentComputed : c;
7434
8479
  return eff != null && !(eff._config & CONFIG_CHILDREN_FORBIDDEN);
7435
8480
  }
8481
+ /** CHILDREN_FORBIDDEN execution scope (createTrackedEffect / onSettled
8482
+ * callbacks). Distinct from context-free: these scopes get committed
8483
+ * visibility even against a projection's authoritative-elect pending
8484
+ * backing (#3082) — parity with signals, where core read() serves
8485
+ * committed to them regardless of staged writes. */
8486
+ function inForbiddenScope() {
8487
+ const c = getOwner();
8488
+ if (c === null) return false;
8489
+ const eff = c._root ? c._parentComputed : c;
8490
+ return eff != null && !!(eff._config & CONFIG_CHILDREN_FORBIDDEN);
8491
+ }
7436
8492
  /** A pending fold is transition-held when any written node's parked value is
7437
8493
  * stamped by a live transition (a plain batch parking — the lazy-recompute
7438
8494
  * read case — has no transition stamp and serves fresh). */
@@ -7451,6 +8507,20 @@ function foldHeld(target) {
7451
8507
  return false;
7452
8508
  }
7453
8509
  function readSource(target) {
8510
+ // Held view first (#3074): an adoption staged under a live hold serves the
8511
+ // pre-hold committed backing to committed-visibility readers. Speculative
8512
+ // readers — drafts, write-override, owner-context computeds recomputing
8513
+ // inside the transaction, and latest() reads — see the adopted backing.
8514
+ if (
8515
+ target.ht !== null &&
8516
+ !latestReadActive &&
8517
+ !inDraft(target) &&
8518
+ !getWriteOverride() &&
8519
+ !inOwnerContext()
8520
+ ) {
8521
+ const hv = heldMaskView(target);
8522
+ if (hv !== null) return hv;
8523
+ }
7454
8524
  // Signal-parity visibility (core read(): owner-context reads serve
7455
8525
  // _pendingValue, context-free reads serve committed — effects recompute
7456
8526
  // BEFORE commitPendingNodes in the flush, so the pending view must be
@@ -7461,15 +8531,37 @@ function readSource(target) {
7461
8531
  target.pb !== null &&
7462
8532
  (inDraft(target) ||
7463
8533
  getWriteOverride() ||
7464
- inOwnerContext() ||
8534
+ // Owner-context readers see the pending backing — EXCEPT held truth
8535
+ // on an optimistic family (#3164 fold): a live pb on an opt family
8536
+ // outside the draft/write-override windows is a staged landing
8537
+ // (tentative drafts never outlive their setter), and only the
8538
+ // authoritative postures and latest() see it (the backing-level twin
8539
+ // of core read()'s A17-for-held-truth arm; ordinary readers keep
8540
+ // committed until the transaction's reveal).
8541
+ (inOwnerContext() && !heldTruthMasked(target)) ||
7465
8542
  // A projection's pending backing is authoritative-elect: serve it to
7466
8543
  // context-free readers too UNLESS a transition is holding the node
7467
- // commits (downstream async hold — stale committed is the contract).
7468
- (target.fam !== null && !foldHeld(target)))
8544
+ // commits (downstream async hold — stale committed is the contract)
8545
+ // or the reader is a CHILDREN_FORBIDDEN scope, which never observes
8546
+ // its own unsettled write (#3082, signal parity per #3006).
8547
+ (target.fam !== null && !heldTruthMasked(target) && !foldHeld(target) && !inForbiddenScope()))
7469
8548
  )
7470
8549
  return target.pb;
7471
8550
  return target.v;
7472
8551
  }
8552
+ /** #3164 fold: HELD truth on an optimistic family — a pending backing
8553
+ * stamped by a live transition that retains optimism — is masked from
8554
+ * ordinary readers (they keep committed until the transaction's reveal);
8555
+ * the authoritative postures and latest() tunnel through. Un-stamped
8556
+ * backings and optimism-free transitions keep ordinary mid-batch/
8557
+ * speculation visibility. */
8558
+ function heldTruthMasked(target) {
8559
+ if (target.fam?.opt !== true || latestReadActive || authoritativeServe()) return false;
8560
+ const fb = foldBatches.get(target);
8561
+ // opt families are only created by createOptimisticStore, whose module
8562
+ // install populates optHooks — the assertion holds by construction.
8563
+ return fb !== undefined && optHooks.retainsOptimism(fb);
8564
+ }
7473
8565
  const hasOwn = Object.prototype.hasOwnProperty;
7474
8566
  // Allocation-free own-accessor probe (replaces eager descriptor scans — the
7475
8567
  // single biggest creation cost in the uibench profile): Annex-B lookups
@@ -7501,6 +8593,26 @@ function runAuthoritative(fn) {
7501
8593
  function hasActiveOverride(node) {
7502
8594
  return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
7503
8595
  }
8596
+ /** The reading computation is until()'s authoritative-view predicate — same
8597
+ * source of truth as core read()'s A17 carve-out (`context`, which persists
8598
+ * under untrack). optimisticView()'s composition gate consults exactly this:
8599
+ * write-side machinery (patch emission, tentative re-application) must keep
8600
+ * composing even when it runs inside an authoritative-write bracket. */
8601
+ function authoritativeRead() {
8602
+ const c = context;
8603
+ return c !== null && (c._config & CONFIG_AUTHORITATIVE_READ) !== 0;
8604
+ }
8605
+ /** Serve-side authoritative gate: until()'s predicate PLUS truth authors —
8606
+ * the projection derive's draft (wrapDraft trap brackets, runAuthoritative;
8607
+ * the same posture pair ensurePB classifies drafts by). A source computing
8608
+ * the next truth must never read its callers' tentative overlays: a derive
8609
+ * continuation's `store.push` computing its index from an action's
8610
+ * optimistic row landed truth in the wrong slot and corrupted committed
8611
+ * state (#3108). Trap-level overlay serves gate on this so values, length,
8612
+ * membership, and keys leave the authoritative view together. */
8613
+ function authoritativeServe() {
8614
+ return projectionWriteActive || getWriteOverride() || authoritativeRead();
8615
+ }
7504
8616
  /** Context-aware node view for reads outside tracking: active override >
7505
8617
  * held pending (owner context) > the BACKING value. Committed truth lives in
7506
8618
  * the backing (single-home rule, O6) — node `_value` is never served here,
@@ -7509,11 +8621,26 @@ function hasActiveOverride(node) {
7509
8621
  * FORCE sentinels never surface (they only bump subscribers of accessor
7510
8622
  * keys, which are served by the trap, not the node). */
7511
8623
  function nodeValue(node, backing) {
7512
- const v = hasActiveOverride(node)
7513
- ? unwrapOverride(node._x?._overrideValue)
7514
- : node._pendingValue !== NOT_PENDING && inOwnerContext()
7515
- ? node._pendingValue
7516
- : backing;
8624
+ // latest() sees the in-flight parked value like an owner-context reader
8625
+ // does (#3075) — signal/memo parity for store-node-backed keys.
8626
+ // Authoritative-view reads (until()'s predicate) skip the override arm
8627
+ // only: staged pending values are authoritative, overrides are the
8628
+ // caller's optimism.
8629
+ const v =
8630
+ !authoritativeServe() && hasActiveOverride(node)
8631
+ ? unwrapOverride(node._x?._overrideValue)
8632
+ : node._pendingValue !== NOT_PENDING &&
8633
+ (latestReadActive ||
8634
+ // Owner-context pending visibility — except HELD truth (#3164,
8635
+ // see CONFIG_HELD_TRUTH: fold-staged or entangle-stolen
8636
+ // confirming truth), which only authoritative/latest readers
8637
+ // see (core read()'s A17-for-held-truth twin; ordinary readers
8638
+ // keep committed until the transaction's reveal — latest() is
8639
+ // exempted by the leading arm above).
8640
+ ((inOwnerContext() || authoritativeServe()) &&
8641
+ !(node._config & CONFIG_HELD_TRUTH && !authoritativeServe())))
8642
+ ? node._pendingValue
8643
+ : backing;
7517
8644
  return v === FORCE ? backing : v;
7518
8645
  }
7519
8646
  /** Serve an own data key: node-first when a node exists (pending visibility,
@@ -7538,13 +8665,18 @@ function serveDataKey(target, key, backingValue, src, node) {
7538
8665
  read(getNode(target, key, backingValue));
7539
8666
  }
7540
8667
  }
7541
- return optHooks.optimisticView(target, src).length;
8668
+ // Truth authors read the backing's own length — an optimistic row from
8669
+ // the caller's transaction must not shift where the author's next write
8670
+ // lands (#3108).
8671
+ return (authoritativeServe() ? src : optHooks.optimisticView(target, src)).length;
7542
8672
  }
7543
8673
  if (inDraft(target)) {
7544
8674
  // Optimistic drafts before their first write have no pending backing yet;
7545
8675
  // reads must still see the live optimistic view (compose, not clobber —
7546
8676
  // #2951). Once ensurePB runs, the seeded clone carries the view.
7547
- if (target.fam?.opt && target.pb === null) {
8677
+ // AUTHORITATIVE drafts (projection derive) never overlay ensurePB's
8678
+ // seeding rule, applied to the read side (#3108).
8679
+ if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
7548
8680
  const node = target.n?.[key];
7549
8681
  if (node !== undefined && hasActiveOverride(node))
7550
8682
  v = unwrapOverride(node._x?._overrideValue);
@@ -7602,6 +8734,27 @@ function firewallGate(target) {
7602
8734
  const fw = target.fam?.node;
7603
8735
  if (fw != null && fw._statusFlags & (STATUS_UNINITIALIZED | STATUS_ERROR)) read(fw);
7604
8736
  }
8737
+ /** latest() pull (#3075): bring the projection computed up to date so the
8738
+ * read serves the IN-FLIGHT derivation — signal/memo parity, where core
8739
+ * read() routes latest() through a companion that recomputes speculatively.
8740
+ * The latest flag is suspended for the recompute (the derive's own reads
8741
+ * are normal reads), and latestPullActive marks any adoption it commits as
8742
+ * staged (see adoptPB) — the speculative swap must not leak to
8743
+ * committed-visibility readers before the flush. */
8744
+ function pullProjectionForLatest(target) {
8745
+ const fw = target.fam.node;
8746
+ if (fw == null) return;
8747
+ const prevLatest = latestReadActive;
8748
+ setLatestReadActive(false);
8749
+ const prevPull = latestPullActive;
8750
+ latestPullActive = true;
8751
+ try {
8752
+ prepareComputed(fw, true);
8753
+ } finally {
8754
+ latestPullActive = prevPull;
8755
+ setLatestReadActive(prevLatest);
8756
+ }
8757
+ }
7605
8758
  const traps = {
7606
8759
  get(target, key, receiver) {
7607
8760
  // One typeof gates every brand-symbol compare off the hot string path
@@ -7629,6 +8782,11 @@ const traps = {
7629
8782
  }
7630
8783
  if (pendingCheckActive) witnessAffectsMark(target, key);
7631
8784
  if (target.fam !== null && getObserver() === null && !inDraft(target)) firewallGate(target);
8785
+ // latest() pull (#3075): store traps never reach core read() without an
8786
+ // observer, so bring the projection computed up to date here — signal/
8787
+ // memo parity for latest() reads through a projection.
8788
+ if (target.fam !== null && latestReadActive && !inDraft(target) && !getWriteOverride())
8789
+ pullProjectionForLatest(target);
7632
8790
  const src = readSource(target);
7633
8791
  // Overlay delete (#3044): a prototype overlay cannot shadow a delete, so
7634
8792
  // deleted keys are tracked aside and read as absent in the pending view.
@@ -7727,7 +8885,15 @@ const traps = {
7727
8885
  if (target.s) return serveShallow(target, key, nv);
7728
8886
  return isWrappable(nv) ? draftServe(target, wrapNext(nv, target, key)) : nv;
7729
8887
  }
7730
- } else if (v === undefined && inDraft(target) && target.fam?.opt && target.pb === null) {
8888
+ } else if (
8889
+ v === undefined &&
8890
+ inDraft(target) &&
8891
+ target.fam?.opt &&
8892
+ target.pb === null &&
8893
+ // AUTHORITATIVE drafts (landing folds) never seed from overrides —
8894
+ // the caller's optimism is not truth (has-trap twin below).
8895
+ !authoritativeServe()
8896
+ ) {
7731
8897
  const node = target.n?.[key];
7732
8898
  if (node !== undefined && hasActiveOverride(node))
7733
8899
  v = unwrapOverride(node._x?._overrideValue);
@@ -7754,14 +8920,16 @@ const traps = {
7754
8920
  if (!inDraft(target)) {
7755
8921
  if (getObserver() !== null) {
7756
8922
  const node = getHasNode(target, key, present);
8923
+ // Authoritative-view readers get the right answer for free: core read()
8924
+ // skips the override arm for them, so nv is authoritative presence.
7757
8925
  const nv = read(node);
7758
8926
  if (hasActiveOverride(node)) present = !!nv;
7759
- } else {
8927
+ } else if (!authoritativeServe()) {
7760
8928
  const node = target.h?.[key];
7761
8929
  if (node !== undefined && hasActiveOverride(node))
7762
8930
  present = !!unwrapOverride(node._x?._overrideValue);
7763
8931
  }
7764
- } else if (target.fam?.opt && target.pb === null) {
8932
+ } else if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
7765
8933
  const node = target.h?.[key];
7766
8934
  if (node !== undefined && hasActiveOverride(node))
7767
8935
  present = !!unwrapOverride(node._x?._overrideValue);
@@ -7787,8 +8955,14 @@ const traps = {
7787
8955
  // Optimistic membership overlay: presence-node overrides add/remove keys
7788
8956
  // (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
7789
8957
  // Draft reads before the first write overlay too (pb, once created, is
7790
- // seeded with the view).
7791
- if (target.fam?.opt && target.h !== null && (!inDraft(target) || target.pb === null)) {
8958
+ // seeded with the view). Authoritative-view reads (until()'s predicate,
8959
+ // truth-author drafts) skip the overlay.
8960
+ if (
8961
+ !authoritativeServe() &&
8962
+ target.fam?.opt &&
8963
+ target.h !== null &&
8964
+ (!inDraft(target) || target.pb === null)
8965
+ ) {
7792
8966
  let set = null;
7793
8967
  for (const key of Reflect.ownKeys(target.h)) {
7794
8968
  const node = target.h[key];
@@ -7810,7 +8984,7 @@ const traps = {
7810
8984
  if (target.del !== null && target.del.has(key)) return undefined;
7811
8985
  if (desc === undefined) desc = Object.getOwnPropertyDescriptor(target.v, key);
7812
8986
  }
7813
- if (target.fam?.opt && !inDraft(target)) {
8987
+ if (!authoritativeServe() && target.fam?.opt && !inDraft(target)) {
7814
8988
  const node = target.h?.[key];
7815
8989
  if (node !== undefined && hasActiveOverride(node)) {
7816
8990
  if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
@@ -7852,14 +9026,15 @@ const traps = {
7852
9026
  // Array length writes implicitly delete indices — the written-keys bound
7853
9027
  // can't see them, so poison to the full scan for this batch. Index
7854
9028
  // writes implicitly GROW length, so arrays always record it alongside.
9029
+ const pcs = pcOf(target);
7855
9030
  if (Array.isArray(pb)) {
7856
- if (key === "length") target.wk = WK_ALL;
7857
- else if (target.wk !== WK_ALL) {
7858
- const wk = (target.wk ??= new Set());
9031
+ if (key === "length") pcs.wk = WK_ALL;
9032
+ else if (pcs.wk !== WK_ALL) {
9033
+ const wk = (pcs.wk ??= new Set());
7859
9034
  wk.add(key);
7860
9035
  wk.add("length");
7861
9036
  }
7862
- } else if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
9037
+ } else if (pcs.wk !== WK_ALL) (pcs.wk ??= new Set()).add(key);
7863
9038
  // Own data keys literally named "prototype"/"constructor" land as data —
7864
9039
  // defineProperty sidesteps a proto-chain setter named the same.
7865
9040
  if (UNSAFE_KEYS.has(key)) {
@@ -7897,12 +9072,20 @@ const traps = {
7897
9072
  const override = !draft && getWriteOverride();
7898
9073
  if (!draft && !override) return true;
7899
9074
  if (key === "__proto__") return true;
7900
- if (desc.get || desc.set) target.a = true;
9075
+ if (desc.get || desc.set) {
9076
+ target.a = true;
9077
+ // Accessor demotion (re-audit blocker 3): a record that acquires an
9078
+ // accessor after patch registration stops being patchable — pull its
9079
+ // patches and re-drive them as tracked effect fallbacks. Hooks are
9080
+ // installed whenever pc.p exists (registration installs them).
9081
+ if (target.pc !== null && target.pc.p !== null) patchHooks.demoteToEffects(target);
9082
+ }
7901
9083
  // Unwrap before ensurePB (see the set trap: self-reference materializes).
7902
9084
  if ("value" in desc) desc = { ...desc, value: unwrapValue(desc.value) };
7903
9085
  const pb = ensurePB(target);
7904
9086
  pendingNotify.add(target);
7905
- if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
9087
+ const pcd = pcOf(target);
9088
+ if (pcd.wk !== WK_ALL) (pcd.wk ??= new Set()).add(key);
7906
9089
  Object.defineProperty(pb, key, desc);
7907
9090
  if (target.del !== null) target.del.delete(key);
7908
9091
  if (override) notifyWrites(target);
@@ -7914,7 +9097,8 @@ const traps = {
7914
9097
  if (!draft && !override) return true;
7915
9098
  const pb = ensurePB(target);
7916
9099
  pendingNotify.add(target);
7917
- if (target.wk !== WK_ALL) (target.wk ??= new Set()).add(key);
9100
+ const pcx = pcOf(target);
9101
+ if (pcx.wk !== WK_ALL) (pcx.wk ??= new Set()).add(key);
7918
9102
  delete pb[key];
7919
9103
  // A prototype overlay cannot shadow a delete of a committed key —
7920
9104
  // record it aside (#3044); reads/has/ownKeys/commit consult the set.
@@ -7987,8 +9171,39 @@ function createStoreNext(init, shallow = false) {
7987
9171
  const setter = fn => storeSetterNext(proxy, fn);
7988
9172
  return [proxy, setter];
7989
9173
  }
9174
+ /** True when `proxy` is a SHALLOW store (children served verbatim, slots
9175
+ * replaced by reference — #2932). The list driver uses this to choose the
9176
+ * slot-patch channel (collected row bodies) over per-record registration. */
9177
+ function storeIsShallow(proxy) {
9178
+ const t = proxy?.[$TARGET];
9179
+ return t !== undefined && t.s === true;
9180
+ }
9181
+ /** True when `proxy` belongs to a projection/optimistic FAMILY. The list
9182
+ * driver must DECLINE family arrays (external audit finding): family
9183
+ * structural changes never emit row/slot ops (the setter channel is
9184
+ * fam-gated; optimistic writes ride node overrides), and the proxy identity
9185
+ * is stable so the each-watch cannot catch the change either — an engaged
9186
+ * list would freeze on optimistic/projection structural updates. Record-
9187
+ * level family patches are unaffected (they have their own emission). */
9188
+ function storeHasFamily(proxy) {
9189
+ const t = proxy?.[$TARGET];
9190
+ return t !== undefined && t.fam !== null;
9191
+ }
9192
+ /** True when `proxy` belongs to an OPTIMISTIC family specifically. The list
9193
+ * driver declines these (audit finding, narrowed): optimistic user writes
9194
+ * ride node-level overrides — they never enter the reconcile walk, so no
9195
+ * row/slot ops are emitted and an engaged list would freeze on optimistic
9196
+ * structural changes. PROJECTION (non-optimistic) families are drivable:
9197
+ * their recomputes go through the reconcile walk, whose emissions are
9198
+ * transition-stamped in the apply queue like any other (equivalence-matrix
9199
+ * gated). Re-admitting optimistic families requires a lane-timed structural
9200
+ * emission mirroring emitPatchOptimistic, plus revert resync. */
9201
+ function storeHasOptimisticFamily(proxy) {
9202
+ const t = proxy?.[$TARGET];
9203
+ return t !== undefined && t.fam?.opt === true;
9204
+ }
7990
9205
  /** Tracking deep snapshot (`deep()` for next targets): subscribes to the
7991
- * key-set and every property node at every reachable level, then returns the
9206
+ * key-set and deep-witness node at every reachable level, then returns the
7992
9207
  * plain view. Shared references and cycles handled via the visited set. */
7993
9208
  function deepNext(value) {
7994
9209
  const t0 = value?.[$TARGET];
@@ -8188,7 +9403,7 @@ function reconcileNextState(value, state, key, replace = false) {
8188
9403
  // positional so old-entity subtrees never merge into the new entity's).
8189
9404
  const prev = t.pb ?? t.v;
8190
9405
  const eq = keyFn(prev);
8191
- if (eq !== undefined && keyFn(incoming) !== eq) {
9406
+ if (eq !== undefined && !sameKey(keyFn(incoming), eq)) {
8192
9407
  if (!replace) throw new Error("Cannot reconcile states with different identity");
8193
9408
  // Entity change: wholesale swap. The root proxy is stable for life
8194
9409
  // (proj R5) but NOTHING below survives — children are never matched
@@ -8229,6 +9444,33 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8229
9444
  const shallow = t.s === true;
8230
9445
  const old = t.v;
8231
9446
  adoptPB(t, incoming, eager);
9447
+ // Patch channel (adoption site): this record transitioned — queue its
9448
+ // patches with the pre-adopt prev. No bubbling walk: the adoption walk
9449
+ // visits parents before children, so ancestors emitted already. EAGER
9450
+ // only — family targets' visibility moment is their fold commit
9451
+ // (drainFolds emits there; emitting here too would double-fire).
9452
+ if (patchHooks !== null && eager && t.pc !== null && t.pc.p !== null) {
9453
+ // Accessor demotion at the ADOPTION seam is DEV-ONLY (prod principle:
9454
+ // explicitly-odd input must not cost correct-input prod — the
9455
+ // per-adoption scan was ~12% of dbmon's tick since adoptPB resets the
9456
+ // verdict every adoption). Dev demotes AND warns; prod emits directly,
9457
+ // so a getter adoptee's OUTSIDE deps (signals) won't re-apply in prod —
9458
+ // caught loudly during development instead. Registration-time admission
9459
+ // (patchableRaw) keeps its full one-time scan in both modes.
9460
+ if (!targetIsPlain(t)) {
9461
+ console.warn(
9462
+ "A reconcile adopted an object with own getters into a record that " +
9463
+ "carries compiled patches. Patches read raw values and will not " +
9464
+ "track the getters' reactive dependencies — this record's patches " +
9465
+ "are demoted to effects in development, but production will NOT " +
9466
+ "demote. Avoid getters on patched records, or key them out of " +
9467
+ "patch-eligible templates."
9468
+ );
9469
+ patchHooks.demoteToEffects(t);
9470
+ } else {
9471
+ patchHooks.emitPatchLocal(t, incoming, old);
9472
+ }
9473
+ }
8232
9474
  // Shallow adoption: records are slot values — sticky raw-mark the incoming
8233
9475
  // set (R41) and never descend; slot notification is the positional diff.
8234
9476
  if (shallow) markRawIngest(incoming);
@@ -8268,7 +9510,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8268
9510
  typeof pvRaw === "object" &&
8269
9511
  nv !== null &&
8270
9512
  typeof nv === "object" &&
8271
- keyFn(pvRaw) === keyFn(nv)
9513
+ sameKey(keyFn(pvRaw), keyFn(nv))
8272
9514
  )
8273
9515
  )
8274
9516
  break; // misaligned: fall to the keyed remainder below
@@ -8296,6 +9538,7 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8296
9538
  }
8297
9539
  }
8298
9540
  if (t.dk !== null && !dkBumpedA && i < nextRows.length) bumpDeep(t);
9541
+ const structStart = i; // misalignment point (== nlen on aligned ticks)
8299
9542
  let prevByKey = null;
8300
9543
  for (; i < nextRows.length; i++) {
8301
9544
  const nv = nextRows[i];
@@ -8305,16 +9548,39 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8305
9548
  let pv;
8306
9549
  if (nk !== undefined) {
8307
9550
  if (prevByKey === null) {
9551
+ // Occurrence-aware (re-audit 2, P1-5): duplicate keys queue
9552
+ // their prev INDICES (rows can themselves be arrays, so index
9553
+ // queues are the unambiguous encoding — same as buildRowOps)
9554
+ // and each is consumed ONCE. First-wins would adopt two next
9555
+ // rows into the SAME prev target while row ops retain two
9556
+ // separate DOM rows (the second one stale).
8308
9557
  prevByKey = new Map();
8309
- for (let j = 0; j < prevRows.length; j++) {
9558
+ // From structStart, not 0 (re-audit 3, P1-2): prefix-aligned
9559
+ // rows already adopted their incoming counterparts — re-offering
9560
+ // them here let a duplicate key adopt a prefix row AGAIN while
9561
+ // row ops (which correctly window from structStart) retained
9562
+ // the later occurrence's DOM row against a never-adopted target.
9563
+ for (let j = structStart; j < prevRows.length; j++) {
8310
9564
  const p = unwrapValue(prevRows[j]);
8311
9565
  if (p !== null && typeof p === "object") {
8312
9566
  const pk = keyFn(p);
8313
- if (pk !== undefined && !prevByKey.has(pk)) prevByKey.set(pk, p);
9567
+ if (pk === undefined) continue;
9568
+ const existing = prevByKey.get(pk);
9569
+ if (existing === undefined) prevByKey.set(pk, j);
9570
+ else if (Array.isArray(existing)) existing.push(j);
9571
+ else prevByKey.set(pk, [existing, j]);
8314
9572
  }
8315
9573
  }
8316
9574
  }
8317
- pv = prevByKey.get(nk);
9575
+ const m = prevByKey.get(nk);
9576
+ if (m === undefined) pv = undefined;
9577
+ else if (Array.isArray(m)) {
9578
+ pv = unwrapValue(prevRows[m.shift()]);
9579
+ if (m.length === 1) prevByKey.set(nk, m[0]);
9580
+ } else {
9581
+ pv = unwrapValue(prevRows[m]);
9582
+ prevByKey.delete(nk);
9583
+ }
8318
9584
  } else {
8319
9585
  pv = unwrapValue(prevRows[i]); // keyless item: positional fallback
8320
9586
  }
@@ -8328,12 +9594,62 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8328
9594
  }
8329
9595
  }
8330
9596
  }
9597
+ // Row ops (PR-B): emit structural ops ONLY when structure changed —
9598
+ // aligned value ticks pay nothing. Built after the walk so retained
9599
+ // rows' value patches queue first (adds bind at op-apply).
9600
+ if (
9601
+ rowHooks !== null &&
9602
+ t.pc !== null &&
9603
+ t.pc.ro !== null &&
9604
+ (structStart < nlen || plen !== nlen)
9605
+ )
9606
+ buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn);
8331
9607
  } else {
8332
9608
  const dlen = Math.min(prevRows.length, nextRows.length);
8333
9609
  const nlen = nextRows.length;
8334
9610
  let dkBumpedP = false;
9611
+ const sp = rowHooks !== null && t.pc !== null ? t.pc.sp : null;
9612
+ // Row ops for shallow/positional lists: track the key-aligned prefix
9613
+ // (keyed) so aligned value ticks emit nothing; keyless lists emit only
9614
+ // on length change (append/truncate). Slot-patch consumers need the
9615
+ // alignment tracking too (aligned = value tick, misaligned = ops).
9616
+ const ro = rowHooks !== null && t.pc !== null ? t.pc.ro : null;
9617
+ let keyAligned = keyFn !== null && (ro !== null || sp !== null);
9618
+ let keyPrefix = 0;
8335
9619
  for (let i = 0; i < nlen; i++) {
8336
9620
  const nvP = nextRows[i];
9621
+ if (keyAligned && i < dlen) {
9622
+ const pvK = prevRows[i];
9623
+ if (
9624
+ pvK !== null &&
9625
+ typeof pvK === "object" &&
9626
+ nvP !== null &&
9627
+ typeof nvP === "object" &&
9628
+ // SameValueZero (self-sweep): strict === here broke slot
9629
+ // alignment on NaN keys while buildRowOps retained the row —
9630
+ // retained DOM with suppressed value ticks (the round-1 NaN
9631
+ // staleness, in the shallow branch).
9632
+ sameKey(keyFn(pvK), keyFn(nvP))
9633
+ )
9634
+ keyPrefix++;
9635
+ else keyAligned = false;
9636
+ }
9637
+ // Slot-patch dispatch (shallow): a KEY-ALIGNED slot whose value was
9638
+ // replaced by reference is a value tick — emit through the queue.
9639
+ // Misaligned/appended slots are STRUCTURE (row ops rebuild or move
9640
+ // them; new rows initial-apply at bind), so they emit nothing here.
9641
+ // Keyless positional lists treat same-index replacement as the value
9642
+ // tick for indices below the common length.
9643
+ // `i < dlen` is load-bearing for BOTH modes: an appended position
9644
+ // past a fully-aligned prefix (vacuously aligned when prev is empty)
9645
+ // has no previous slot — emitting a slot tick for it races the row
9646
+ // ops that CREATE the row (the slot queue applies first, indexing a
9647
+ // row that does not exist yet). Equivalence-matrix finding:
9648
+ // clear-then-refill and pure appends crashed the driver.
9649
+ if (sp !== null && i < dlen && (keyFn === null || keyAligned)) {
9650
+ const pvS = prevRows[i];
9651
+ if (pvS !== nvP) rowHooks.emitSlotPatch(t, i, nvP, pvS);
9652
+ }
8337
9653
  if (!shallow && i < dlen && nvP !== null && typeof nvP === "object")
8338
9654
  descend(unwrapValue(prevRows[i]), nvP, keyFn, fam, proj);
8339
9655
  if (
@@ -8354,6 +9670,15 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8354
9670
  }
8355
9671
  }
8356
9672
  }
9673
+ if (ro !== null) {
9674
+ const plen = prevRows.length;
9675
+ if (keyFn !== null) {
9676
+ if (keyPrefix < nlen || plen !== nlen)
9677
+ buildAndEmitRowOps(t, prevRows, nextRows, keyPrefix, keyFn);
9678
+ } else if (plen !== nlen) {
9679
+ buildAndEmitRowOps(t, prevRows, nextRows, dlen, null);
9680
+ }
9681
+ }
8357
9682
  }
8358
9683
  if (eager) {
8359
9684
  if (nodes !== null && nodesHit < t.nc) {
@@ -8374,6 +9699,22 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8374
9699
  // slots must not notify, R9). This replaces the notifyFold re-walk that
8375
9700
  // doubled dbmon's diff cost. for-in covers own enumerable string keys
8376
9701
  // with no key-array allocation; symbols get a pass only when present.
9702
+ // PROTOTYPE compiled-patch fast path: a pure-patch record (no nodes,
9703
+ // no presence/key-set/deep subscribers, no family) adopts and hands the
9704
+ // (next, prev) pair to its compiled patch — no per-key walk at all.
9705
+ if (
9706
+ t.pc !== null &&
9707
+ t.pc.p !== null &&
9708
+ eager &&
9709
+ t.n === null &&
9710
+ t.h === null &&
9711
+ t.k === null &&
9712
+ t.dk === null &&
9713
+ fam === null
9714
+ ) {
9715
+ // Adoption already ran at applyAdopt entry; emission was queued there.
9716
+ return;
9717
+ }
8377
9718
  const nodes = eager ? t.n : null;
8378
9719
  let nodesHit = 0;
8379
9720
  let dkBumped = false;
@@ -8439,6 +9780,93 @@ function applyAdopt(t, incoming, keyFn, proj = false) {
8439
9780
  }
8440
9781
  }
8441
9782
  const hasOwnP = Object.prototype.hasOwnProperty;
9783
+ /** Setter-channel row ops (the fold site calls this for array targets with
9784
+ * ops consumers): structural mutation through the setter — push/splice/index
9785
+ * assignment/permutation — is a visibility transition for the list container
9786
+ * just like a reconcile walk, and drivers consuming registerRowOps must see
9787
+ * it. Setter mutations move the SAME row objects around, so RAW IDENTITY is
9788
+ * the key. Aligned arrays (value-only folds) emit nothing. */
9789
+ const identityKey = r => unwrapValue(r);
9790
+ /** Key equality for EVERY key comparison in this module (re-audit 2, P1-5):
9791
+ * SameValueZero, matching the Map-based matchers (buildRowOps, the adoption
9792
+ * window) — NaN keys are equal to themselves, so aligned NaN rows stay
9793
+ * aligned in the prefix walk instead of forever misaligning. Adoption and
9794
+ * row ops MUST agree on key equality or retained DOM rows go stale. */
9795
+ function sameKey(a, b) {
9796
+ return a === b || (a !== a && b !== b);
9797
+ }
9798
+ function emitSetterRowOps(t, prevRows, nextRows) {
9799
+ const ops = buildIdentityRowOps(prevRows, nextRows);
9800
+ if (ops !== null) rowHooks.emitRowOps(t, nextRows, ops);
9801
+ }
9802
+ /** Identity-keyed structural diff, returned rather than emitted: shared by
9803
+ * the setter channel (regular queue) and the OPTIMISTIC write channel (lane
9804
+ * queue) — same retention semantics, different dispatch timing. Returns
9805
+ * null when the lists are identity-aligned (no structure changed). */
9806
+ function buildIdentityRowOps(prevRows, nextRows) {
9807
+ let p = 0;
9808
+ const min = prevRows.length < nextRows.length ? prevRows.length : nextRows.length;
9809
+ while (p < min && unwrapValue(prevRows[p]) === unwrapValue(nextRows[p])) p++;
9810
+ if (p === prevRows.length && p === nextRows.length) return null;
9811
+ return buildRowOps(prevRows, nextRows, p, identityKey);
9812
+ }
9813
+ /** Shared row-ops builder (keyed deep branch + shallow/positional branch):
9814
+ * key-matches the misaligned window into { prefix, sources, removed }.
9815
+ * `keyFn === null` degrades to positional ops (append/truncate only). */
9816
+ function buildAndEmitRowOps(t, prevRows, nextRows, structStart, keyFn) {
9817
+ rowHooks.emitRowOps(t, nextRows, buildRowOps(prevRows, nextRows, structStart, keyFn));
9818
+ }
9819
+ function buildRowOps(prevRows, nextRows, structStart, keyFn) {
9820
+ const plen = prevRows.length;
9821
+ const nlen = nextRows.length;
9822
+ const sources = new Array(nlen - structStart);
9823
+ // Occurrence-aware matching (re-audit): duplicate keys queue their old
9824
+ // indices and each is consumed ONCE — first-wins reuse would hand the same
9825
+ // source (and its one DOM row) to multiple next positions. The no-dup fast
9826
+ // shape stays a bare number; collisions upgrade to a queue.
9827
+ let oldIndexByKey = null;
9828
+ if (keyFn !== null && structStart < plen) {
9829
+ oldIndexByKey = new Map();
9830
+ for (let j = structStart; j < plen; j++) {
9831
+ const p = unwrapValue(prevRows[j]);
9832
+ if (p !== null && typeof p === "object") {
9833
+ const pk = keyFn(p);
9834
+ if (pk === undefined) continue;
9835
+ const existing = oldIndexByKey.get(pk);
9836
+ if (existing === undefined) oldIndexByKey.set(pk, j);
9837
+ else if (Array.isArray(existing)) existing.push(j);
9838
+ else oldIndexByKey.set(pk, [existing, j]);
9839
+ }
9840
+ }
9841
+ }
9842
+ const consumed = oldIndexByKey !== null ? new Set() : null;
9843
+ for (let k = structStart; k < nlen; k++) {
9844
+ const nv = nextRows[k];
9845
+ let oldIdx = -1;
9846
+ if (nv !== null && typeof nv === "object" && oldIndexByKey !== null) {
9847
+ const nk = keyFn(nv);
9848
+ if (nk !== undefined) {
9849
+ const m = oldIndexByKey.get(nk);
9850
+ if (m !== undefined) {
9851
+ if (Array.isArray(m)) {
9852
+ oldIdx = m.shift();
9853
+ if (m.length === 1) oldIndexByKey.set(nk, m[0]);
9854
+ } else {
9855
+ oldIdx = m;
9856
+ oldIndexByKey.delete(nk);
9857
+ }
9858
+ consumed.add(oldIdx);
9859
+ }
9860
+ }
9861
+ }
9862
+ sources[k - structStart] = oldIdx;
9863
+ }
9864
+ const removed = [];
9865
+ for (let j = structStart; j < plen; j++) {
9866
+ if (consumed === null || !consumed.has(j)) removed.push(unwrapValue(prevRows[j]));
9867
+ }
9868
+ return { prefix: structStart, sources, removed };
9869
+ }
8442
9870
  function descend(pv, nv, keyFn, fam, proj = false) {
8443
9871
  if (pv === null || typeof pv !== "object" || nv === null || typeof nv !== "object") return;
8444
9872
  // Lookup FIRST: a hit implies pv was wrappable and never raw-marked (only
@@ -8462,7 +9890,10 @@ function descend(pv, nv, keyFn, fam, proj = false) {
8462
9890
  const nk = keyFn(nv);
8463
9891
  // Key mismatch detaches: the slot takes the new entity; the old proxy
8464
9892
  // keeps its (old) backing and a fresh proxy wraps the new value on read.
8465
- if (pk !== undefined && nk !== undefined && pk !== nk) return;
9893
+ // SameValueZero (re-audit 2, P1-5): NaN keys are self-equal strict
9894
+ // inequality detached every NaN-keyed slot on every tick while the
9895
+ // Map-based row-ops matcher retained its DOM row (stale forever).
9896
+ if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return;
8466
9897
  }
8467
9898
  // Reachability pruning (§6d) is MODE-dependent, both pinned:
8468
9899
  // - keyed matching descends only where subscriptions exist at/below (`d`) —
@@ -8516,7 +9947,8 @@ function descend(pv, nv, keyFn, fam, proj = false) {
8516
9947
  * driven from inside an enclosing authoritative-write scope (next-store
8517
9948
  * optimistic derives), and a hard `false` would clobber it mid-derive.
8518
9949
  */
8519
- function wrapDraft(inner, isActive, onDraftWrite) {
9950
+ function wrapDraft(inner, isActive, aroundWrite) {
9951
+ const write = op => (aroundWrite ? aroundWrite(op) : op());
8520
9952
  const traps = {
8521
9953
  get(_, prop) {
8522
9954
  let value;
@@ -8531,7 +9963,7 @@ function wrapDraft(inner, isActive, onDraftWrite) {
8531
9963
  }
8532
9964
  if (prop === $TARGET) return value;
8533
9965
  return typeof value === "object" && value !== null
8534
- ? wrapDraft(value, isActive, onDraftWrite)
9966
+ ? wrapDraft(value, isActive, aroundWrite)
8535
9967
  : value;
8536
9968
  },
8537
9969
  has(_, prop) {
@@ -8553,8 +9985,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
8553
9985
  setWriteOverride(true);
8554
9986
  setProjectionWriteActive(true);
8555
9987
  try {
8556
- inner[prop] = value;
8557
- onDraftWrite?.();
9988
+ write(() => {
9989
+ inner[prop] = value;
9990
+ });
8558
9991
  } finally {
8559
9992
  setWriteOverride(false);
8560
9993
  setProjectionWriteActive(was);
@@ -8567,8 +10000,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
8567
10000
  setWriteOverride(true);
8568
10001
  setProjectionWriteActive(true);
8569
10002
  try {
8570
- delete inner[prop];
8571
- onDraftWrite?.();
10003
+ write(() => {
10004
+ delete inner[prop];
10005
+ });
8572
10006
  } finally {
8573
10007
  setWriteOverride(false);
8574
10008
  setProjectionWriteActive(was);
@@ -8609,8 +10043,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
8609
10043
  setWriteOverride(true);
8610
10044
  setProjectionWriteActive(true);
8611
10045
  try {
8612
- Reflect.defineProperty(inner, prop, desc);
8613
- onDraftWrite?.();
10046
+ write(() => {
10047
+ Reflect.defineProperty(inner, prop, desc);
10048
+ });
8614
10049
  } finally {
8615
10050
  setWriteOverride(false);
8616
10051
  setProjectionWriteActive(was);
@@ -8662,7 +10097,7 @@ function createStoreDerivedNext(fn, seed, options) {
8662
10097
  }
8663
10098
  ];
8664
10099
  }
8665
- function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWrite) {
10100
+ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraftWrite) {
8666
10101
  const owner = getOwner();
8667
10102
  let settled = false;
8668
10103
  let result;
@@ -8676,7 +10111,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
8676
10111
  const draft = wrapDraft(
8677
10112
  wrappedStore,
8678
10113
  () => !settled || owner._x?._inFlight === result,
8679
- onDraftWrite
10114
+ aroundDraftWrite
8680
10115
  );
8681
10116
  storeSetterNext(
8682
10117
  draft,
@@ -8691,7 +10126,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
8691
10126
  if (v === s || v === undefined) return;
8692
10127
  const write = () =>
8693
10128
  storeSetterNext(wrappedStore, st => reconcileNextState(v, st, key, true), false);
8694
- wrapCommit ? wrapCommit(write) : write();
10129
+ wrapCommit ? wrapCommit(write, v) : write();
8695
10130
  };
8696
10131
  const sync = handleAsync(owner, result, commit);
8697
10132
  if (!owner._loading) commit(sync);
@@ -8701,6 +10136,558 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
8701
10136
  return owner;
8702
10137
  }
8703
10138
 
10139
+ /**
10140
+ * PR-A: the patch channel (DESIGN-PATCH-CHANNEL.md).
10141
+ *
10142
+ * Compiled patch functions — per-record compare-and-write consumers —
10143
+ * dispatched by the store's visibility transitions instead of render
10144
+ * effects. This module owns registration, the per-flush apply queue
10145
+ * (effect-phase timing, §2b), the owned-prev rule (§2c), and dispatch
10146
+ * bubbling (§4b). Emission calls live at the four visibility-transition
10147
+ * sites (adoption walk, setter notify, fold commit, override lifecycle)
10148
+ * and are gated on registration, so unpatched stores pay a null check.
10149
+ *
10150
+ * Bubbling contract: a targeted nested write reaches ancestor patches as a
10151
+ * FORCED re-apply — the third `force` argument makes every compiled compare
10152
+ * pass, so the ancestor rewrites its bound fields from its current backing
10153
+ * (idempotent, and prev-free: an ancestor's pre-state is not reconstructible
10154
+ * after in-place folds). Compiled bodies therefore have the signature
10155
+ * `(next, prev, force?)`.
10156
+ *
10157
+ * Tree-shaking: core never imports this module; stores without patches
10158
+ * never schedule the queue.
10159
+ */
10160
+ let queue = null;
10161
+ let scheduled = false;
10162
+ function drainApplyQueue() {
10163
+ // Settle-time fallback for optimistic emissions (a reverting flush may
10164
+ // have no active lanes left to run the lane-slot drain).
10165
+ drainOptimistic();
10166
+ const q = queue;
10167
+ queue = null;
10168
+ scheduled = false;
10169
+ if (q === null) return;
10170
+ // Per-entry isolation: one throwing patch must not abort its siblings
10171
+ // (effect parity — each effect isolates its failure). A throwing patch
10172
+ // routes through its REGISTERING OWNER's queue chain exactly like a
10173
+ // render-effect error (§2b): an Errored boundary above the row collects
10174
+ // it (source = the owner, error read via owner._x?._error). Unhandled errors
10175
+ // rethrow after the drain so they still surface.
10176
+ let firstError = UNSET;
10177
+ for (let i = 0; i < q.length; i++) {
10178
+ clearStamp(q[i]);
10179
+ const { list, prev, force, t } = q[i];
10180
+ const next = t !== null ? (t.pb ?? t.v) : q[i].next;
10181
+ firstError = applyEntries(list, next, prev, force, firstError);
10182
+ }
10183
+ if (firstError !== UNSET) {
10184
+ // Unhandled patch errors HALT like unhandled effect errors (re-audit 2,
10185
+ // P1-4): app state is undefined past an unboundaried throw.
10186
+ haltReactivity(firstError);
10187
+ throw firstError;
10188
+ }
10189
+ }
10190
+ const UNSET = Symbol();
10191
+ /** ONE callback/error primitive for every drain (normal, transition-held,
10192
+ * optimistic): per-entry isolation — a throwing patch must not abort its
10193
+ * siblings (effect parity) — and failures route through the REGISTERING
10194
+ * OWNER's queue chain exactly like a render-effect error (§2b): an Errored
10195
+ * boundary above the row collects it. Unhandled errors are aggregated by the
10196
+ * caller (first one rethrows after its drain completes). */
10197
+ function applyEntries(list, next, prev, force, firstError) {
10198
+ // SNAPSHOT multi-consumer lists (re-audit 5, P1-3): a callback can dispose
10199
+ // a sibling's owner, whose unbind SPLICES this same array mid-iteration —
10200
+ // index-walking the live array skips the shifted consumer. The dominant
10201
+ // single-consumer case pays nothing; unbound entries are marked so a
10202
+ // snapshot never applies a consumer severed by an earlier callback.
10203
+ const snap = list.length > 1 ? list.slice() : list;
10204
+ for (let j = 0; j < snap.length; j++) {
10205
+ const entry = snap[j];
10206
+ if (entry.u === true) continue;
10207
+ // Disposed owners drop their patches (the row unmounted mid-flush).
10208
+ if (entry.owner !== null && isDisposed(entry.owner)) continue;
10209
+ try {
10210
+ entry.fn(next, prev, force);
10211
+ } catch (err) {
10212
+ let handled = false;
10213
+ const owner = entry.owner;
10214
+ if (owner !== null) {
10215
+ // Route through the nearest COMPUTED ancestor (re-audit 2, P1-4):
10216
+ // <Errored>.reset() recomputes its sources, and a plain owner (the
10217
+ // list driver's listOwner) is not recomputable — the component/memo
10218
+ // scope above it is, and recomputing it rebuilds the rows, exactly
10219
+ // what reset means for a throwing render effect.
10220
+ let source = owner;
10221
+ while (source !== null && source._fn === undefined) source = source._parent;
10222
+ source ??= owner;
10223
+ const statusErr = new StatusError(source, err);
10224
+ ext(source)._error = statusErr;
10225
+ source._statusFlags = (source._statusFlags ?? 0) | STATUS_ERROR;
10226
+ handled = owner._queue.notify(source, STATUS_ERROR, STATUS_ERROR, statusErr);
10227
+ }
10228
+ if (!handled && firstError === UNSET) firstError = err;
10229
+ }
10230
+ }
10231
+ return firstError;
10232
+ }
10233
+ // Transition-stamped emissions (§2b, "the walk is not the visibility moment
10234
+ // inside a transition"): entries stash DIRECTLY on their transition
10235
+ // (`_heldPatches`) and release into the live queue when THAT batch commits
10236
+ // (patchCommitHook). Reverted transitions never commit — their stash drops
10237
+ // with the transition object, no revert bookkeeping. The field (rather than
10238
+ // a WeakMap) keeps the every-flush commit-hook check to one property read;
10239
+ // the ambient batch never stashes.
10240
+ let commitHookInstalled = false;
10241
+ function releaseBatch(batch) {
10242
+ const held = batch._heldPatches;
10243
+ if (held === undefined) return;
10244
+ batch._heldPatches = undefined;
10245
+ for (let i = 0; i < held.length; i++) pushLive(held[i]);
10246
+ }
10247
+ function pushLive(item) {
10248
+ if (queue === null) queue = [];
10249
+ queue.push(item);
10250
+ if (!scheduled) {
10251
+ scheduled = true;
10252
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
10253
+ }
10254
+ }
10255
+ function push(item) {
10256
+ const tx = activeTransition;
10257
+ if (tx !== null) {
10258
+ let held = tx._heldPatches;
10259
+ if (held === undefined) tx._heldPatches = held = [];
10260
+ held.push(item);
10261
+ return;
10262
+ }
10263
+ pushLive(item);
10264
+ }
10265
+ /** Self-entry push with SAME-BATCH COALESCING (re-audit 2/3): a record's
10266
+ * later non-forced emission into the same container UPDATES the queued
10267
+ * entry in place — `next` takes the newest capture (adoption swaps the
10268
+ * backing object per emission; dropping the later one applied STALE state),
10269
+ * `prev` keeps the batch's earliest (effect semantics: one application per
10270
+ * batch spanning the whole window). The entry's consumer list is the live
10271
+ * pc.p array, so mid-batch registrants ride the single application. Forced
10272
+ * entries and row/slot ops never coalesce; the drain clears the stamps so a
10273
+ * quiet record retains nothing from its last batch. */
10274
+ function pushSelf(pc, item) {
10275
+ const tx = activeTransition;
10276
+ let arr;
10277
+ if (tx !== null) {
10278
+ let held = tx._heldPatches;
10279
+ if (held === undefined) tx._heldPatches = held = [];
10280
+ arr = held;
10281
+ } else {
10282
+ if (queue === null) queue = [];
10283
+ arr = queue;
10284
+ }
10285
+ if (pc.qa === arr && pc.qe !== null) {
10286
+ const qe = pc.qe;
10287
+ qe.next = item.next;
10288
+ qe.list = item.list; // pc.p can be re-created if emptied mid-batch
10289
+ return;
10290
+ }
10291
+ pc.qa = arr;
10292
+ pc.qe = item;
10293
+ item.pc = pc;
10294
+ arr.push(item);
10295
+ if (arr === queue && !scheduled) {
10296
+ scheduled = true;
10297
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
10298
+ }
10299
+ }
10300
+ /** Drain-side stamp clear (re-audit 3, P2-6): without it a quiet long-lived
10301
+ * record's channel retains its last batch's container array, entry, and both
10302
+ * captured backings for the record's lifetime. */
10303
+ function clearStamp(item) {
10304
+ const pc = item.pc;
10305
+ if (pc !== undefined && pc.qe === item) {
10306
+ pc.qa = null;
10307
+ pc.qe = null;
10308
+ }
10309
+ }
10310
+ /** Shallow clone for the owned-prev rule (§2c): owned backings fold values
10311
+ * INTO the same raw at commit, so a queued prev must be snapshotted. */
10312
+ function clonePrev(prev) {
10313
+ return Array.isArray(prev) ? prev.slice() : { ...prev };
10314
+ }
10315
+ /**
10316
+ * Emit a record's visibility transition. Callers gate on `hasPatches()` and
10317
+ * `t.d` cheaply; this function re-checks and walks ancestors (§4b).
10318
+ */
10319
+ function emitPatch(t, next, prev) {
10320
+ const p = t.pc !== null ? t.pc.p : null;
10321
+ if (p !== null)
10322
+ pushSelf(t.pc, {
10323
+ list: p,
10324
+ next,
10325
+ prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
10326
+ force: false,
10327
+ t: null
10328
+ });
10329
+ // Bubbling: ancestors force-re-apply from their LIVE backing, resolved at
10330
+ // drain (privatization may clone it between now and then).
10331
+ let u = t.u;
10332
+ while (u !== null) {
10333
+ const up = u.pc !== null ? u.pc.p : null;
10334
+ if (up !== null) push({ list: up, next: null, prev: null, force: true, t: u });
10335
+ u = u.u;
10336
+ }
10337
+ }
10338
+ /** Emission for sites that already stand at the record with both sides in
10339
+ * hand and have already handled ancestors (the adoption walk descends —
10340
+ * parents were visited first), so no bubbling walk. */
10341
+ function emitPatchLocal(t, next, prev) {
10342
+ const p = t.pc !== null ? t.pc.p : null;
10343
+ if (p !== null)
10344
+ pushSelf(t.pc, {
10345
+ list: p,
10346
+ next,
10347
+ prev: ownedRaw.has(prev) ? clonePrev(prev) : prev,
10348
+ force: false,
10349
+ t: null
10350
+ });
10351
+ }
10352
+ /** Optimistic-channel emission: overrides are visible THIS flush while the
10353
+ * transaction is in flight — that is what optimism means. These ride a
10354
+ * dedicated queue drained at LANE-EFFECT timing (the regular effect queues
10355
+ * are stashed by an in-flight action), with the regular drain as the
10356
+ * settle-time fallback. `next === null` = forced re-apply from the live
10357
+ * target (the revert shape: committed truth back onto the DOM). */
10358
+ let optQueue = null;
10359
+ function drainOptimistic() {
10360
+ const q = optQueue;
10361
+ optQueue = null;
10362
+ if (q === null) return;
10363
+ // Same isolation/routing primitive as the normal drain (re-audit blocker
10364
+ // 5): one throwing optimistic patch must not abort its siblings, and it
10365
+ // must reach the registering owner's Errored boundary.
10366
+ let firstError = UNSET;
10367
+ for (let i = 0; i < q.length; i++) {
10368
+ clearStamp(q[i]);
10369
+ const { list, prev, force, t } = q[i];
10370
+ const next = t !== null ? (t.pb ?? t.v) : q[i].next;
10371
+ firstError = applyEntries(list, next, prev, force, firstError);
10372
+ }
10373
+ if (firstError !== UNSET) {
10374
+ haltReactivity(firstError);
10375
+ throw firstError;
10376
+ }
10377
+ }
10378
+ function emitPatchOptimistic(t, next, prev) {
10379
+ const p = t.pc !== null ? t.pc.p : null;
10380
+ if (p === null) return;
10381
+ if (optQueue === null) optQueue = [];
10382
+ if (next === null) optQueue.push({ list: p, next: null, prev: null, force: true, t });
10383
+ else {
10384
+ // Same-batch coalescing, optimistic container (re-audit 3): later
10385
+ // non-forced emission updates the queued entry's next in place.
10386
+ const pc = t.pc;
10387
+ if (pc.qa === optQueue && pc.qe !== null) {
10388
+ const qe = pc.qe;
10389
+ qe.next = next;
10390
+ qe.list = p;
10391
+ } else {
10392
+ const item = { list: p, next, prev, force: false, t: null };
10393
+ pc.qa = optQueue;
10394
+ pc.qe = item;
10395
+ item.pc = pc;
10396
+ optQueue.push(item);
10397
+ }
10398
+ }
10399
+ // Backup scheduling: the lane-slot drain covers in-flight application; a
10400
+ // stashed regular drain guarantees settle-time application when no lane
10401
+ // survives to the final flush (pure reverts).
10402
+ if (!scheduled) {
10403
+ scheduled = true;
10404
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
10405
+ }
10406
+ }
10407
+ /** Row-ops emission at OPTIMISTIC (lane) timing: user drafts on an
10408
+ * optimistic family must show structure IN FLIGHT — bypassing the
10409
+ * transition stash exactly like emitPatchOptimistic. Two forms:
10410
+ * - `ops` given (write site): `nextRows` is the draft's intended visible
10411
+ * list, ops the identity diff against the pre-write optimistic view.
10412
+ * - `ops === null` (revert site): RESYNC — the consumer rebuilds retention
10413
+ * by row identity against the live post-revert view, resolved from the
10414
+ * target at drain time (overrides are gone by then, so `pb ?? v` IS the
10415
+ * committed truth). */
10416
+ function emitRowOpsOptimistic(t, nextRows, ops) {
10417
+ const list = t.pc !== null ? t.pc.ro : null;
10418
+ if (list === null) return;
10419
+ if (optQueue === null) optQueue = [];
10420
+ optQueue.push({
10421
+ list: list.map(e => ({
10422
+ owner: e.owner,
10423
+ fn: (n, _p) => e.fn(n, ops)
10424
+ })),
10425
+ next: nextRows,
10426
+ prev: null,
10427
+ force: false,
10428
+ t: nextRows === null ? t : null
10429
+ });
10430
+ if (!scheduled) {
10431
+ scheduled = true;
10432
+ globalQueue.enqueue(EFFECT_RENDER, drainApplyQueue);
10433
+ }
10434
+ }
10435
+ /**
10436
+ * Register a compiled patch on a store record. Multi-consumer (two lists
10437
+ * can render one record); owner-scoped for disposal. Returns unbind.
10438
+ */
10439
+ // Global registration count: the cheap gate emission sites check before any
10440
+ // per-record work (unpatched apps pay one number compare per transition).
10441
+ let patchCount = 0;
10442
+ function hasPatches() {
10443
+ return patchCount > 0;
10444
+ }
10445
+ function registerPatch(record, fn) {
10446
+ let t = record?.[$TARGET];
10447
+ if (t === undefined) throw new Error("registerPatch: not a store record");
10448
+ // Chained backings (§7b): register on the ULTIMATE owner — that is where
10449
+ // value transitions fold and dispatch; the wrapper's identity is stable
10450
+ // and would never fire (see ultimateTarget).
10451
+ t = ultimateTarget(t) ?? t;
10452
+ if (!commitHookInstalled) {
10453
+ commitHookInstalled = true;
10454
+ armPatchHooks();
10455
+ setPatchCommitHook(releaseBatch);
10456
+ GlobalQueue._drainPatchOptimistic = drainOptimistic;
10457
+ }
10458
+ const entry = { fn, owner: getOwner() };
10459
+ const pc = pcOf(t);
10460
+ const list = (pc.p ??= []);
10461
+ list.push(entry);
10462
+ patchCount++;
10463
+ // Bindings are subscriptions for reachability (§6d pruning must descend
10464
+ // into bound records).
10465
+ markDescendants(t);
10466
+ let unbound = false;
10467
+ return () => {
10468
+ if (unbound) return;
10469
+ unbound = true;
10470
+ entry.u = true; // dispatch snapshots skip severed consumers
10471
+ // Decrement ONLY on actual removal: a demotion (demoteToEffects) may
10472
+ // have already pulled this entry and repaired the count — the splice
10473
+ // miss is how this closure learns that.
10474
+ const idx = list.indexOf(entry);
10475
+ if (idx >= 0) {
10476
+ list.splice(idx, 1);
10477
+ patchCount--;
10478
+ }
10479
+ if (list.length === 0 && pc.p === list) pc.p = null;
10480
+ };
10481
+ }
10482
+ /** Resolve a target through CHAINED backings (§7b) to the ultimate owner.
10483
+ * A projection family wrapper's backing IS another store's proxy: value
10484
+ * transitions fold on the ULTIMATE target (the wrapper's identity never
10485
+ * changes), so patch registration and raw resolution must land there or
10486
+ * registered patches never fire (equivalence-matrix finding: projection
10487
+ * value ticks froze driver rows while classic effects tracked through). */
10488
+ function ultimateTarget(t) {
10489
+ while (t.ch) {
10490
+ const u = (t.pb ?? t.v)?.[$TARGET];
10491
+ if (u === undefined) return undefined;
10492
+ t = u;
10493
+ }
10494
+ return t;
10495
+ }
10496
+ /** Dual-driver bind probe (compiler runtime contract): when `record` is a
10497
+ * patchable store record, returns its CURRENT raw backing (the driver's
10498
+ * initial force-apply reads it directly — no proxy traffic, no tracking);
10499
+ * returns undefined otherwise (driver falls back to the effect path).
10500
+ * Not patchable: non-records, non-proxies, accessor-bearing records
10501
+ * (patches read raw — getters need tracked evaluation), broken chains. */
10502
+ function patchableRaw(record) {
10503
+ let t = record?.[$TARGET];
10504
+ if (t === undefined || t.px !== record || t.a === true) return undefined;
10505
+ t = ultimateTarget(t);
10506
+ // SCAN before trusting (re-audit blocker 3): `a` starts false and is only
10507
+ // discovered lazily (first draft, deep walks) — admission must run the
10508
+ // one-time own-accessor scan itself, or a getter-bearing record takes the
10509
+ // patch path and its getter's OUTSIDE dependencies (signals, other
10510
+ // records) never re-apply. Sticky `sc` makes this one probe pass per
10511
+ // record lifetime.
10512
+ if (t === undefined || !targetIsPlain(t)) return undefined;
10513
+ return t.pb ?? t.v;
10514
+ }
10515
+ /** Accessor demotion (design §5): a record that acquires an accessor after
10516
+ * registration stops being patchable — reads must go through tracked
10517
+ * evaluation. Clears patches and repairs the global count; callers re-drive
10518
+ * the pulled bodies (demoteToEffects). */
10519
+ function demotePatches(t) {
10520
+ if (t.pc === null) return null;
10521
+ const p = t.pc.p;
10522
+ t.pc.p = null;
10523
+ if (p === null) return null;
10524
+ patchCount -= p.length;
10525
+ // Drain IN PLACE: unbind closures captured this array — a late unbind must
10526
+ // miss its indexOf and not double-decrement the repaired count.
10527
+ return p.splice(0, p.length);
10528
+ }
10529
+ /** The demotion re-drive (re-audit blocker 3): each pulled body becomes the
10530
+ * SAME dual-driver effect fallback the web runtime would have chosen had the
10531
+ * record carried the accessor at bind — a tracked compute pass (next === prev
10532
+ * short-circuits every compare into a pure read THROUGH THE PROXY, so getter
10533
+ * dependencies track) plus an untracked force-apply at effect timing.
10534
+ *
10535
+ * Creation is DEFERRED to the effect phase: the trap that discovers the
10536
+ * accessor runs mid-draft, and an effect's initial pass must not read
10537
+ * through the proxy inside the write window. The record's own transition
10538
+ * for that draft is covered by the new effect's initial force-apply.
10539
+ *
10540
+ * Known edge (documented): a demoted LIST-ROW body re-drives under its
10541
+ * registering owner (the list owner), so per-row severing on removal is
10542
+ * lost for demoted rows — the effect lives until the LIST disposes. Rows
10543
+ * only demote when user code defines an accessor on a row record at
10544
+ * runtime. */
10545
+ function demoteToEffects(t) {
10546
+ const entries = demotePatches(t);
10547
+ if (entries === null || entries.length === 0) return;
10548
+ const proxy = t.px;
10549
+ globalQueue.enqueue(EFFECT_RENDER, () => {
10550
+ for (let i = 0; i < entries.length; i++) {
10551
+ const entry = entries[i];
10552
+ if (entry.owner !== null && isDisposed(entry.owner)) continue;
10553
+ const fn = entry.fn;
10554
+ runWithOwner(entry.owner, () =>
10555
+ createRenderEffect(
10556
+ () => {
10557
+ fn(proxy, proxy, false);
10558
+ },
10559
+ () => {
10560
+ // Block body: a compiled patch body's return value must not be
10561
+ // mistaken for an effect cleanup.
10562
+ untrack(() => fn(proxy, undefined, true));
10563
+ }
10564
+ )
10565
+ );
10566
+ }
10567
+ });
10568
+ }
10569
+ /** Register a structural-ops consumer on a keyed store array (the list
10570
+ * container's channel — what `For` consumes through the seam). */
10571
+ function registerRowOps(array, fn) {
10572
+ let t = array?.[$TARGET];
10573
+ if (t === undefined) throw new Error("registerRowOps: not a store array");
10574
+ // Chained backings resolve to the ULTIMATE owner, same as registerPatch
10575
+ // (§7b) — the walk/fold emits there (re-audit blocker 4).
10576
+ t = ultimateTarget(t) ?? t;
10577
+ armRowHooks();
10578
+ if (!commitHookInstalled) {
10579
+ commitHookInstalled = true;
10580
+ armPatchHooks();
10581
+ setPatchCommitHook(releaseBatch);
10582
+ GlobalQueue._drainPatchOptimistic = drainOptimistic;
10583
+ }
10584
+ const entry = { fn, owner: getOwner() };
10585
+ const pc = pcOf(t);
10586
+ const list = (pc.ro ??= []);
10587
+ list.push(entry);
10588
+ patchCount++;
10589
+ markDescendants(t);
10590
+ let unbound = false;
10591
+ return () => {
10592
+ if (unbound) return;
10593
+ unbound = true;
10594
+ patchCount--;
10595
+ const idx = list.indexOf(entry);
10596
+ if (idx >= 0) list.splice(idx, 1);
10597
+ if (list.length === 0 && pc.ro === list) pc.ro = null;
10598
+ };
10599
+ }
10600
+ /** Slot patches (shallow arrays) ride the same apply queue: the walk emits
10601
+ * per aligned value-replaced slot; application happens at effect phase under
10602
+ * the registration owner's lifetime. */
10603
+ function emitSlotPatch(t, index, next, prev) {
10604
+ const sp = t.pc !== null ? t.pc.sp : null;
10605
+ if (sp === null) return;
10606
+ push({
10607
+ list: sp.map(e => ({ owner: e.owner, fn: () => e.fn(index, next, prev) })),
10608
+ next,
10609
+ prev,
10610
+ force: false,
10611
+ t: null
10612
+ });
10613
+ }
10614
+ /** Slot patch for shallow arrays: the reconcile walk emits (index, next,
10615
+ * prev) for KEY-ALIGNED value-replaced slots (structure rides row ops), and
10616
+ * the emission queues through the patch apply queue — effect-phase timing,
10617
+ * transition stamping, disposed-owner drop — like every other channel. */
10618
+ function registerSlotPatchNext(arr, fn) {
10619
+ let t = arr?.[$TARGET];
10620
+ if (t === undefined) throw new Error("registerSlotPatchNext: not a store array");
10621
+ // Chained backings resolve to the ULTIMATE owner, same as registerPatch
10622
+ // (§7b) — the walk emits slot ticks there (re-audit blocker 4).
10623
+ t = ultimateTarget(t) ?? t;
10624
+ armRowHooks();
10625
+ if (!commitHookInstalled) {
10626
+ commitHookInstalled = true;
10627
+ armPatchHooks();
10628
+ setPatchCommitHook(releaseBatch);
10629
+ GlobalQueue._drainPatchOptimistic = drainOptimistic;
10630
+ }
10631
+ // Multi-consumer (external audit): one shallow array can drive several
10632
+ // lists — registrations are a list, unbinds splice their own entry.
10633
+ const pc = pcOf(t);
10634
+ const entry = { fn, owner: getOwner() };
10635
+ (pc.sp ??= []).push(entry);
10636
+ markDescendants(t);
10637
+ let unbound = false;
10638
+ return () => {
10639
+ if (unbound || pc.sp === null) return;
10640
+ unbound = true;
10641
+ const idx = pc.sp.indexOf(entry);
10642
+ if (idx >= 0) pc.sp.splice(idx, 1);
10643
+ if (pc.sp.length === 0) pc.sp = null;
10644
+ };
10645
+ }
10646
+ /** Row-ops ride the SAME apply queue/timing as record patches: transition-
10647
+ * stamped, applied at effect phase, in emission order (structure before the
10648
+ * new rows' own patches can exist; retained rows' value patches commute). */
10649
+ function emitRowOps(t, next, ops) {
10650
+ const list = t.pc !== null ? t.pc.ro : null;
10651
+ if (list === null) return;
10652
+ push({
10653
+ list: list.map(e => ({
10654
+ owner: e.owner,
10655
+ fn: (n, _p) => e.fn(n, ops)
10656
+ })),
10657
+ next,
10658
+ prev: null,
10659
+ force: false,
10660
+ t: null
10661
+ });
10662
+ }
10663
+ // Pay-for-use seams: the write paths (store/reconcile/optimistic) emit
10664
+ // through installed hooks instead of importing this module. Installation is
10665
+ // LAZY (first registration) rather than a module-scope call — the dist is a
10666
+ // flat bundle, and a top-level side effect would retain the whole channel in
10667
+ // every consumer. TWO TIERS so a value-only registration (registerPatch —
10668
+ // present in ~every bundle under patch-mode default) does not retain the
10669
+ // list machinery (row-ops emitters + reconcile's diff builders): row hooks
10670
+ // arm only from the list driver's registrations. Sound because every
10671
+ // emission site is guarded by the matching pc channel, which only the
10672
+ // corresponding registration creates. See patch-hooks.ts.
10673
+ function armPatchHooks() {
10674
+ installPatchHooks({
10675
+ emitPatch,
10676
+ emitPatchLocal,
10677
+ emitPatchOptimistic,
10678
+ hasPatches,
10679
+ demoteToEffects
10680
+ });
10681
+ }
10682
+ function armRowHooks() {
10683
+ installRowHooks({
10684
+ emitRowOps,
10685
+ emitSlotPatch,
10686
+ emitSetterRowOps,
10687
+ emitRowOpsOptimistic
10688
+ });
10689
+ }
10690
+
8704
10691
  /**
8705
10692
  * Store rewrite — optimistic stores (§3/§7, RUL-3): no store-side layer, no
8706
10693
  * backup snapshots. Nodes in an optimistic family are ARMED core signals
@@ -8710,12 +10697,31 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
8710
10697
  * armed presence nodes (the §6 overlay), so structural optimism reverts with
8711
10698
  * the same per-transaction granularity (FINDING-2's fix by construction).
8712
10699
  *
8713
- * Derived form = an optimistic projection: the derive's recompute and its
8714
- * async commits run under projectionWriteActive (authoritative landings
8715
- * commit silently beneath any active overrides). The transitionBlocked
8716
- * store-half (#2951) is installed here for next-shaped targets, chaining the
10700
+ * Derived form = an optimistic projection. Landings follow the fold rule
10701
+ * (#3164, RUL-2 as re-ruled): while a transaction retains optimistic edits
10702
+ * on the family, truth that lands STAGES into that transaction — a keyed
10703
+ * identity-preserving walk written through the ordinary staged setter
10704
+ * channel — and reveals atomically at settle, exactly like a signal landing
10705
+ * under an active override (asyncWrite's held branch). Optimistic edits are
10706
+ * never consumed by landings; they live exactly as long as their transaction
10707
+ * and die by engine-native revert. With no retainer, landings commit
10708
+ * immediately under projectionWriteActive (authoritative, silently beneath
10709
+ * any bare-write overrides — those ride the flight's own transition, #2951).
10710
+ * Authoritative readers (until()'s predicate) tunnel into staged truth via
10711
+ * the node read path's pending-value arm. The transitionBlocked store-half
10712
+ * (#2951) is installed here for next-shaped targets, chaining the
8717
10713
  * legacy/engine checks.
8718
10714
  */
10715
+ /** #3164 fold: a stamped truth is HELD (masked from ordinary readers until
10716
+ * the reveal) only while its transition is live AND retaining optimism —
10717
+ * overrides are what make partial-coverage composition a tear. A plain
10718
+ * async transition carries no overrides, so downstream computes must see
10719
+ * staged values to converge (normal speculation). Resolves merges first:
10720
+ * merge unions optimistic nodes/stores into the target. */
10721
+ function transitionHoldsOptimism(transition) {
10722
+ const t = currentTransition(transition);
10723
+ return t._done !== true && (t._optimisticNodes.length !== 0 || t._optimisticStores.size !== 0);
10724
+ }
8719
10725
  let blockedInstalled = false;
8720
10726
  function installNextBlockedHalf() {
8721
10727
  if (blockedInstalled) return;
@@ -8723,13 +10729,42 @@ function installNextBlockedHalf() {
8723
10729
  // Late-bind the optimistic machinery into the plain store/reconcile paths
8724
10730
  // (all call sites are fam?.opt-gated, so this always runs first) and the
8725
10731
  // affects witness's view resolver.
8726
- setOptHooks({ notifyOptimisticWrites, optimisticView, applyTentative });
10732
+ setOptHooks({
10733
+ notifyOptimisticWrites,
10734
+ optimisticView,
10735
+ applyTentative,
10736
+ retainsOptimism: transitionHoldsOptimism
10737
+ });
8727
10738
  setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw));
8728
10739
  // Scheduler flush tails call _clearOptimisticStores whenever tracked
8729
10740
  // stores exist; next has no layer to clear — reverts are engine-native —
8730
10741
  // so the hook only empties the batch set.
8731
10742
  if (!GlobalQueue._clearOptimisticStores) {
8732
10743
  GlobalQueue._clearOptimisticStores = stores => {
10744
+ // Patch channel (revert site): engine-native reverts flip node values
10745
+ // back to committed; patched records need a forced DOM re-apply from
10746
+ // the post-revert view. Emission only — next keeps no layer to clear.
10747
+ for (const px of stores) {
10748
+ const t = px?.[$TARGET];
10749
+ const overlaid = t?.fam?.overlaid;
10750
+ if (overlaid !== undefined) {
10751
+ for (const ot of overlaid) {
10752
+ if (ot.pc !== null && ot.pc.p !== null) patchHooks.emitPatchOptimistic(ot, null, null);
10753
+ // Row-ops resync (family increment 2): reverts flip node values
10754
+ // back engine-natively; a driven list must rebuild retention by
10755
+ // row identity against the post-revert view (resolved from the
10756
+ // target at drain — overrides are gone by then).
10757
+ if (ot.pc !== null && ot.pc.ro !== null) rowHooks.emitRowOpsOptimistic(ot, null, null);
10758
+ // Keyset resync (classic channel twin): the keyset node's own
10759
+ // revert can compare EQUAL (a landing's bump matched the
10760
+ // tentative bump) while the arrangement underneath changed —
10761
+ // mapArray/ownKeys subscribers must re-read the post-revert
10762
+ // view. Authoritative bump: never re-arm the node we are
10763
+ // clearing.
10764
+ if (ot.k !== null) runAuthoritative(() => setSignal(ot.k, v => v + 1));
10765
+ }
10766
+ }
10767
+ }
8733
10768
  stores.clear();
8734
10769
  };
8735
10770
  }
@@ -8787,20 +10822,41 @@ function createOptimisticStoreNext(first, second, options) {
8787
10822
  };
8788
10823
  const store = wrapNext(initialValue, null, null, fam);
8789
10824
  fam.px = store;
10825
+ // Same key resolution the projection channels use ("id" default) — replay's
10826
+ // satisfaction rule reads it off the family.
10827
+ const keyOption = options?.key === undefined ? "id" : options.key;
10828
+ fam.key =
10829
+ typeof keyOption === "function"
10830
+ ? keyOption
10831
+ : keyOption === null
10832
+ ? null
10833
+ : row => (isWrappable(row) ? row[keyOption] : undefined);
8790
10834
  if (fam.shallow) {
8791
10835
  store[$TARGET].s = true;
8792
10836
  markRawIngest(initialValue);
8793
10837
  }
8794
10838
  if (derived) {
8795
10839
  const fn = first;
8796
- // Async commits land outside the computed's sync body — re-apply the
8797
- // authoritative-write posture there too. Landings consume the family's
8798
- // tentative overrides (RUL-2: visible landed truth replaces optimism)
8799
- // both the reconcile-channel commit and per-op post-await draft writes.
8800
- const consume = () => consumeOverridesNext(fam);
8801
- const wrapCommit = write => {
8802
- runAuthoritative(write);
8803
- consume();
10840
+ // Landing router (#3164 fold ruling): while a transaction retains
10841
+ // optimistic edits on this family, truth landings stage INTO it and
10842
+ // reveal atomically at settle; with no retainer they commit immediately
10843
+ // under the authoritative posture (async commits land outside the
10844
+ // computed's sync body, so the posture is re-applied here).
10845
+ const wrapCommit = (write, value) => {
10846
+ const txn = retainingTransition(fam);
10847
+ if (txn === null) runAuthoritative(write);
10848
+ else stageLanding(fam, txn, value);
10849
+ };
10850
+ // Draft writes (the derive mutating its draft, sync body and post-await
10851
+ // continuations alike) are the same truth channel per-operation: bind
10852
+ // each op to the retaining transaction so its node writes stage and its
10853
+ // backing fold defers (ensurePB stamps foldBatches with the swapped-in
10854
+ // batch; the write-override eager-commit branch in notifyWrites yields
10855
+ // to any active transaction).
10856
+ const aroundDraftWrite = op => {
10857
+ const txn = retainingTransition(fam);
10858
+ if (txn === null) op();
10859
+ else runFolded(txn, op);
8804
10860
  };
8805
10861
  let nodeOptions;
8806
10862
  if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
@@ -8812,14 +10868,201 @@ function createOptimisticStoreNext(first, second, options) {
8812
10868
  fn,
8813
10869
  options?.key === undefined ? "id" : options.key,
8814
10870
  wrapCommit,
8815
- consume
10871
+ aroundDraftWrite
8816
10872
  )
8817
10873
  );
8818
10874
  }, nodeOptions);
8819
10875
  node._config &= ~CONFIG_AUTO_DISPOSE;
8820
10876
  fam.node = node;
8821
10877
  }
8822
- return [store, fn => storeSetterNext(store, fn)];
10878
+ return [
10879
+ store,
10880
+ fn => {
10881
+ // Retention ledger (#3164): record the owning transaction so landings
10882
+ // know to fold. Captured at entry — the action machinery has the
10883
+ // transaction ambient while user code runs; a bare write with no
10884
+ // transaction retains nothing (it rides the flight's own transition
10885
+ // per #2951 and dies with it).
10886
+ const txn = activeTransition;
10887
+ storeSetterNext(store, fn);
10888
+ if (txn !== null) (fam.rt ??= new Set()).add(txn);
10889
+ }
10890
+ ];
10891
+ }
10892
+ /** Resolve a retained transition through its merge chain (`_done` holds the
10893
+ * merge target while merged, `true` once settled). Null = dead. */
10894
+ function liveTransition(txn) {
10895
+ while (typeof txn._done === "object") txn = txn._done;
10896
+ return txn._done === true ? null : txn;
10897
+ }
10898
+ /** The transaction truth landings fold into: the first live member of the
10899
+ * family's retention ledger (dead members prune here). Multiple live
10900
+ * retainers entangle through their shared family writes and settle
10901
+ * together, so folding into the first reaches all of them. */
10902
+ function retainingTransition(fam) {
10903
+ const rt = fam.rt;
10904
+ if (rt === undefined || rt.size === 0) return null;
10905
+ let live = null;
10906
+ for (const txn of rt) {
10907
+ const resolved = liveTransition(txn);
10908
+ if (resolved === null) rt.delete(txn);
10909
+ else live ??= resolved;
10910
+ }
10911
+ return live;
10912
+ }
10913
+ /** Fold a landing into the retaining transaction (#3164): the landed value
10914
+ * is written through the ORDINARY staged setter channel — node writes park
10915
+ * as `_pendingValue` registered with the transaction's batch (speculation
10916
+ * and until()'s authoritative tunnel see them; live view stays coherent),
10917
+ * and the backing fold defers via the foldBatches stamp — under the
10918
+ * authoritative posture, so armed nodes take the engine bypass and no
10919
+ * override is created. The engine's own commit machinery reveals everything
10920
+ * atomically when the transaction settles (transitions never abort: failed
10921
+ * actions still commit — only optimistic overrides revert). */
10922
+ function stageLanding(fam, txn, incoming) {
10923
+ runFolded(txn, () =>
10924
+ runAuthoritative(() =>
10925
+ storeSetterNext(
10926
+ fam.px,
10927
+ draft => {
10928
+ stagedApply(draft, unwrapValue(incoming), fam.key ?? null);
10929
+ },
10930
+ false
10931
+ )
10932
+ )
10933
+ );
10934
+ }
10935
+ /** Run a fold write inside the retaining transaction's batch, then
10936
+ * transition-stamp its staged nodes NOW (parity with the parked-transition
10937
+ * flush path's reassignPendingTransition): the stamp is what routes stale
10938
+ * (render) readers to the committed value — core read's cross-transaction
10939
+ * guard — and what makes foldHeld defer the backing for context-free
10940
+ * readers. A microtask staging never crosses that flush path, so without
10941
+ * the stamp a render effect's speculative recompute would compose staged
10942
+ * truth with live overrides — the #3164 tear, one window later. Armed
10943
+ * nodes additionally raise CONFIG_HELD_TRUTH: their staged value is
10944
+ * confirming truth masked from ordinary readers until the reveal (plain
10945
+ * staged nodes stay visible — normal speculation; override-covered nodes
10946
+ * stay unarmed — the override is their display and its revert their
10947
+ * notification, A17). */
10948
+ function runFolded(txn, op) {
10949
+ runAsTransitionBatch(txn, op);
10950
+ const pending = txn._pendingNodes;
10951
+ for (let i = 0; i < pending.length; i++) {
10952
+ const node = pending[i];
10953
+ node._transition = txn;
10954
+ if (node._config & CONFIG_OPTIMISTIC && !hasActiveOverride(node))
10955
+ node._config |= CONFIG_HELD_TRUTH;
10956
+ }
10957
+ }
10958
+ /** Keyed identity-preserving deep merge through live draft proxies — the
10959
+ * staged twin of the adoption walk. Reads see the pending backing (staged
10960
+ * view), so consecutive landings during one hold compose; key-matched rows
10961
+ * keep their raw (and so their proxy) in the slot with only changed leaves
10962
+ * written; unmatched rows land wholesale. Runs inside stageLanding's
10963
+ * authoritative bracket: drafts seed from committed truth, never overlays. */
10964
+ function stagedApply(cur, incoming, keyFn) {
10965
+ const curArr = Array.isArray(cur);
10966
+ if (curArr && Array.isArray(incoming)) {
10967
+ const len = incoming.length;
10968
+ if (keyFn !== null) {
10969
+ // Occurrence-aware key queues (parity with the adoption window):
10970
+ // duplicate keys match per occurrence, each current row consumed once.
10971
+ let byKey = null;
10972
+ const curLen = cur.length;
10973
+ for (let j = 0; j < curLen; j++) {
10974
+ const raw = unwrapValue(cur[j]);
10975
+ if (!isWrappable(raw)) continue;
10976
+ const k = keyFn(raw);
10977
+ if (k === undefined) continue;
10978
+ const q = (byKey ??= new Map()).get(k);
10979
+ if (q === undefined) byKey.set(k, [raw]);
10980
+ else q.push(raw);
10981
+ }
10982
+ // Echo adoption: an optimistic structural add whose key the landing
10983
+ // confirms must keep its raw (and so its proxy — list drivers keep the
10984
+ // DOM row). Tentative rows never reach committed truth (they live in
10985
+ // node overrides), so key-match the draft target's active override
10986
+ // rows as a secondary pool. Committed rows queued first own their
10987
+ // keys; overlay rows only extend coverage. Adopted raws enter staged
10988
+ // truth; at settle the override reverts and the reveal re-seats the
10989
+ // same raw.
10990
+ const overlayNodes = cur[$TARGET]?.n;
10991
+ if (overlayNodes != null) {
10992
+ for (const ok of Reflect.ownKeys(overlayNodes)) {
10993
+ const node = overlayNodes[ok];
10994
+ if (!hasActiveOverride(node)) continue;
10995
+ const raw = unwrapValue(unwrapOverride(node._x._overrideValue));
10996
+ if (!isWrappable(raw)) continue;
10997
+ const k = keyFn(raw);
10998
+ if (k === undefined) continue;
10999
+ const q = (byKey ??= new Map()).get(k);
11000
+ if (q === undefined) byKey.set(k, [raw]);
11001
+ else q.push(raw);
11002
+ }
11003
+ }
11004
+ for (let i = 0; i < len; i++) {
11005
+ const nv = incoming[i];
11006
+ let matched;
11007
+ if (isWrappable(nv) && byKey !== null) {
11008
+ const nk = keyFn(nv);
11009
+ if (nk !== undefined) {
11010
+ for (const [k, q] of byKey) {
11011
+ if (!sameKey(k, nk)) continue;
11012
+ matched = q.shift();
11013
+ if (q.length === 0) byKey.delete(k);
11014
+ break;
11015
+ }
11016
+ }
11017
+ }
11018
+ if (matched !== undefined) {
11019
+ if (unwrapValue(cur[i]) !== matched) cur[i] = matched;
11020
+ stagedApply(cur[i], nv, keyFn);
11021
+ } else {
11022
+ const pv = unwrapValue(cur[i]);
11023
+ if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) cur[i] = nv;
11024
+ }
11025
+ }
11026
+ } else {
11027
+ for (let i = 0; i < len; i++) {
11028
+ const nv = incoming[i];
11029
+ const pv = unwrapValue(cur[i]);
11030
+ if (pv === nv) continue;
11031
+ if (isWrappable(nv) && isWrappable(pv) && Array.isArray(nv) === Array.isArray(pv))
11032
+ stagedApply(cur[i], nv, keyFn);
11033
+ else if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) cur[i] = nv;
11034
+ }
11035
+ }
11036
+ if (cur.length !== len) cur.length = len;
11037
+ return;
11038
+ }
11039
+ // Object merge; also the degenerate root-kind-change shape (arrays accept
11040
+ // keyed writes/deletes, so a wholesale restatement still lands staged).
11041
+ for (const k of Reflect.ownKeys(incoming)) {
11042
+ if (curArr && k === "length") continue;
11043
+ const nv = incoming[k];
11044
+ const pv = unwrapValue(cur[k]);
11045
+ if (pv === nv) continue;
11046
+ if (isWrappable(nv) && isWrappable(pv) && Array.isArray(nv) === Array.isArray(pv)) {
11047
+ // Different-keyed entities never merge (tentative-channel parity):
11048
+ // the incoming object replaces the slot wholesale.
11049
+ if (keyFn !== null) {
11050
+ const pk = keyFn(pv);
11051
+ const nk = keyFn(nv);
11052
+ if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) {
11053
+ cur[k] = nv;
11054
+ continue;
11055
+ }
11056
+ }
11057
+ stagedApply(cur[k], nv, keyFn);
11058
+ } else if (!isEqual(pv, nv) && !targetsEqual(pv, nv)) {
11059
+ cur[k] = nv;
11060
+ }
11061
+ }
11062
+ for (const k of Reflect.ownKeys(cur)) {
11063
+ if ((curArr && k === "length") || k in incoming) continue;
11064
+ delete cur[k];
11065
+ }
8823
11066
  }
8824
11067
  // ---- optimistic-only store machinery (moved from next/store.ts /
8825
11068
  // next/reconcile.ts so plain-store bundles tree-shake it) ----
@@ -8836,6 +11079,22 @@ function notifyOptimisticWrites(t, pb) {
8836
11079
  const fw = t.fam?.node;
8837
11080
  if (fw?._transition) globalQueue.initTransition(fw._transition);
8838
11081
  const old = t.v;
11082
+ // Patch channel (override-application site): the draft IS the intended
11083
+ // visible state; prev is the view before these overrides apply. Bypasses
11084
+ // the transition stash — optimism is visible in flight.
11085
+ if (t.pc !== null && t.pc.p !== null)
11086
+ patchHooks.emitPatchOptimistic(t, pb, optimisticView(t, old));
11087
+ // Row-ops channel (family increment 2): optimistic STRUCTURE on an array
11088
+ // rides node overrides — it never enters the reconcile walk — so a driven
11089
+ // list must get its structural ops here, lane-timed. Identity diff of the
11090
+ // pre-write optimistic view against the draft; aligned writes emit nothing.
11091
+ if (t.pc !== null && t.pc.ro !== null && Array.isArray(pb)) {
11092
+ const prevView = optimisticView(t, old);
11093
+ if (Array.isArray(prevView)) {
11094
+ const ops = buildIdentityRowOps(prevView, pb);
11095
+ if (ops !== null) rowHooks.emitRowOpsOptimistic(t, pb, ops);
11096
+ }
11097
+ }
8839
11098
  const visible = (key, fallback) => {
8840
11099
  const node = t.n?.[key];
8841
11100
  return node !== undefined && hasActiveOverride(node)
@@ -8886,92 +11145,22 @@ function notifyOptimisticWrites(t, pb) {
8886
11145
  // (structural ones already ride the key-set bump above).
8887
11146
  bumpDeep(t);
8888
11147
  // Discard the draft — committed raw is untouched (revert target by
8889
- // construction). Register the root store for the scheduler's settle hooks
8890
- // and the target for landing consumption (RUL-2).
8891
- t.pb = null;
11148
+ // construction) restoring any truth-staged backing this draft displaced
11149
+ // (#3164 fold: ensurePB parked it so tentative writes could not pollute
11150
+ // staged truth). Register the root store for the scheduler's settle hooks.
11151
+ t.pb = stagedTruthPB.get(t) ?? null;
11152
+ if (t.pb !== null) stagedTruthPB.delete(t);
8892
11153
  (t.fam.overlaid ??= new Set()).add(t);
8893
11154
  GlobalQueue._trackOptimisticStore?.(t.fam.px ?? t.px);
8894
11155
  }
8895
- /**
8896
- * Landing consumption (RUL-2): fresh authoritative data supersedes every
8897
- * tentative override in the family. Mirrors legacy clearProjectionOverride —
8898
- * drop the override, clear lane/ownership, notify subscribers whose visible
8899
- * value changes (reversion effects go to regular queues via the projection
8900
- * write posture the caller holds).
8901
- */
8902
- function consumeOverridesNext(fam) {
8903
- const overlaid = fam.overlaid;
8904
- if (overlaid === undefined || overlaid.size === 0) return;
8905
- runAuthoritative(() => {
8906
- for (const t of overlaid) {
8907
- const drop = (node, committed) => {
8908
- if (!hasActiveOverride(node)) return;
8909
- const prev = unwrapOverride(node._x?._overrideValue);
8910
- // Full legacy reset (clearOptimisticOverride parity): the landing is
8911
- // authoritative NOW — fold committed into the node directly instead
8912
- // of riding a transaction's commit (whose queues may be stashed with
8913
- // the transaction parked; the wake would strand until it settles).
8914
- ext(node)._overrideValue = NOT_PENDING;
8915
- node._config |= CONFIG_OPTIMISTIC;
8916
- const nx = node._x;
8917
- if (nx) {
8918
- nx._overrideOwner = null;
8919
- nx._optimisticLane = undefined;
8920
- }
8921
- node._pendingValue = NOT_PENDING;
8922
- node._value = committed;
8923
- if (!node._equals || !node._equals(prev, committed)) {
8924
- insertSubs(node, true);
8925
- schedule();
8926
- }
8927
- };
8928
- // Landing consumes STRUCTURAL optimism only (legacy layer parity):
8929
- // membership edits, array length, and the value overrides written WITH
8930
- // them (a key carrying an active presence override is an add/delete —
8931
- // classified BEFORE the adoption may have made the key exist in landed
8932
- // data). A pure value override on a key the landing carries stays with
8933
- // its owning transaction (rapid-toggle contract: a live action's edit
8934
- // of an existing entity rides on top of landed truth).
8935
- const isArr = Array.isArray(t.v);
8936
- const has = t.h;
8937
- let structuralKeys = null;
8938
- if (has !== null) {
8939
- for (const key of Reflect.ownKeys(has)) {
8940
- if (hasActiveOverride(has[key])) (structuralKeys ??= new Set()).add(key);
8941
- }
8942
- }
8943
- const nodes = t.n;
8944
- if (nodes !== null) {
8945
- for (const key of Reflect.ownKeys(nodes)) {
8946
- const structural =
8947
- structuralKeys?.has(key) || !(key in t.v) || (isArr && key === "length");
8948
- if (!structural) continue;
8949
- drop(nodes[key], isArr && key === "length" ? t.v.length : t.v[key]);
8950
- }
8951
- }
8952
- if (has !== null) {
8953
- for (const key of Reflect.ownKeys(has)) drop(has[key], key in t.v);
8954
- }
8955
- if (t.k !== null && hasActiveOverride(t.k)) {
8956
- ext(t.k)._overrideValue = NOT_PENDING;
8957
- t.k._config |= CONFIG_OPTIMISTIC;
8958
- const kx = t.k._x;
8959
- if (kx) {
8960
- kx._overrideOwner = null;
8961
- kx._optimisticLane = undefined;
8962
- }
8963
- insertSubs(t.k, true);
8964
- schedule();
8965
- }
8966
- }
8967
- overlaid.clear();
8968
- });
8969
- }
8970
11156
  /** Optimistic-view composition for snapshot/deep (O1: snapshot is the CURRENT
8971
11157
  * view, lane values included; a fresh copy per call during pending windows —
8972
- * RUL-12). Returns `src` untouched when no override is active on `t`. */
11158
+ * RUL-12). Returns `src` untouched when no override is active on `t`.
11159
+ * Authoritative-view reads (until()'s predicate) skip composition entirely:
11160
+ * the predicate observes authoritative truth, never the caller's tentative
11161
+ * overlay. (Write-side emission callers never run under such a compute.) */
8973
11162
  function optimisticView(t, src) {
8974
- if (t.fam?.opt !== true) return src;
11163
+ if (t.fam?.opt !== true || authoritativeRead()) return src;
8975
11164
  let out = null;
8976
11165
  const ensure = () => (out ??= Array.isArray(src) ? [...src] : { ...src });
8977
11166
  const nodes = t.n;
@@ -9011,7 +11200,9 @@ function applyTentative(t, incoming, keyFn) {
9011
11200
  if (keyFn) {
9012
11201
  const pk = keyFn(pv);
9013
11202
  const nk = keyFn(nv);
9014
- if (pk !== undefined && nk !== undefined && pk !== nk) return null;
11203
+ // SameValueZero (re-audit 3, P1-3): parity with the plain reconcile
11204
+ // channel — NaN keys are self-equal.
11205
+ if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return null;
9015
11206
  }
9016
11207
  return map.get(unwrapValue(pv)) ?? null;
9017
11208
  };
@@ -9026,16 +11217,31 @@ function applyTentative(t, incoming, keyFn) {
9026
11217
  const nk = keyFn(nv);
9027
11218
  if (nk !== undefined) {
9028
11219
  if (viewByKey === null) {
11220
+ // Occurrence-aware index queues (re-audit 3, P1-3): parity with
11221
+ // the plain adoption window — duplicate keys match per
11222
+ // occurrence, each view row consumed once.
9029
11223
  viewByKey = new Map();
9030
11224
  for (let j = 0; j < viewRows.length; j++) {
9031
11225
  const p = unwrapValue(viewRows[j]);
9032
11226
  if (isWrappable(p)) {
9033
11227
  const pk = keyFn(p);
9034
- if (pk !== undefined && !viewByKey.has(pk)) viewByKey.set(pk, p);
11228
+ if (pk === undefined) continue;
11229
+ const existing = viewByKey.get(pk);
11230
+ if (existing === undefined) viewByKey.set(pk, j);
11231
+ else if (Array.isArray(existing)) existing.push(j);
11232
+ else viewByKey.set(pk, [existing, j]);
9035
11233
  }
9036
11234
  }
9037
11235
  }
9038
- pv = viewByKey.get(nk);
11236
+ const m = viewByKey.get(nk);
11237
+ if (m === undefined) pv = undefined;
11238
+ else if (Array.isArray(m)) {
11239
+ pv = unwrapValue(viewRows[m.shift()]);
11240
+ if (m.length === 1) viewByKey.set(nk, m[0]);
11241
+ } else {
11242
+ pv = unwrapValue(viewRows[m]);
11243
+ viewByKey.delete(nk);
11244
+ }
9039
11245
  } else pv = unwrapValue(viewRows[i]);
9040
11246
  } else pv = unwrapValue(viewRows[i]);
9041
11247
  const ct = match(pv, nv);
@@ -10139,7 +12345,12 @@ function createLoadingBoundary(fn, fallback, options) {
10139
12345
  function createErrorBoundary(fn, fallback) {
10140
12346
  return createCollectionBoundary(STATUS_ERROR, fn, queue => {
10141
12347
  return fallback(accessor(queue._error), () => {
10142
- for (const source of queue._sources) recompute(source);
12348
+ for (const source of queue._sources) {
12349
+ // Non-computed sources (patch-channel registrations under plain
12350
+ // owners) are not recomputable — their reset is the record's next
12351
+ // transition re-applying the patch (re-audit 2, P1-4).
12352
+ if (source._fn !== undefined) recompute(source);
12353
+ }
10143
12354
  schedule();
10144
12355
  });
10145
12356
  });
@@ -10273,7 +12484,10 @@ function flattenArray(children, results = [], options) {
10273
12484
  } while (typeof child === "function" && !child.length);
10274
12485
  }
10275
12486
  if (Array.isArray(child)) {
10276
- needsUnwrap = flattenArray(child, results, options);
12487
+ // OR, don't overwrite: an accessor already pushed under doNotUnwrap
12488
+ // still needs the resolving wrapper even when a later sibling
12489
+ // fragment contains no functions (#3133).
12490
+ needsUnwrap = flattenArray(child, results, options) || needsUnwrap;
10277
12491
  } else if (
10278
12492
  options?.skipNonRendered &&
10279
12493
  (child == null || child === true || child === false || child === "")
@@ -10301,6 +12515,7 @@ export {
10301
12515
  NoOwnerError,
10302
12516
  NotReadyError,
10303
12517
  SUPPORTS_PROXY,
12518
+ TimeoutError,
10304
12519
  action,
10305
12520
  affects,
10306
12521
  clearSnapshots,
@@ -10340,9 +12555,13 @@ export {
10340
12555
  omit,
10341
12556
  onCleanup,
10342
12557
  onSettled,
12558
+ patchableRaw,
10343
12559
  peekNextChildId,
10344
12560
  reconcile,
10345
12561
  refresh,
12562
+ registerPatch,
12563
+ registerRowOps,
12564
+ registerSlotPatchNext as registerSlotPatch,
10346
12565
  releaseSnapshotScope,
10347
12566
  repeat,
10348
12567
  resetErrorHalt,
@@ -10351,6 +12570,10 @@ export {
10351
12570
  setContext,
10352
12571
  setSnapshotCapture,
10353
12572
  snapshot,
12573
+ storeHasFamily,
12574
+ storeHasOptimisticFamily,
12575
+ storeIsShallow,
10354
12576
  storePath,
12577
+ until,
10355
12578
  untrack
10356
12579
  };