@solidjs/signals 2.0.0-rc.4 → 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 (53) hide show
  1. package/dist/dev.js +1169 -186
  2. package/dist/node.cjs +2041 -1167
  3. package/dist/prod/boundaries.js +4 -1
  4. package/dist/prod/core/async.js +124 -95
  5. package/dist/prod/core/constants.js +55 -1
  6. package/dist/prod/core/core.js +297 -222
  7. package/dist/prod/core/effect.js +28 -28
  8. package/dist/prod/core/error.js +13 -1
  9. package/dist/prod/core/external.js +2 -2
  10. package/dist/prod/core/graph.js +27 -27
  11. package/dist/prod/core/heap.js +30 -30
  12. package/dist/prod/core/lanes.js +32 -32
  13. package/dist/prod/core/optimistic.js +54 -54
  14. package/dist/prod/core/owner.js +34 -34
  15. package/dist/prod/core/scheduler.js +318 -137
  16. package/dist/prod/core/verdict.js +112 -59
  17. package/dist/prod/index.js +3 -3
  18. package/dist/prod/map.js +106 -106
  19. package/dist/prod/signals.js +253 -25
  20. package/dist/prod/store/next/optimistic.js +262 -123
  21. package/dist/prod/store/next/patch.js +6 -6
  22. package/dist/prod/store/next/projection.js +23 -19
  23. package/dist/prod/store/next/store.js +129 -35
  24. package/dist/prod/store/store.js +2 -2
  25. package/dist/types/core/async.d.ts +2 -0
  26. package/dist/types/core/attribution.d.ts +9 -4
  27. package/dist/types/core/constants.d.ts +54 -0
  28. package/dist/types/core/core.d.ts +19 -20
  29. package/dist/types/core/error.d.ts +9 -0
  30. package/dist/types/core/index.d.ts +2 -2
  31. package/dist/types/core/scheduler.d.ts +34 -0
  32. package/dist/types/core/types.d.ts +12 -0
  33. package/dist/types/index.d.ts +3 -3
  34. package/dist/types/signals.d.ts +108 -0
  35. package/dist/types/store/next/optimistic.d.ts +13 -10
  36. package/dist/types/store/next/projection.d.ts +1 -1
  37. package/dist/types/store/next/store.d.ts +22 -0
  38. package/dist/types/store/next/target.d.ts +15 -0
  39. package/dist/types-cjs/core/async.d.cts +2 -0
  40. package/dist/types-cjs/core/attribution.d.cts +9 -4
  41. package/dist/types-cjs/core/constants.d.cts +54 -0
  42. package/dist/types-cjs/core/core.d.cts +19 -20
  43. package/dist/types-cjs/core/error.d.cts +9 -0
  44. package/dist/types-cjs/core/index.d.cts +2 -2
  45. package/dist/types-cjs/core/scheduler.d.cts +34 -0
  46. package/dist/types-cjs/core/types.d.cts +12 -0
  47. package/dist/types-cjs/index.d.cts +3 -3
  48. package/dist/types-cjs/signals.d.cts +108 -0
  49. package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
  50. package/dist/types-cjs/store/next/projection.d.cts +1 -1
  51. package/dist/types-cjs/store/next/store.d.cts +22 -0
  52. package/dist/types-cjs/store/next/target.d.cts +15 -0
  53. package/package.json +1 -1
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
@@ -1248,6 +1326,107 @@ function mergeTransitionState(target, outgoing) {
1248
1326
  }
1249
1327
  for (const sub of outgoing._gatedSubs) target._gatedSubs.add(sub);
1250
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
+ }
1251
1430
  function schedule() {
1252
1431
  if (halted) {
1253
1432
  notifyHalted();
@@ -1446,6 +1625,10 @@ class GlobalQueue extends Queue {
1446
1625
  static _laneReadsCommitted = null;
1447
1626
  static _recomputeLane = null;
1448
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;
1449
1632
  static _laneAsyncSettled = null;
1450
1633
  static _trackOptimisticStore = null;
1451
1634
  flush() {
@@ -1589,8 +1772,17 @@ class GlobalQueue extends Queue {
1589
1772
  return false;
1590
1773
  }
1591
1774
  initTransition(transition) {
1592
- if (transition) transition = currentTransition(transition);
1593
- 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
+ }
1594
1786
  if (!transition && activeTransition && activeTransition._time === clock) return;
1595
1787
  if (!activeTransition) {
1596
1788
  activeTransition = transition ?? createBatch();
@@ -1634,11 +1826,25 @@ class GlobalQueue extends Queue {
1634
1826
  for (const lane of activeLanes) {
1635
1827
  if (!lane._transition) lane._transition = activeTransition;
1636
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();
1637
1838
  }
1638
1839
  }
1639
1840
  function queuePendingNode(node) {
1841
+ lastStagedNodeName = node._name ?? null;
1640
1842
  currentBatch._pendingNodes.push(node);
1641
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;
1642
1848
  // Sticky: flips true on the first refresh() ever (the only setter of
1643
1849
  // REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
1644
1850
  // clear entirely in apps that never refresh.
@@ -1744,10 +1950,34 @@ let patchCommitHook = null;
1744
1950
  function setPatchCommitHook(fn) {
1745
1951
  patchCommitHook = fn;
1746
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 = [];
1747
1960
  function commitPendingNodes() {
1748
1961
  const pendingNodes = currentBatch._pendingNodes;
1749
1962
  for (let i = 0; i < pendingNodes.length; i++) {
1750
- 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
+ }
1751
1981
  }
1752
1982
  pendingNodes.length = 0;
1753
1983
  storeCommitHook?.();
@@ -1799,6 +2029,22 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
1799
2029
  // completing transition scopes the clear to its own layer keys (#2899).
1800
2030
  if (batch._optimisticStores.size)
1801
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
+ }
1802
2048
  sweepTransientStoreNodes();
1803
2049
  // Lanes only enter activeLanes through the engine's getOrCreateLane.
1804
2050
  if (activeLanes.size) GlobalQueue._cleanupLanes(completingTransition);
@@ -1879,7 +2125,20 @@ function flush(fn) {
1879
2125
  // `flush()` is an explicit drain point, so it must also process an active
1880
2126
  // transition even if no microtask was scheduled for it yet.
1881
2127
  while (scheduled$1 || activeTransition) {
1882
- if (++count === 1e5) throw new Error("Potential Infinite Loop Detected.");
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
+ }
1883
2142
  globalQueue.flush();
1884
2143
  }
1885
2144
  }
@@ -1941,6 +2200,29 @@ function runInTransition(transition, fn) {
1941
2200
  activeTransition = prevTransition;
1942
2201
  }
1943
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
+ }
1944
2226
 
1945
2227
  /** The queue a node belongs to, picked from its own zombie flag. */
1946
2228
  function queueFor(n) {
@@ -2709,11 +2991,11 @@ function settlePendingSource(el) {
2709
2991
  const errored = node._statusFlags & STATUS_ERROR;
2710
2992
  if (remaining) {
2711
2993
  if (!errored) setPendingError(node, remaining);
2712
- updateCompanions !== null && updateCompanions(node);
2994
+ updateCompanions?.(node);
2713
2995
  } else {
2714
2996
  node._statusFlags &= ~STATUS_PENDING;
2715
2997
  if (!errored) setPendingError(node);
2716
- updateCompanions !== null && updateCompanions(node);
2998
+ updateCompanions?.(node);
2717
2999
  if (node._x?._blocked) {
2718
3000
  enqueueSub(node);
2719
3001
  scheduled = true;
@@ -2736,6 +3018,14 @@ function settlePendingSource(el) {
2736
3018
  function isThenable(value) {
2737
3019
  return value != null && typeof value === "object" && typeof value.then === "function";
2738
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
+ }
2739
3029
  function handleAsync(el, result, setter) {
2740
3030
  let iterator = false;
2741
3031
  let thenable = false;
@@ -2773,6 +3063,11 @@ function handleAsync(el, result, setter) {
2773
3063
  });
2774
3064
  throw new Error(message);
2775
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.
2776
3071
  ext(el)._inFlight = result;
2777
3072
  let syncValue;
2778
3073
  // Settle-time transition re-entry. The loading rail is invisible to
@@ -2876,10 +3171,21 @@ function handleAsync(el, result, setter) {
2876
3171
  // only notified when the hold is visible to them: under an active
2877
3172
  // override every reader sees the override (A17), so waking subs would
2878
3173
  // re-show an unchanged view — the revert is the notification point.
2879
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, value);
3174
+ GlobalQueue._syncCompanions?.(el, value);
2880
3175
  if (!hasActiveOverride$1(el)) {
2881
3176
  if (attrHooks !== null) attrHooks.asyncEnd(el, undefined, value, true);
2882
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);
2883
3189
  }
2884
3190
  el._time = clock;
2885
3191
  } else if (lane) {
@@ -2894,7 +3200,7 @@ function handleAsync(el, result, setter) {
2894
3200
  // The latest() shadow write gives latest() effects independent lanes; the
2895
3201
  // _pendingSignal update is a no-op repeat of the clearStatus() call above
2896
3202
  // (computePendingState doesn't read _value).
2897
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, value);
3203
+ GlobalQueue._syncCompanions?.(el, value);
2898
3204
  insertSubs(el, true);
2899
3205
  }
2900
3206
  } catch (e) {
@@ -2976,6 +3282,11 @@ function handleAsync(el, result, setter) {
2976
3282
  } catch {}
2977
3283
  };
2978
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;
2979
3290
  // Release check before each next pull: an unobserved lazy node must tear
2980
3291
  // down (its close above runs via disposal, closing the iterator) instead
2981
3292
  // of pumping the stream forever with zero subscribers (#2935).
@@ -3207,7 +3518,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
3207
3518
  status | (status !== STATUS_ERROR ? el._statusFlags & STATUS_UNINITIALIZED : 0);
3208
3519
  ext(el)._error = error;
3209
3520
  }
3210
- GlobalQueue._updatePendingSignal !== null && GlobalQueue._updatePendingSignal(el);
3521
+ GlobalQueue._updatePendingSignal?.(el);
3211
3522
  if (
3212
3523
  el._x?._child &&
3213
3524
  el._config & CONFIG_CHILD_COMPANIONS &&
@@ -3352,7 +3663,15 @@ function recompute(el, create = false) {
3352
3663
  if (el._transition && (!isEffect || activeTransition) && activeTransition !== el._transition)
3353
3664
  globalQueue.initTransition(el._transition);
3354
3665
  deleteFromHeap(el, queueFor(el));
3355
- 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
+ }
3356
3675
  // Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring
3357
3676
  if (el._transition || isEffect === EFFECT_TRACKED) disposeChildren(el);
3358
3677
  else if (el._firstChild !== null || el._disposal !== null) {
@@ -3376,6 +3695,14 @@ function recompute(el, create = false) {
3376
3695
  // recovers to an unchanged value, dependents still holding this object must
3377
3696
  // be swept (settleErroredDependents, #2949).
3378
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;
3379
3706
  // Re-ask classification lives in the verdict module; capture the flag before
3380
3707
  // the recompute wipes _flags below.
3381
3708
  const hadReask = (el._flags & REACTIVE_REASK) !== 0;
@@ -3550,7 +3877,14 @@ function recompute(el, create = false) {
3550
3877
  // values directly — the pending round-trip (queuePendingNode +
3551
3878
  // commitPendingNodes) exists to sequence transition reveals, and
3552
3879
  // paying it per effect on the plain path is pure overhead.
3553
- (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)) ||
3554
3888
  isOptimisticDirty
3555
3889
  // NOTE (stage-3, 2026-08-21): a quiet-world MEMO direct-commit was
3556
3890
  // attempted here and REVERTED — memo staging is load-bearing beyond
@@ -3596,6 +3930,12 @@ function recompute(el, create = false) {
3596
3930
  if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
3597
3931
  el._pendingValue = value;
3598
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);
3599
3939
  } else if (el._height != oldHeight) {
3600
3940
  for (let s = el._subs; s !== null; s = s._nextSub) {
3601
3941
  insertIntoHeapHeight(s._sub, queueFor(s._sub));
@@ -3608,6 +3948,17 @@ function recompute(el, create = false) {
3608
3948
  // (el._x?._error re-set), so this only runs on a genuinely clean recovery.
3609
3949
  if (outgoingError !== undefined && !valueChanged && !el._x?._error)
3610
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);
3611
3962
  }
3612
3963
  // Attribution hook: fired before the lane restore so `currentOptimisticLane`
3613
3964
  // still reflects THIS run's posture. The facts distinguish an overlay
@@ -3656,7 +4007,9 @@ function updateIfNecessary(el) {
3656
4007
  // (_depsTail/_depGen) is live, and a nested recompute would corrupt it.
3657
4008
  // A mid-pass mark stays latched for recompute's own tail to reschedule
3658
4009
  // (#3037); readers meanwhile serve the values the pass has so far.
3659
- 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;
3660
4013
  if (el._flags & REACTIVE_CHECK) {
3661
4014
  for (let d = el._deps; d; d = d._nextDep) {
3662
4015
  const dep1 = d._dep;
@@ -3692,7 +4045,7 @@ function computed(fn, options) {
3692
4045
  (options?.sync ? CONFIG_SYNC : 0) |
3693
4046
  (options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0) |
3694
4047
  (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
3695
- _equals: options?.equals != null ? options.equals : isEqual,
4048
+ _equals: options?.equals ?? isEqual,
3696
4049
  _disposal: null,
3697
4050
  _queue: context?._queue ?? globalQueue,
3698
4051
  _context: context?._context ?? defaultContext,
@@ -3744,6 +4097,7 @@ function ext(el) {
3744
4097
  _parentSource: undefined,
3745
4098
  _affectsCount: 0,
3746
4099
  _inFlight: null,
4100
+ _flightTeardown: null,
3747
4101
  _error: undefined,
3748
4102
  _blocked: undefined,
3749
4103
  _pendingSources: undefined,
@@ -3771,6 +4125,7 @@ function createEffectNode(fn, effectFn, errorFn, type, options) {
3771
4125
  (transparent ? CONFIG_TRANSPARENT : 0) |
3772
4126
  (options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
3773
4127
  (options?.sync ? CONFIG_SYNC : 0) |
4128
+ (options?._extraConfig ?? 0) |
3774
4129
  (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
3775
4130
  _equals: false,
3776
4131
  _disposal: null,
@@ -3832,8 +4187,7 @@ function setEffectStatusNotify(fn) {
3832
4187
  * "display consumer" membership test in the status walks, exactly as the
3833
4188
  * per-node field did when every effect carried one. */
3834
4189
  function statusNotifierOf(el) {
3835
- const x = el._x;
3836
- const own = x !== null && x !== undefined ? x._notifyStatus : undefined;
4190
+ const own = el._x?._notifyStatus;
3837
4191
  if (own !== undefined) return own;
3838
4192
  return el._type ? (effectStatusNotify ?? undefined) : undefined;
3839
4193
  }
@@ -3876,7 +4230,7 @@ function setupComputedNode(self, options) {
3876
4230
  }
3877
4231
  function signal(v, options, firewall = null) {
3878
4232
  const s = {
3879
- _equals: options?.equals != null ? options.equals : isEqual,
4233
+ _equals: options?.equals ?? isEqual,
3880
4234
  _config:
3881
4235
  (options?.ownedWrite ? CONFIG_OWNED_WRITE : 0) |
3882
4236
  (options?._noSnapshot ? CONFIG_NO_SNAPSHOT : 0),
@@ -4013,6 +4367,35 @@ const READ_SLOW = Symbol("read-slow");
4013
4367
  * snapshot / transition / lane / dev-strictRead state all take the full
4014
4368
  * resolution. Anything slow returns READ_SLOW; the caller then calls read().
4015
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
+ }
4016
4399
  function readNodeFast(el) {
4017
4400
  if (
4018
4401
  latestReadActive ||
@@ -4092,6 +4475,13 @@ function read(el) {
4092
4475
  markHeap(elQueue);
4093
4476
  updateIfNecessary(owner);
4094
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);
4095
4485
  const height = owner._height;
4096
4486
  // parent check is shallow, might need to be recursive
4097
4487
  if (height >= c._height && el._parent !== c) {
@@ -4170,8 +4560,18 @@ function read(el) {
4170
4560
  nodeName: owner?._name
4171
4561
  });
4172
4562
  if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
4173
- // A17: the override IS the value for every reader.
4174
- 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;
4175
4575
  }
4176
4576
  // Entanglement gate: a reader recomputing under an optimistic lane that reads
4177
4577
  // a pending mid-transition write sees the committed value. Projection-store
@@ -4199,7 +4599,18 @@ function read(el) {
4199
4599
  (currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted(el, owner, c)) ||
4200
4600
  el._pendingValue === NOT_PENDING ||
4201
4601
  c._config & CONFIG_CHILDREN_FORBIDDEN ||
4202
- (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))
4203
4614
  ? el._value
4204
4615
  : el._pendingValue;
4205
4616
  // Record that this isPending() probe observed the fresh pending value, so
@@ -4244,9 +4655,17 @@ function devGuardStoreSetterWrite() {
4244
4655
  ownerName: context._name,
4245
4656
  data: { operation: "setStore" }
4246
4657
  });
4247
- 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));
4248
4661
  }
4249
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
+ }
4250
4669
  function setSignal(el, v) {
4251
4670
  if (
4252
4671
  !(el._config & CONFIG_OWNED_WRITE) &&
@@ -4264,7 +4683,7 @@ function setSignal(el, v) {
4264
4683
  nodeName: el._name,
4265
4684
  data: { operation: "setSignal" }
4266
4685
  });
4267
- throw new Error(REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE);
4686
+ throw new Error(ownedScopeWriteMessage(context));
4268
4687
  }
4269
4688
  if (el._transition && activeTransition !== el._transition)
4270
4689
  globalQueue.initTransition(el._transition);
@@ -4390,41 +4809,13 @@ function staleValues(fn, set = true) {
4390
4809
  }
4391
4810
  }
4392
4811
  /**
4393
- * Invalidates one reactive source, forcing it to re-execute even if its inputs
4394
- * haven't changed.
4395
- *
4396
- * Pass either a Solid-created accessor or a projected store created from
4397
- * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
4398
- * write-like invalidation operation: it does not read the target's value, and
4399
- * refreshing a plain signal accessor is a no-op.
4400
- *
4401
- * Use it to invalidate cached async values (e.g. force a re-fetch) without
4402
- * tearing the consumer down.
4403
- *
4404
- * @example
4405
- * ```ts
4406
- * const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
4407
- *
4408
- * // Re-fetch on demand
4409
- * <button onClick={() => refresh(user)}>Reload</button>
4410
- * ```
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.
4411
4817
  */
4412
- function refresh(target) {
4413
- const node = target?.[$REFRESH];
4414
- if (!node) {
4415
- {
4416
- const message =
4417
- "[INVALID_REFRESH_TARGET] refresh() expects a Solid source accessor or refreshable store. " +
4418
- "Pass the original source target, not a wrapper function or derived property read.";
4419
- emitDiagnostic({
4420
- code: "INVALID_REFRESH_TARGET",
4421
- kind: "write",
4422
- severity: "error",
4423
- message
4424
- });
4425
- throw new Error(message);
4426
- }
4427
- }
4818
+ function markRefresh(node) {
4428
4819
  if (
4429
4820
  context &&
4430
4821
  !((node._config ?? 0) & CONFIG_OWNED_WRITE) &&
@@ -5236,7 +5627,22 @@ function latestRead(el) {
5236
5627
  !(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
5237
5628
  ) {
5238
5629
  markHeap(queue);
5239
- 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
+ }
5240
5646
  }
5241
5647
  value = read(pendingComputed);
5242
5648
  } catch (e) {
@@ -5266,12 +5672,42 @@ function latestRead(el) {
5266
5672
  return pendingComputed._pendingValue;
5267
5673
  return value;
5268
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
+ }
5269
5691
  /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
5270
5692
  function pendingCheckRead(el, c, owner, firewall) {
5271
5693
  setPendingCheckActive(false);
5272
5694
  if (typeof el._fn === "function") prepareComputed(el, true);
5273
5695
  const ownerStatus = owner._statusFlags;
5274
- 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
+ ) {
5275
5711
  if (tracking && el !== c) link(el, c);
5276
5712
  setPendingCheckActive(true);
5277
5713
  throw owner._x?._error;
@@ -5352,6 +5788,17 @@ function isPending(fn) {
5352
5788
  });
5353
5789
  const collectPending = () => {
5354
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);
5355
5802
  const prevStrictRead = strictRead;
5356
5803
  setStrictRead(false);
5357
5804
  try {
@@ -5363,6 +5810,7 @@ function isPending(fn) {
5363
5810
  });
5364
5811
  } finally {
5365
5812
  setStrictRead(prevStrictRead);
5813
+ setLatestReadActive(prevLatest);
5366
5814
  setPendingCheckActive(true);
5367
5815
  }
5368
5816
  // A "not pending" verdict that exists only because this reader saw the
@@ -6086,11 +6534,259 @@ function resolve(fn) {
6086
6534
  rej(err);
6087
6535
  dispose();
6088
6536
  },
6089
- { 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 }
6090
6542
  );
6091
6543
  });
6092
6544
  });
6093
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 }
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
+ }
6787
+ });
6788
+ });
6789
+ }
6094
6790
  function createOptimistic(first, second) {
6095
6791
  // Install before the node exists: only engine-installed programs can carry
6096
6792
  // an _overrideValue slot (same runtime-install pattern as
@@ -7060,8 +7756,29 @@ function materializePB(target) {
7060
7756
  target.ovl = false;
7061
7757
  }
7062
7758
  function ensurePB(target) {
7063
- if (activeTransition !== null) foldBatches.set(target, activeTransition);
7064
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);
7065
7782
  if (pb === null) {
7066
7783
  // Prototype-chain overlay (#3044): plain-data non-array containers
7067
7784
  // outside projection/optimistic families open drafts in O(1) — own keys
@@ -7083,6 +7800,7 @@ function ensurePB(target) {
7083
7800
  // seed from committed truth — seeding overrides there would fold a lane
7084
7801
  // value into the committed home ("authority wins at reveal" would break).
7085
7802
  if (target.fam?.opt && !projectionWriteActive && !getWriteOverride()) {
7803
+ tentativePBs.add(pb);
7086
7804
  const nodes = target.n;
7087
7805
  if (nodes !== null) {
7088
7806
  for (const key of Reflect.ownKeys(nodes)) {
@@ -7200,6 +7918,17 @@ function queueFold(target) {
7200
7918
  * Refreshed on every write; resolved through currentTransition at drain
7201
7919
  * (transitions merge — same rule as heldMaskView). */
7202
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();
7203
7932
  /** Committed-time privatization for parent-chain slot updates (path copying). */
7204
7933
  function privatizeCommitted(target) {
7205
7934
  if (ownedRaw.has(target.v)) return;
@@ -7533,7 +8262,13 @@ function notifyWrites(t) {
7533
8262
  // IMMEDIATE — landed truth shows to untracked readers even while a
7534
8263
  // downstream consumer's own async still holds the effect-level reveal
7535
8264
  // (spec-async "verdicts never inherit consumers' in-flight state").
7536
- 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) {
7537
8272
  // Landed truth (post-await write-override): immediately visible to every
7538
8273
  // reader — any staged held view is superseded.
7539
8274
  if (t.ht !== null) t.ht = t.hv = null;
@@ -7796,17 +8531,37 @@ function readSource(target) {
7796
8531
  target.pb !== null &&
7797
8532
  (inDraft(target) ||
7798
8533
  getWriteOverride() ||
7799
- 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)) ||
7800
8542
  // A projection's pending backing is authoritative-elect: serve it to
7801
8543
  // context-free readers too UNLESS a transition is holding the node
7802
8544
  // commits (downstream async hold — stale committed is the contract)
7803
8545
  // or the reader is a CHILDREN_FORBIDDEN scope, which never observes
7804
8546
  // its own unsettled write (#3082, signal parity per #3006).
7805
- (target.fam !== null && !foldHeld(target) && !inForbiddenScope()))
8547
+ (target.fam !== null && !heldTruthMasked(target) && !foldHeld(target) && !inForbiddenScope()))
7806
8548
  )
7807
8549
  return target.pb;
7808
8550
  return target.v;
7809
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
+ }
7810
8565
  const hasOwn = Object.prototype.hasOwnProperty;
7811
8566
  // Allocation-free own-accessor probe (replaces eager descriptor scans — the
7812
8567
  // single biggest creation cost in the uibench profile): Annex-B lookups
@@ -7838,6 +8593,26 @@ function runAuthoritative(fn) {
7838
8593
  function hasActiveOverride(node) {
7839
8594
  return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
7840
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
+ }
7841
8616
  /** Context-aware node view for reads outside tracking: active override >
7842
8617
  * held pending (owner context) > the BACKING value. Committed truth lives in
7843
8618
  * the backing (single-home rule, O6) — node `_value` is never served here,
@@ -7848,11 +8623,24 @@ function hasActiveOverride(node) {
7848
8623
  function nodeValue(node, backing) {
7849
8624
  // latest() sees the in-flight parked value like an owner-context reader
7850
8625
  // does (#3075) — signal/memo parity for store-node-backed keys.
7851
- const v = hasActiveOverride(node)
7852
- ? unwrapOverride(node._x?._overrideValue)
7853
- : node._pendingValue !== NOT_PENDING && (latestReadActive || inOwnerContext())
7854
- ? node._pendingValue
7855
- : backing;
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;
7856
8644
  return v === FORCE ? backing : v;
7857
8645
  }
7858
8646
  /** Serve an own data key: node-first when a node exists (pending visibility,
@@ -7877,13 +8665,18 @@ function serveDataKey(target, key, backingValue, src, node) {
7877
8665
  read(getNode(target, key, backingValue));
7878
8666
  }
7879
8667
  }
7880
- 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;
7881
8672
  }
7882
8673
  if (inDraft(target)) {
7883
8674
  // Optimistic drafts before their first write have no pending backing yet;
7884
8675
  // reads must still see the live optimistic view (compose, not clobber —
7885
8676
  // #2951). Once ensurePB runs, the seeded clone carries the view.
7886
- 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()) {
7887
8680
  const node = target.n?.[key];
7888
8681
  if (node !== undefined && hasActiveOverride(node))
7889
8682
  v = unwrapOverride(node._x?._overrideValue);
@@ -8092,7 +8885,15 @@ const traps = {
8092
8885
  if (target.s) return serveShallow(target, key, nv);
8093
8886
  return isWrappable(nv) ? draftServe(target, wrapNext(nv, target, key)) : nv;
8094
8887
  }
8095
- } 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
+ ) {
8096
8897
  const node = target.n?.[key];
8097
8898
  if (node !== undefined && hasActiveOverride(node))
8098
8899
  v = unwrapOverride(node._x?._overrideValue);
@@ -8119,14 +8920,16 @@ const traps = {
8119
8920
  if (!inDraft(target)) {
8120
8921
  if (getObserver() !== null) {
8121
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.
8122
8925
  const nv = read(node);
8123
8926
  if (hasActiveOverride(node)) present = !!nv;
8124
- } else {
8927
+ } else if (!authoritativeServe()) {
8125
8928
  const node = target.h?.[key];
8126
8929
  if (node !== undefined && hasActiveOverride(node))
8127
8930
  present = !!unwrapOverride(node._x?._overrideValue);
8128
8931
  }
8129
- } else if (target.fam?.opt && target.pb === null) {
8932
+ } else if (target.fam?.opt && target.pb === null && !authoritativeServe()) {
8130
8933
  const node = target.h?.[key];
8131
8934
  if (node !== undefined && hasActiveOverride(node))
8132
8935
  present = !!unwrapOverride(node._x?._overrideValue);
@@ -8152,8 +8955,14 @@ const traps = {
8152
8955
  // Optimistic membership overlay: presence-node overrides add/remove keys
8153
8956
  // (per-transaction lifecycle rides the nodes — §6, FINDING-2's fix).
8154
8957
  // Draft reads before the first write overlay too (pb, once created, is
8155
- // seeded with the view).
8156
- 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
+ ) {
8157
8966
  let set = null;
8158
8967
  for (const key of Reflect.ownKeys(target.h)) {
8159
8968
  const node = target.h[key];
@@ -8175,7 +8984,7 @@ const traps = {
8175
8984
  if (target.del !== null && target.del.has(key)) return undefined;
8176
8985
  if (desc === undefined) desc = Object.getOwnPropertyDescriptor(target.v, key);
8177
8986
  }
8178
- if (target.fam?.opt && !inDraft(target)) {
8987
+ if (!authoritativeServe() && target.fam?.opt && !inDraft(target)) {
8179
8988
  const node = target.h?.[key];
8180
8989
  if (node !== undefined && hasActiveOverride(node)) {
8181
8990
  if (!unwrapOverride(node._x?._overrideValue)) return undefined; // opt delete
@@ -9138,7 +9947,8 @@ function descend(pv, nv, keyFn, fam, proj = false) {
9138
9947
  * driven from inside an enclosing authoritative-write scope (next-store
9139
9948
  * optimistic derives), and a hard `false` would clobber it mid-derive.
9140
9949
  */
9141
- function wrapDraft(inner, isActive, onDraftWrite) {
9950
+ function wrapDraft(inner, isActive, aroundWrite) {
9951
+ const write = op => (aroundWrite ? aroundWrite(op) : op());
9142
9952
  const traps = {
9143
9953
  get(_, prop) {
9144
9954
  let value;
@@ -9153,7 +9963,7 @@ function wrapDraft(inner, isActive, onDraftWrite) {
9153
9963
  }
9154
9964
  if (prop === $TARGET) return value;
9155
9965
  return typeof value === "object" && value !== null
9156
- ? wrapDraft(value, isActive, onDraftWrite)
9966
+ ? wrapDraft(value, isActive, aroundWrite)
9157
9967
  : value;
9158
9968
  },
9159
9969
  has(_, prop) {
@@ -9175,8 +9985,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
9175
9985
  setWriteOverride(true);
9176
9986
  setProjectionWriteActive(true);
9177
9987
  try {
9178
- inner[prop] = value;
9179
- onDraftWrite?.();
9988
+ write(() => {
9989
+ inner[prop] = value;
9990
+ });
9180
9991
  } finally {
9181
9992
  setWriteOverride(false);
9182
9993
  setProjectionWriteActive(was);
@@ -9189,8 +10000,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
9189
10000
  setWriteOverride(true);
9190
10001
  setProjectionWriteActive(true);
9191
10002
  try {
9192
- delete inner[prop];
9193
- onDraftWrite?.();
10003
+ write(() => {
10004
+ delete inner[prop];
10005
+ });
9194
10006
  } finally {
9195
10007
  setWriteOverride(false);
9196
10008
  setProjectionWriteActive(was);
@@ -9231,8 +10043,9 @@ function wrapDraft(inner, isActive, onDraftWrite) {
9231
10043
  setWriteOverride(true);
9232
10044
  setProjectionWriteActive(true);
9233
10045
  try {
9234
- Reflect.defineProperty(inner, prop, desc);
9235
- onDraftWrite?.();
10046
+ write(() => {
10047
+ Reflect.defineProperty(inner, prop, desc);
10048
+ });
9236
10049
  } finally {
9237
10050
  setWriteOverride(false);
9238
10051
  setProjectionWriteActive(was);
@@ -9284,7 +10097,7 @@ function createStoreDerivedNext(fn, seed, options) {
9284
10097
  }
9285
10098
  ];
9286
10099
  }
9287
- function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWrite) {
10100
+ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, aroundDraftWrite) {
9288
10101
  const owner = getOwner();
9289
10102
  let settled = false;
9290
10103
  let result;
@@ -9298,7 +10111,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
9298
10111
  const draft = wrapDraft(
9299
10112
  wrappedStore,
9300
10113
  () => !settled || owner._x?._inFlight === result,
9301
- onDraftWrite
10114
+ aroundDraftWrite
9302
10115
  );
9303
10116
  storeSetterNext(
9304
10117
  draft,
@@ -9313,7 +10126,7 @@ function runProjectionComputedNext(wrappedStore, fn, key, wrapCommit, onDraftWri
9313
10126
  if (v === s || v === undefined) return;
9314
10127
  const write = () =>
9315
10128
  storeSetterNext(wrappedStore, st => reconcileNextState(v, st, key, true), false);
9316
- wrapCommit ? wrapCommit(write) : write();
10129
+ wrapCommit ? wrapCommit(write, v) : write();
9317
10130
  };
9318
10131
  const sync = handleAsync(owner, result, commit);
9319
10132
  if (!owner._loading) commit(sync);
@@ -9884,12 +10697,31 @@ function armRowHooks() {
9884
10697
  * armed presence nodes (the §6 overlay), so structural optimism reverts with
9885
10698
  * the same per-transaction granularity (FINDING-2's fix by construction).
9886
10699
  *
9887
- * Derived form = an optimistic projection: the derive's recompute and its
9888
- * async commits run under projectionWriteActive (authoritative landings
9889
- * commit silently beneath any active overrides). The transitionBlocked
9890
- * 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
9891
10713
  * legacy/engine checks.
9892
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
+ }
9893
10725
  let blockedInstalled = false;
9894
10726
  function installNextBlockedHalf() {
9895
10727
  if (blockedInstalled) return;
@@ -9897,7 +10729,12 @@ function installNextBlockedHalf() {
9897
10729
  // Late-bind the optimistic machinery into the plain store/reconcile paths
9898
10730
  // (all call sites are fam?.opt-gated, so this always runs first) and the
9899
10731
  // affects witness's view resolver.
9900
- setOptHooks({ notifyOptimisticWrites, optimisticView, applyTentative });
10732
+ setOptHooks({
10733
+ notifyOptimisticWrites,
10734
+ optimisticView,
10735
+ applyTentative,
10736
+ retainsOptimism: transitionHoldsOptimism
10737
+ });
9901
10738
  setNextOptimisticViewResolver((t, raw) => optimisticView(t, raw));
9902
10739
  // Scheduler flush tails call _clearOptimisticStores whenever tracked
9903
10740
  // stores exist; next has no layer to clear — reverts are engine-native —
@@ -9918,6 +10755,13 @@ function installNextBlockedHalf() {
9918
10755
  // row identity against the post-revert view (resolved from the
9919
10756
  // target at drain — overrides are gone by then).
9920
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));
9921
10765
  }
9922
10766
  }
9923
10767
  }
@@ -9978,20 +10822,41 @@ function createOptimisticStoreNext(first, second, options) {
9978
10822
  };
9979
10823
  const store = wrapNext(initialValue, null, null, fam);
9980
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);
9981
10834
  if (fam.shallow) {
9982
10835
  store[$TARGET].s = true;
9983
10836
  markRawIngest(initialValue);
9984
10837
  }
9985
10838
  if (derived) {
9986
10839
  const fn = first;
9987
- // Async commits land outside the computed's sync body — re-apply the
9988
- // authoritative-write posture there too. Landings consume the family's
9989
- // tentative overrides (RUL-2: visible landed truth replaces optimism)
9990
- // both the reconcile-channel commit and per-op post-await draft writes.
9991
- const consume = () => consumeOverridesNext(fam);
9992
- const wrapCommit = write => {
9993
- runAuthoritative(write);
9994
- 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);
9995
10860
  };
9996
10861
  let nodeOptions;
9997
10862
  if (options?.seedLoadingValue) nodeOptions = { loadingValue: undefined };
@@ -10003,14 +10868,201 @@ function createOptimisticStoreNext(first, second, options) {
10003
10868
  fn,
10004
10869
  options?.key === undefined ? "id" : options.key,
10005
10870
  wrapCommit,
10006
- consume
10871
+ aroundDraftWrite
10007
10872
  )
10008
10873
  );
10009
10874
  }, nodeOptions);
10010
10875
  node._config &= ~CONFIG_AUTO_DISPOSE;
10011
10876
  fam.node = node;
10012
10877
  }
10013
- 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
+ }
10014
11066
  }
10015
11067
  // ---- optimistic-only store machinery (moved from next/store.ts /
10016
11068
  // next/reconcile.ts so plain-store bundles tree-shake it) ----
@@ -10093,96 +11145,22 @@ function notifyOptimisticWrites(t, pb) {
10093
11145
  // (structural ones already ride the key-set bump above).
10094
11146
  bumpDeep(t);
10095
11147
  // Discard the draft — committed raw is untouched (revert target by
10096
- // construction). Register the root store for the scheduler's settle hooks
10097
- // and the target for landing consumption (RUL-2).
10098
- 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);
10099
11153
  (t.fam.overlaid ??= new Set()).add(t);
10100
11154
  GlobalQueue._trackOptimisticStore?.(t.fam.px ?? t.px);
10101
11155
  }
10102
- /**
10103
- * Landing consumption (RUL-2): fresh authoritative data supersedes every
10104
- * tentative override in the family. Mirrors legacy clearProjectionOverride —
10105
- * drop the override, clear lane/ownership, notify subscribers whose visible
10106
- * value changes (reversion effects go to regular queues via the projection
10107
- * write posture the caller holds).
10108
- */
10109
- function consumeOverridesNext(fam) {
10110
- const overlaid = fam.overlaid;
10111
- if (overlaid === undefined || overlaid.size === 0) return;
10112
- runAuthoritative(() => {
10113
- for (const t of overlaid) {
10114
- const drop = (node, committed) => {
10115
- if (!hasActiveOverride(node)) return;
10116
- const prev = unwrapOverride(node._x?._overrideValue);
10117
- // Full legacy reset (clearOptimisticOverride parity): the landing is
10118
- // authoritative NOW — fold committed into the node directly instead
10119
- // of riding a transaction's commit (whose queues may be stashed with
10120
- // the transaction parked; the wake would strand until it settles).
10121
- ext(node)._overrideValue = NOT_PENDING;
10122
- node._config |= CONFIG_OPTIMISTIC;
10123
- const nx = node._x;
10124
- if (nx) {
10125
- nx._overrideOwner = null;
10126
- nx._optimisticLane = undefined;
10127
- }
10128
- node._pendingValue = NOT_PENDING;
10129
- node._value = committed;
10130
- if (!node._equals || !node._equals(prev, committed)) {
10131
- insertSubs(node, true);
10132
- schedule();
10133
- }
10134
- };
10135
- // Landing consumes STRUCTURAL optimism only (legacy layer parity):
10136
- // membership edits, array length, and the value overrides written WITH
10137
- // them (a key carrying an active presence override is an add/delete —
10138
- // classified BEFORE the adoption may have made the key exist in landed
10139
- // data). A pure value override on a key the landing carries stays with
10140
- // its owning transaction (rapid-toggle contract: a live action's edit
10141
- // of an existing entity rides on top of landed truth).
10142
- const isArr = Array.isArray(t.v);
10143
- const has = t.h;
10144
- let structuralKeys = null;
10145
- if (has !== null) {
10146
- for (const key of Reflect.ownKeys(has)) {
10147
- if (hasActiveOverride(has[key])) (structuralKeys ??= new Set()).add(key);
10148
- }
10149
- }
10150
- const nodes = t.n;
10151
- if (nodes !== null) {
10152
- for (const key of Reflect.ownKeys(nodes)) {
10153
- const structural =
10154
- structuralKeys?.has(key) || !(key in t.v) || (isArr && key === "length");
10155
- if (!structural) continue;
10156
- drop(nodes[key], isArr && key === "length" ? t.v.length : t.v[key]);
10157
- }
10158
- }
10159
- if (has !== null) {
10160
- for (const key of Reflect.ownKeys(has)) drop(has[key], key in t.v);
10161
- }
10162
- if (t.k !== null && hasActiveOverride(t.k)) {
10163
- ext(t.k)._overrideValue = NOT_PENDING;
10164
- t.k._config |= CONFIG_OPTIMISTIC;
10165
- const kx = t.k._x;
10166
- if (kx) {
10167
- kx._overrideOwner = null;
10168
- kx._optimisticLane = undefined;
10169
- }
10170
- insertSubs(t.k, true);
10171
- schedule();
10172
- }
10173
- // Patch channel (override-consumption site): visible truth flipped to
10174
- // committed for the consumed keys — force a re-apply from the live
10175
- // view so the DOM leaves the override state.
10176
- if (t.pc !== null && t.pc.p !== null) patchHooks.emitPatchOptimistic(t, null, null);
10177
- }
10178
- overlaid.clear();
10179
- });
10180
- }
10181
11156
  /** Optimistic-view composition for snapshot/deep (O1: snapshot is the CURRENT
10182
11157
  * view, lane values included; a fresh copy per call during pending windows —
10183
- * 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.) */
10184
11162
  function optimisticView(t, src) {
10185
- if (t.fam?.opt !== true) return src;
11163
+ if (t.fam?.opt !== true || authoritativeRead()) return src;
10186
11164
  let out = null;
10187
11165
  const ensure = () => (out ??= Array.isArray(src) ? [...src] : { ...src });
10188
11166
  const nodes = t.n;
@@ -11506,7 +12484,10 @@ function flattenArray(children, results = [], options) {
11506
12484
  } while (typeof child === "function" && !child.length);
11507
12485
  }
11508
12486
  if (Array.isArray(child)) {
11509
- 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;
11510
12491
  } else if (
11511
12492
  options?.skipNonRendered &&
11512
12493
  (child == null || child === true || child === false || child === "")
@@ -11534,6 +12515,7 @@ export {
11534
12515
  NoOwnerError,
11535
12516
  NotReadyError,
11536
12517
  SUPPORTS_PROXY,
12518
+ TimeoutError,
11537
12519
  action,
11538
12520
  affects,
11539
12521
  clearSnapshots,
@@ -11592,5 +12574,6 @@ export {
11592
12574
  storeHasOptimisticFamily,
11593
12575
  storeIsShallow,
11594
12576
  storePath,
12577
+ until,
11595
12578
  untrack
11596
12579
  };