@solidjs/signals 2.0.0-beta.30 → 2.0.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dev.js CHANGED
@@ -916,6 +916,12 @@ class GlobalQueue extends Queue {
916
916
  }
917
917
  if (batch._affectsNodes.length) activeTransition._affectsNodes.push(...batch._affectsNodes);
918
918
  for (const store of batch._optimisticStores) activeTransition._optimisticStores.add(store);
919
+ // Gated readers recorded against the ambient batch move with it: their
920
+ // replay-at-commit now happens at the transaction's completion.
921
+ if (batch._gatedSubs.size) {
922
+ for (const sub of batch._gatedSubs) activeTransition._gatedSubs.add(sub);
923
+ batch._gatedSubs.clear();
924
+ }
919
925
  currentBatch = this._batch = activeTransition;
920
926
  }
921
927
  for (const lane of activeLanes) {
@@ -1003,13 +1009,19 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
1003
1009
  // which installed the engine's hooks.
1004
1010
  if (batch._optimisticNodes.length) GlobalQueue._resolveOptimistic(batch._optimisticNodes);
1005
1011
  // Replay entanglement: subs recorded by the read-time gate get rescheduled
1006
- // so they re-run with the now-committed values visible.
1007
- if (completingTransition && completingTransition._gatedSubs.size) {
1008
- for (const sub of completingTransition._gatedSubs) {
1012
+ // so they re-run with the now-committed values visible. The ambient batch
1013
+ // replays too laneReadsCommitted records readers whose committed-view
1014
+ // read hid a same-tick plain write that just committed above (#2963).
1015
+ if (batch._gatedSubs.size) {
1016
+ for (const sub of batch._gatedSubs) {
1009
1017
  if (sub._flags & REACTIVE_DISPOSED) continue;
1010
1018
  enqueueSub(sub);
1011
1019
  }
1012
- completingTransition._gatedSubs.clear();
1020
+ batch._gatedSubs.clear();
1021
+ // A completing transition keeps the outer flush loop alive by itself;
1022
+ // the ambient batch needs the re-arm or the replay sits in the heap
1023
+ // until the next unrelated write.
1024
+ schedule();
1013
1025
  }
1014
1026
  // Declared motion ends with the transaction: settle (or plain flush end
1015
1027
  // for ambient marks) releases each registration's refcount. A non-empty
@@ -3503,12 +3515,25 @@ function gatedRead(el, owner, c) {
3503
3515
  * optimistic/lane-assigned signals, stale-mode reads, and pending owners.
3504
3516
  */
3505
3517
  function laneReadsCommitted(el, owner, c) {
3506
- return (
3518
+ if (
3507
3519
  el._overrideValue !== undefined ||
3508
3520
  !!el._optimisticLane ||
3509
- (owner === el && stale && c._parentSource !== el) ||
3510
3521
  !!(owner._statusFlags & STATUS_PENDING)
3511
- );
3522
+ )
3523
+ return true;
3524
+ if (owner === el && stale && c._parentSource !== el) {
3525
+ // The committed view can hide a same-tick ambient write (a lane member —
3526
+ // even just an isPending companion flip — puts the reader "under a lane"
3527
+ // against unrelated plain writes). With no transaction the write commits
3528
+ // at THIS flush's end with no re-delivery, so record the reader for
3529
+ // replay at commit — the same contract gatedRead provides when a
3530
+ // transaction is active (#2963). If a transaction forms mid-flush,
3531
+ // initTransition carries the recording over to it.
3532
+ if (el._pendingValue !== NOT_PENDING && activeTransition === null)
3533
+ globalQueue._batch._gatedSubs.add(c);
3534
+ return true;
3535
+ }
3536
+ return false;
3512
3537
  }
3513
3538
  /**
3514
3539
  * recompute()'s lane posture: resolve the node's own lane (own=true), or adopt
package/dist/node.cjs CHANGED
@@ -806,6 +806,12 @@ class GlobalQueue extends Queue {
806
806
  }
807
807
  if (t.G.length) activeTransition.G.push(...t.G);
808
808
  for (const e of t.V) activeTransition.V.add(e);
809
+ // Gated readers recorded against the ambient batch move with it: their
810
+ // replay-at-commit now happens at the transaction's completion.
811
+ if (t.q.size) {
812
+ for (const e of t.q) activeTransition.q.add(e);
813
+ t.q.clear();
814
+ }
809
815
  currentBatch = this.m = activeTransition;
810
816
  }
811
817
  for (const e of activeLanes) {
@@ -867,10 +873,10 @@ function commitPendingNode(e) {
867
873
  e.We = e.k;
868
874
  e.k = NOT_PENDING;
869
875
  // Set _modified for effects, but not for tracked effects (they handle their own scheduling)
870
- if (e.Fe && e.Fe !== EFFECT_TRACKED) e.xe = true;
876
+ if (e.Fe && e.Fe !== EFFECT_TRACKED) e.Qe = true;
871
877
  }
872
878
  t.D &= ~REACTIVE_MANUAL_WRITE;
873
- if (!(t.Qe & STATUS_PENDING)) t.Qe &= ~STATUS_UNINITIALIZED;
879
+ if (!(t.xe & STATUS_PENDING)) t.xe &= ~STATUS_UNINITIALIZED;
874
880
  if (t.He !== null || t.Me !== null) GlobalQueue.ee(t, false, true);
875
881
  if (e._ || e.R) GlobalQueue.Ee(e);
876
882
  }
@@ -899,13 +905,19 @@ function finalizePureQueue(e = null, t = false) {
899
905
  // which installed the engine's hooks.
900
906
  if (t.L.length) GlobalQueue.Ie(t.L);
901
907
  // Replay entanglement: subs recorded by the read-time gate get rescheduled
902
- // so they re-run with the now-committed values visible.
903
- if (e && e.q.size) {
904
- for (const t of e.q) {
905
- if (t.D & REACTIVE_DISPOSED) continue;
906
- enqueueSub(t);
908
+ // so they re-run with the now-committed values visible. The ambient batch
909
+ // replays too laneReadsCommitted records readers whose committed-view
910
+ // read hid a same-tick plain write that just committed above (#2963).
911
+ if (t.q.size) {
912
+ for (const e of t.q) {
913
+ if (e.D & REACTIVE_DISPOSED) continue;
914
+ enqueueSub(e);
907
915
  }
908
- e.q.clear();
916
+ t.q.clear();
917
+ // A completing transition keeps the outer flush loop alive by itself;
918
+ // the ambient batch needs the re-arm or the replay sits in the heap
919
+ // until the next unrelated write.
920
+ schedule();
909
921
  }
910
922
  // Declared motion ends with the transaction: settle (or plain flush end
911
923
  // for ambient marks) releases each registration's refcount. A non-empty
@@ -1006,7 +1018,7 @@ function reporterBlocksSource(e, t) {
1006
1018
  e = e.t;
1007
1019
  }
1008
1020
  }
1009
- return !!(e.Qe & STATUS_PENDING && e.me instanceof NotReadyError && e.me.source === t);
1021
+ return !!(e.xe & STATUS_PENDING && e.me instanceof NotReadyError && e.me.source === t);
1010
1022
  }
1011
1023
 
1012
1024
  function transitionComplete(e) {
@@ -1022,7 +1034,7 @@ function transitionComplete(e) {
1022
1034
  }
1023
1035
  r.delete(e);
1024
1036
  }
1025
- if (!i) e.$.delete(n); else if (n.Qe & STATUS_PENDING && n.me?.source === n) {
1037
+ if (!i) e.$.delete(n); else if (n.xe & STATUS_PENDING && n.me?.source === n) {
1026
1038
  t = false;
1027
1039
  break;
1028
1040
  }
@@ -1061,8 +1073,8 @@ function runInTransition(e, t) {
1061
1073
  */ function enqueueSub(e) {
1062
1074
  if (e.Fe === EFFECT_TRACKED) {
1063
1075
  const t = e;
1064
- if (!t.xe) {
1065
- t.xe = true;
1076
+ if (!t.Qe) {
1077
+ t.Qe = true;
1066
1078
  t.Ze.enqueue(EFFECT_USER, t.Xe);
1067
1079
  }
1068
1080
  return;
@@ -1485,7 +1497,7 @@ function unlinkSubs(e) {
1485
1497
  // this same last-one-out check when that observer releases (the
1486
1498
  // untracked-read dispose in core.ts guards on pending identically).
1487
1499
  const e = t;
1488
- e.ke && e.Ue & CONFIG_AUTO_DISPOSE && !(e.D & REACTIVE_ZOMBIE) && !(e.Qe & STATUS_PENDING) && unobserved(e);
1500
+ e.ke && e.Ue & CONFIG_AUTO_DISPOSE && !(e.D & REACTIVE_ZOMBIE) && !(e.xe & STATUS_PENDING) && unobserved(e);
1489
1501
  }
1490
1502
  }
1491
1503
  return n;
@@ -1618,7 +1630,7 @@ function forEachDependent(e, t) {
1618
1630
  // callbacks handle their own release (settleAutodispose in handleAsync); this
1619
1631
  // covers derivatively-pending dependents, which have no callbacks of their own.
1620
1632
  function releaseIfSettledUnobserved(e) {
1621
- e.ke && e.Ue & CONFIG_AUTO_DISPOSE && !e.U && !(e.D & REACTIVE_ZOMBIE) && !(e.Qe & STATUS_PENDING) && unobserved(e);
1633
+ e.ke && e.Ue & CONFIG_AUTO_DISPOSE && !e.U && !(e.D & REACTIVE_ZOMBIE) && !(e.xe & STATUS_PENDING) && unobserved(e);
1622
1634
  }
1623
1635
 
1624
1636
  // Error-path sweep: notifyStatus(STATUS_ERROR) clears dependents' pending
@@ -1684,7 +1696,7 @@ function settlePendingSource(e) {
1684
1696
  setPendingError(o, s);
1685
1697
  i !== null && i(o);
1686
1698
  } else {
1687
- o.Qe &= ~STATUS_PENDING;
1699
+ o.xe &= ~STATUS_PENDING;
1688
1700
  setPendingError(o);
1689
1701
  i !== null && i(o);
1690
1702
  if (o.At) {
@@ -1738,7 +1750,7 @@ function handleAsync(e, t, n) {
1738
1750
  // are the transaction's reveal machinery and always re-enter.
1739
1751
  const settleTransition = () => {
1740
1752
  const t = resolveTransition(e);
1741
- if (t && e.Qe & STATUS_UNINITIALIZED && !currentTransition(t).$.has(e)) {
1753
+ if (t && e.xe & STATUS_UNINITIALIZED && !currentTransition(t).$.has(e)) {
1742
1754
  // Drop the stale stamp too: the plain settle write (setSignal) and the
1743
1755
  // stash-path restamp both re-enter the transaction through it.
1744
1756
  e.T = null;
@@ -1765,7 +1777,7 @@ function handleAsync(e, t, n) {
1765
1777
  // with the new value, creating a fresh Promise that supersedes this one.
1766
1778
  if (e.D & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
1767
1779
  settleTransition();
1768
- const o = !!(e.Qe & STATUS_UNINITIALIZED);
1780
+ const o = !!(e.xe & STATUS_UNINITIALIZED);
1769
1781
  trimStaleDeps(e);
1770
1782
  clearStatus(e);
1771
1783
  const s = resolveLane(e);
@@ -1839,7 +1851,7 @@ function handleAsync(e, t, n) {
1839
1851
  // Returns whether the node released, so the iterator branch can stop
1840
1852
  // pulling values instead of pumping an unobserved stream forever (#2935).
1841
1853
  const settleAutodispose = () => {
1842
- if (e.Ue & CONFIG_AUTO_DISPOSE && !e.U && !(e.Qe & STATUS_PENDING)) {
1854
+ if (e.Ue & CONFIG_AUTO_DISPOSE && !e.U && !(e.xe & STATUS_PENDING)) {
1843
1855
  unobserved(e);
1844
1856
  return true;
1845
1857
  }
@@ -1961,7 +1973,7 @@ function clearStatus(e, t = false) {
1961
1973
  // (Unconditional: _reask is baked into the node literals, so this is a
1962
1974
  // plain store to an existing slot — no shape change.)
1963
1975
  e.Nt = false;
1964
- e.Qe = t ? 0 : e.Qe & STATUS_UNINITIALIZED;
1976
+ e.xe = t ? 0 : e.xe & STATUS_UNINITIALIZED;
1965
1977
  if (e.me) setPendingError(e);
1966
1978
  // Update pending signal for isPending() reactivity (companions only exist
1967
1979
  // once the verdict layer created them, which installs the hooks).
@@ -1980,13 +1992,13 @@ function notifyStatus(e, t, n, r, i) {
1980
1992
  if (!r) {
1981
1993
  if (t === STATUS_PENDING && o) {
1982
1994
  addPendingSource(e, o);
1983
- e.Qe = STATUS_PENDING | e.Qe & STATUS_UNINITIALIZED;
1995
+ e.xe = STATUS_PENDING | e.xe & STATUS_UNINITIALIZED;
1984
1996
  // Preserve the current source on this propagation so render-effect notification
1985
1997
  // can register every distinct pending source with the transition.
1986
1998
  setPendingError(e, o, n);
1987
1999
  } else {
1988
2000
  clearPendingSources(e);
1989
- e.Qe = t | (t !== STATUS_ERROR ? e.Qe & STATUS_UNINITIALIZED : 0);
2001
+ e.xe = t | (t !== STATUS_ERROR ? e.xe & STATUS_UNINITIALIZED : 0);
1990
2002
  e.me = n;
1991
2003
  }
1992
2004
  GlobalQueue.ce !== null && GlobalQueue.ce(e);
@@ -2136,11 +2148,11 @@ function recompute(e, t = false) {
2136
2148
  }
2137
2149
  let r = !!(e.D & REACTIVE_OPTIMISTIC_DIRTY);
2138
2150
  const i = e.A !== undefined && e.A !== NOT_PENDING;
2139
- const o = !!(e.Qe & STATUS_UNINITIALIZED);
2151
+ const o = !!(e.xe & STATUS_UNINITIALIZED);
2140
2152
  // Outgoing error, captured before the compute clears status: if this run
2141
2153
  // recovers to an unchanged value, dependents still holding this object must
2142
2154
  // be swept (settleErroredDependents, #2949).
2143
- const s = e.Qe & STATUS_ERROR ? e.me : undefined;
2155
+ const s = e.xe & STATUS_ERROR ? e.me : undefined;
2144
2156
  // Re-ask classification lives in the verdict module; capture the flag before
2145
2157
  // the recompute wipes _flags below.
2146
2158
  const u = (e.D & REACTIVE_REASK) !== 0;
@@ -2202,7 +2214,7 @@ function recompute(e, t = false) {
2202
2214
  // On a status-free node clearStatus is a guaranteed no-op: every branch
2203
2215
  // in its body is gated on one of these fields, and with _statusFlags === 0
2204
2216
  // the flags write (create or not) stores the 0 already there.
2205
- if (e.Qe !== 0 || e.yt !== undefined || e.me || e.Nt || e.At || e.je !== undefined || e._ !== undefined || e.R !== undefined || e.rt !== null) clearStatus(e, t);
2217
+ if (e.xe !== 0 || e.yt !== undefined || e.me || e.Nt || e.At || e.je !== undefined || e._ !== undefined || e.R !== undefined || e.rt !== null) clearStatus(e, t);
2206
2218
  // _optimisticLane is only ever assigned by engine paths.
2207
2219
  if (e.i) GlobalQueue.De(e);
2208
2220
  } catch (t) {
@@ -2242,7 +2254,7 @@ function recompute(e, t = false) {
2242
2254
  // gate: the explicit recompute(node, true) inside effect() does not enqueue, so
2243
2255
  // effect() can call its runner synchronously for the first run.
2244
2256
  if (n && l) {
2245
- e.xe = !e.me;
2257
+ e.Qe = !e.me;
2246
2258
  // Reuse one bound runner per effect — runEffect no-ops on a stale
2247
2259
  // `_modified`, so re-enqueueing the same function is harmless.
2248
2260
  if (!t) e.Ze.enqueue(n, e.Ct ??= GlobalQueue.te.bind(null, e));
@@ -2295,13 +2307,13 @@ function recompute(e, t = false) {
2295
2307
  if (s !== undefined && !l && !e.me) settleErroredDependents(e, s);
2296
2308
  }
2297
2309
  currentOptimisticLane = E;
2298
- const O = e.k !== NOT_PENDING || e.He !== null || e.Me !== null || (e.Qe & (STATUS_PENDING | STATUS_UNINITIALIZED)) !== 0;
2310
+ const O = e.k !== NOT_PENDING || e.He !== null || e.Me !== null || (e.xe & (STATUS_PENDING | STATUS_UNINITIALIZED)) !== 0;
2299
2311
  // Override-covered holds (hasOverride) always queue: their commit belongs
2300
2312
  // to their own transition's schedule (A18 re-rule) and is unobservable
2301
2313
  // under the override (A17). Revert no longer commits anything, so an
2302
2314
  // unqueued covered hold would leak (INV-7) once the revert clears
2303
2315
  // _transition.
2304
- O && (!t || e.Qe & STATUS_PENDING) && (!e.T || i) && queuePendingNode(e);
2316
+ O && (!t || e.xe & STATUS_PENDING) && (!e.T || i) && queuePendingNode(e);
2305
2317
  e.T && n && activeTransition !== e.T && runInTransition(e.T, () => recompute(e));
2306
2318
  }
2307
2319
 
@@ -2351,7 +2363,7 @@ function computed(e, t) {
2351
2363
  ft: null,
2352
2364
  ot: null,
2353
2365
  D: t?.lazy ? REACTIVE_LAZY : REACTIVE_NONE,
2354
- Qe: STATUS_UNINITIALIZED,
2366
+ xe: STATUS_UNINITIALIZED,
2355
2367
  H: clock,
2356
2368
  k: NOT_PENDING,
2357
2369
  Me: null,
@@ -2396,7 +2408,7 @@ function computed(e, t) {
2396
2408
  ft: null,
2397
2409
  ot: null,
2398
2410
  D: REACTIVE_LAZY,
2399
- Qe: STATUS_UNINITIALIZED,
2411
+ xe: STATUS_UNINITIALIZED,
2400
2412
  H: clock,
2401
2413
  k: NOT_PENDING,
2402
2414
  Me: null,
@@ -2404,7 +2416,7 @@ function computed(e, t) {
2404
2416
  lt: null,
2405
2417
  T: null,
2406
2418
  Nt: false,
2407
- xe: false,
2419
+ Qe: false,
2408
2420
  bt: undefined,
2409
2421
  Dt: t,
2410
2422
  wt: n,
@@ -2437,7 +2449,7 @@ function setupComputedNode(e, t) {
2437
2449
  if (GlobalQueue.ue !== null) GlobalQueue.ue(e);
2438
2450
  !t?.lazy && recompute(e, true);
2439
2451
  if (snapshotCaptureActive && !t?.lazy) {
2440
- if (!(e.Qe & STATUS_PENDING) && !(e.Ue & CONFIG_NO_SNAPSHOT)) {
2452
+ if (!(e.xe & STATUS_PENDING) && !(e.Ue & CONFIG_NO_SNAPSHOT)) {
2441
2453
  e.Le = e.We === undefined ? NO_SNAPSHOT : e.We;
2442
2454
  snapshotSources.add(e);
2443
2455
  }
@@ -2458,7 +2470,7 @@ function signal(e, t, n = null) {
2458
2470
  k: NOT_PENDING
2459
2471
  };
2460
2472
  n && (n.rt = r);
2461
- if (snapshotCaptureActive && !(r.Ue & CONFIG_NO_SNAPSHOT) && !((n?.Qe ?? 0) & STATUS_PENDING)) {
2473
+ if (snapshotCaptureActive && !(r.Ue & CONFIG_NO_SNAPSHOT) && !((n?.xe ?? 0) & STATUS_PENDING)) {
2462
2474
  r.Le = e === undefined ? NO_SNAPSHOT : e;
2463
2475
  snapshotSources.add(r);
2464
2476
  }
@@ -2589,7 +2601,7 @@ function read(e) {
2589
2601
  }
2590
2602
  }
2591
2603
  }
2592
- if (i.Qe & STATUS_PENDING) {
2604
+ if (i.xe & STATUS_PENDING) {
2593
2605
  if (t && !(stale && i.T && activeTransition !== i.T)) {
2594
2606
  // Per-lane suspension lives with the engine (a non-null lane implies it
2595
2607
  // is installed): under a lane, only same-lane pending async without an
@@ -2598,10 +2610,10 @@ function read(e) {
2598
2610
  if (!tracking && e !== t) link(e, t);
2599
2611
  throw i.me;
2600
2612
  }
2601
- } else if (t && i !== e && i.Qe & STATUS_UNINITIALIZED) {
2613
+ } else if (t && i !== e && i.xe & STATUS_UNINITIALIZED) {
2602
2614
  if (!tracking && e !== t) link(e, t);
2603
2615
  throw i.me;
2604
- } else if (!t && i.Qe & STATUS_UNINITIALIZED) {
2616
+ } else if (!t && i.xe & STATUS_UNINITIALIZED) {
2605
2617
  throw i.me;
2606
2618
  }
2607
2619
  }
@@ -2609,7 +2621,7 @@ function read(e) {
2609
2621
  // firewall-backed reads follow the same rules (memo parity, #2897 ruling):
2610
2622
  // an errored derive throws for every late reader instead of silently
2611
2623
  // serving node values (the seed, or last-good data after a failed refetch).
2612
- if (i.ke && i.Qe & STATUS_ERROR) {
2624
+ if (i.ke && i.xe & STATUS_ERROR) {
2613
2625
  // Only a genuine reactive re-read may retry an errored async source:
2614
2626
  // - tracking: owned/tracked scope only (never events / `untrack` / effect side-effect phase)
2615
2627
  // - !pendingCheckActive: an `isPending` probe observes the error, never refetches
@@ -2650,7 +2662,7 @@ function read(e) {
2650
2662
  // Record that this isPending() probe observed the fresh pending value, so
2651
2663
  // the probe doesn't pair "pending" with the new value (#2831).
2652
2664
  if (pendingCheckActive) GlobalQueue.Te(e, o);
2653
- if (!t && i === e && typeof n.ke === "function" && e.Ue & CONFIG_AUTO_DISPOSE && !(i.Qe & STATUS_PENDING) && !e.U) {
2665
+ if (!t && i === e && typeof n.ke === "function" && e.Ue & CONFIG_AUTO_DISPOSE && !(i.xe & STATUS_PENDING) && !e.U) {
2654
2666
  unobserved(e);
2655
2667
  }
2656
2668
  return o;
@@ -2667,7 +2679,7 @@ function setSignal(e, t) {
2667
2679
  if (typeof t === "function") t = t(n);
2668
2680
  // Uninitialized check first: the first commit has no previous value, so the
2669
2681
  // user comparator must not run against `undefined` (matches recompute).
2670
- const r = !!(e.Qe & STATUS_UNINITIALIZED) || !e.ht || !e.ht(n, t);
2682
+ const r = !!(e.xe & STATUS_UNINITIALIZED) || !e.ht || !e.ht(n, t);
2671
2683
  if (!r) return t;
2672
2684
  if (e.k === NOT_PENDING) queuePendingNode(e);
2673
2685
  e.k = t;
@@ -2960,7 +2972,7 @@ function isUndefined(e) {
2960
2972
  const n = e.A !== NOT_PENDING;
2961
2973
  const r = n ? unwrapOverride(e.A) : e.We;
2962
2974
  if (typeof t === "function") t = t(r);
2963
- const i = !!(e.Qe & STATUS_UNINITIALIZED) || !e.ht || !e.ht(r, t);
2975
+ const i = !!(e.xe & STATUS_UNINITIALIZED) || !e.ht || !e.ht(r, t);
2964
2976
  if (!i) {
2965
2977
  // Same-value write with an active override still entangles the current
2966
2978
  // action's transition — the hold must outlast all overlapping actions.
@@ -3001,7 +3013,7 @@ function isUndefined(e) {
3001
3013
  */ function transitionBlocked(e) {
3002
3014
  for (let t = 0; t < e.L.length; t++) {
3003
3015
  const n = e.L[t];
3004
- if (hasActiveOverride(n) && "Qe" in n && n.Qe & STATUS_PENDING && n.me instanceof NotReadyError) {
3016
+ if (hasActiveOverride(n) && "xe" in n && n.xe & STATUS_PENDING && n.me instanceof NotReadyError) {
3005
3017
  return true;
3006
3018
  }
3007
3019
  }
@@ -3019,7 +3031,7 @@ function resolveOptimisticNodes(e) {
3019
3031
  // Revert is a pure drop: there is no revert target to commit —
3020
3032
  // override-covered authoritative values hold in _pendingValue and
3021
3033
  // elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
3022
- if (!(t.Qe & STATUS_PENDING)) t.Qe &= ~STATUS_UNINITIALIZED;
3034
+ if (!(t.xe & STATUS_PENDING)) t.xe &= ~STATUS_UNINITIALIZED;
3023
3035
  const r = t.A;
3024
3036
  t.A = NOT_PENDING;
3025
3037
  if (r !== NOT_PENDING && t.We !== unwrapOverride(r)) insertSubs(t, true);
@@ -3097,7 +3109,19 @@ function cleanupCompletedLanes(e) {
3097
3109
  * read()'s value selection under a lane: return the committed `_value` for
3098
3110
  * optimistic/lane-assigned signals, stale-mode reads, and pending owners.
3099
3111
  */ function laneReadsCommitted(e, t, n) {
3100
- return e.A !== undefined || !!e.i || t === e && stale && n.t !== e || !!(t.Qe & STATUS_PENDING);
3112
+ if (e.A !== undefined || !!e.i || !!(t.xe & STATUS_PENDING)) return true;
3113
+ if (t === e && stale && n.t !== e) {
3114
+ // The committed view can hide a same-tick ambient write (a lane member —
3115
+ // even just an isPending companion flip — puts the reader "under a lane"
3116
+ // against unrelated plain writes). With no transaction the write commits
3117
+ // at THIS flush's end with no re-delivery, so record the reader for
3118
+ // replay at commit — the same contract gatedRead provides when a
3119
+ // transaction is active (#2963). If a transaction forms mid-flush,
3120
+ // initTransition carries the recording over to it.
3121
+ if (e.k !== NOT_PENDING && activeTransition === null) globalQueue.m.q.add(n);
3122
+ return true;
3123
+ }
3124
+ return false;
3101
3125
  }
3102
3126
 
3103
3127
  /**
@@ -3223,7 +3247,7 @@ function collectPendingSources(e) {
3223
3247
  // not flow through it — matching the rails' behavior, where propagation
3224
3248
  // stopped at errored nodes. A DIRECT mark on an errored node still reads
3225
3249
  // pending (the count check above), also matching.
3226
- if (e.Qe & STATUS_ERROR) return false;
3250
+ if (e.xe & STATUS_ERROR) return false;
3227
3251
  if (t.has(e)) return false;
3228
3252
  t.add(e);
3229
3253
  const n = e.Be;
@@ -3256,7 +3280,7 @@ function quietPending(e) {
3256
3280
  }
3257
3281
 
3258
3282
  function newQuestionInFlight(e) {
3259
- return !!(e.Qe & STATUS_PENDING) && !(e.Qe & STATUS_UNINITIALIZED) && !quietPending(e);
3283
+ return !!(e.xe & STATUS_PENDING) && !(e.xe & STATUS_UNINITIALIZED) && !quietPending(e);
3260
3284
  }
3261
3285
 
3262
3286
  function computePendingState(e) {
@@ -3273,9 +3297,9 @@ function computePendingState(e) {
3273
3297
  return newQuestionInFlight(n);
3274
3298
  }
3275
3299
  if (n && e.k !== NOT_PENDING && !hasActiveOverride(e)) {
3276
- return !!(n.D & REACTIVE_MANUAL_WRITE) || !n.lt && !(n.Qe & STATUS_PENDING) || !!(n.Qe & STATUS_PENDING) && quietPending(n);
3300
+ return !!(n.D & REACTIVE_MANUAL_WRITE) || !n.lt && !(n.xe & STATUS_PENDING) || !!(n.xe & STATUS_PENDING) && quietPending(n);
3277
3301
  }
3278
- if (e.k !== NOT_PENDING && !(t.Qe & STATUS_UNINITIALIZED)) {
3302
+ if (e.k !== NOT_PENDING && !(t.xe & STATUS_UNINITIALIZED)) {
3279
3303
  if (hasActiveOverride(e)) return !e.ht || !e.ht(e.k, unwrapOverride(e.A));
3280
3304
  return true;
3281
3305
  }
@@ -3385,12 +3409,12 @@ function getLatestValueComputed(e) {
3385
3409
  }
3386
3410
  i = read(t);
3387
3411
  } catch (t) {
3388
- if (t instanceof NotReadyError && (!context || !(e.Qe & STATUS_UNINITIALIZED))) return r;
3412
+ if (t instanceof NotReadyError && (!context || !(e.xe & STATUS_UNINITIALIZED))) return r;
3389
3413
  throw t;
3390
3414
  } finally {
3391
3415
  setLatestReadActive(n);
3392
3416
  }
3393
- if (t.Qe & STATUS_PENDING) return r;
3417
+ if (t.xe & STATUS_PENDING) return r;
3394
3418
  if (stale && currentOptimisticLane && t.i) {
3395
3419
  const e = findLane(t.i);
3396
3420
  const n = findLane(currentOptimisticLane);
@@ -3409,7 +3433,7 @@ function getLatestValueComputed(e) {
3409
3433
  /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */ function pendingCheckRead(e, t, n, r) {
3410
3434
  setPendingCheckActive(false);
3411
3435
  if (typeof e.ke === "function") prepareComputed(e, true);
3412
- const i = n.Qe;
3436
+ const i = n.xe;
3413
3437
  if (t && i & STATUS_PENDING && i & STATUS_UNINITIALIZED) {
3414
3438
  if (tracking && e !== t) link(e, t);
3415
3439
  setPendingCheckActive(true);
@@ -3425,7 +3449,7 @@ function recordFreshRead(e, t) {
3425
3449
  }
3426
3450
 
3427
3451
  function applyReask(e, t) {
3428
- const n = !!(e.Qe & STATUS_PENDING);
3452
+ const n = !!(e.xe & STATUS_PENDING);
3429
3453
  const r = t && !(n && !e.Nt);
3430
3454
  const i = n && e.Nt !== r;
3431
3455
  e.Nt = r;
@@ -3468,7 +3492,7 @@ function isPending(e) {
3468
3492
  } catch (e) {
3469
3493
  collectPending();
3470
3494
  if (e instanceof NotReadyError) {
3471
- const t = !!(e.source?.Qe & STATUS_UNINITIALIZED);
3495
+ const t = !!(e.source?.xe & STATUS_UNINITIALIZED);
3472
3496
  if (r.found && !t) return true;
3473
3497
  if (context && t) throw e;
3474
3498
  }
@@ -3515,7 +3539,7 @@ GlobalQueue.Re = witnessAffects;
3515
3539
 
3516
3540
  function notifyEffectStatus(e, t) {
3517
3541
  // Use passed values if provided, otherwise read from node
3518
- const n = e !== undefined ? e : this.Qe;
3542
+ const n = e !== undefined ? e : this.xe;
3519
3543
  const r = t !== undefined ? t : this.me;
3520
3544
  if (n & STATUS_ERROR) {
3521
3545
  this.Ze.notify(this, STATUS_PENDING, 0);
@@ -3529,8 +3553,8 @@ function notifyEffectStatus(e, t) {
3529
3553
  // phase takes the success arm instead. Blocked forwards (explicit
3530
3554
  // `status` arg without node-state writes) don't queue: the status
3531
3555
  // re-propagates unblocked at commit.
3532
- if (this.Qe & STATUS_ERROR) {
3533
- this.xe = true;
3556
+ if (this.xe & STATUS_ERROR) {
3557
+ this.Qe = true;
3534
3558
  this.Ze.enqueue(this.Fe, this.Ct ??= runEffect.bind(null, this));
3535
3559
  }
3536
3560
  return;
@@ -3545,7 +3569,7 @@ function notifyEffectStatus(e, t) {
3545
3569
  }
3546
3570
 
3547
3571
  function runEffect(e) {
3548
- if (!e.xe || e.D & REACTIVE_DISPOSED) return;
3572
+ if (!e.Qe || e.D & REACTIVE_DISPOSED) return;
3549
3573
  // Error arm (#2840), user effects only: a compute-phase error that is still
3550
3574
  // the node's settled state at effect time runs the bundle's error handler in
3551
3575
  // this same imperative, writable scope. Unwrap the StatusError used for
@@ -3556,10 +3580,10 @@ function runEffect(e) {
3556
3580
  // Render effects bypass: their errors route to boundaries synchronously in
3557
3581
  // notifyEffectStatus, and a runner queued by an earlier valueChanged in the
3558
3582
  // same flush must not be hijacked by a later-arriving error status.
3559
- if (e.Qe & STATUS_ERROR && e.Fe === EFFECT_USER) {
3583
+ if (e.xe & STATUS_ERROR && e.Fe === EFFECT_USER) {
3560
3584
  const t = unwrapStatusError(e.me);
3561
3585
  e.bt = e.We;
3562
- e.xe = false;
3586
+ e.Qe = false;
3563
3587
  try {
3564
3588
  e.wt ? e.wt(t, () => {
3565
3589
  const t = e.Et;
@@ -3584,14 +3608,14 @@ function runEffect(e) {
3584
3608
  e.Et = n;
3585
3609
  } catch (t) {
3586
3610
  e.me = new StatusError(e, t);
3587
- e.Qe |= STATUS_ERROR;
3611
+ e.xe |= STATUS_ERROR;
3588
3612
  if (!e.Ze.notify(e, STATUS_ERROR, STATUS_ERROR)) {
3589
3613
  haltReactivity(t);
3590
3614
  throw t;
3591
3615
  }
3592
3616
  } finally {
3593
3617
  e.bt = e.We;
3594
- e.xe = false;
3618
+ e.Qe = false;
3595
3619
  }
3596
3620
  }
3597
3621
 
@@ -3603,9 +3627,9 @@ GlobalQueue.te = runEffect;
3603
3627
  * Uses stale reads.
3604
3628
  */ function trackedEffect(e, t) {
3605
3629
  const run = () => {
3606
- if (!n.xe || n.D & REACTIVE_DISPOSED) return;
3630
+ if (!n.Qe || n.D & REACTIVE_DISPOSED) return;
3607
3631
  try {
3608
- n.xe = false;
3632
+ n.Qe = false;
3609
3633
  recompute(n);
3610
3634
  } finally {}
3611
3635
  };
@@ -3621,10 +3645,10 @@ GlobalQueue.te = runEffect;
3621
3645
  });
3622
3646
  n.Et = undefined;
3623
3647
  n.Ue = n.Ue & ~CONFIG_AUTO_DISPOSE | CONFIG_CHILDREN_FORBIDDEN;
3624
- n.xe = true;
3648
+ n.Qe = true;
3625
3649
  n.Fe = EFFECT_TRACKED;
3626
3650
  n.yt = (e, t) => {
3627
- const r = e !== undefined ? e : n.Qe;
3651
+ const r = e !== undefined ? e : n.xe;
3628
3652
  if (r & STATUS_ERROR) {
3629
3653
  n.Ze.notify(n, STATUS_PENDING, 0);
3630
3654
  const e = t !== undefined ? t : n.me;
@@ -5629,7 +5653,7 @@ function prepareStoreWrite(e, t, n) {
5629
5653
  }
5630
5654
  const r = e[STORE_VALUE];
5631
5655
  const i = r[n];
5632
- if (snapshotCaptureActive && typeof n !== "symbol" && !((e[STORE_FIREWALL]?.Qe ?? 0) & STATUS_PENDING)) {
5656
+ if (snapshotCaptureActive && typeof n !== "symbol" && !((e[STORE_FIREWALL]?.xe ?? 0) & STATUS_PENDING)) {
5633
5657
  if (!e[STORE_SNAPSHOT_PROPS]) {
5634
5658
  e[STORE_SNAPSHOT_PROPS] = Object.create(null);
5635
5659
  snapshotSources?.add(e);
@@ -5752,7 +5776,7 @@ let Writing = null;
5752
5776
  */ function throwIfUnreadable(e) {
5753
5777
  const t = e[STORE_FIREWALL];
5754
5778
  if (!t) return;
5755
- const n = t.Qe;
5779
+ const n = t.xe;
5756
5780
  if (n & STATUS_ERROR || n & STATUS_UNINITIALIZED && n & STATUS_PENDING) throw t.me ?? new NotReadyError(t);
5757
5781
  }
5758
5782
 
@@ -6218,7 +6242,7 @@ function createOptimisticStore(e, t, n) {
6218
6242
  const e = GlobalQueue.Ae;
6219
6243
  GlobalQueue.Ae = t => {
6220
6244
  for (const e of t.V) {
6221
- if ((e[$TARGET]?.[STORE_FIREWALL]?.Qe ?? 0) & STATUS_PENDING) return true;
6245
+ if ((e[$TARGET]?.[STORE_FIREWALL]?.xe ?? 0) & STATUS_PENDING) return true;
6222
6246
  }
6223
6247
  return e(t);
6224
6248
  };
@@ -6815,8 +6839,8 @@ function mapArray(e, t, n) {
6815
6839
  kt: [],
6816
6840
  Wt: [],
6817
6841
  Ft: r,
6818
- xt: r || n?.keyed === false ? [] : undefined,
6819
- Qt: i && n?.keyed !== false ? [] : undefined,
6842
+ Qt: r || n?.keyed === false ? [] : undefined,
6843
+ xt: i && n?.keyed !== false ? [] : undefined,
6820
6844
  Ht: n?.keyed === false,
6821
6845
  Mt: n?.fallback
6822
6846
  };
@@ -6849,14 +6873,14 @@ function updateKeyedMap() {
6849
6873
  let n, r, i, o,
6850
6874
  // Mappers write freshly-created row/index signals into the STAGE
6851
6875
  // arrays (`rows`/`indexes`), never into `this._rows`/`this._indexes`.
6852
- s = this.xt ? this.Ht ? () => {
6876
+ s = this.Qt ? this.Ht ? () => {
6853
6877
  i[r] = signal(e[r], pureOptions);
6854
6878
  return this.Ut(accessor(i[r]), r);
6855
6879
  } : () => {
6856
6880
  i[r] = signal(e[r], pureOptions);
6857
6881
  o && (o[r] = signal(r, pureOptions));
6858
6882
  return this.Ut(accessor(i[r]), o ? accessor(o[r]) : undefined);
6859
- } : this.Qt ? () => {
6883
+ } : this.xt ? () => {
6860
6884
  const t = e[r];
6861
6885
  o[r] = signal(r, pureOptions);
6862
6886
  return this.Ut(t, accessor(o[r]));
@@ -6872,8 +6896,8 @@ function updateKeyedMap() {
6872
6896
  this.Vt = [];
6873
6897
  this.kt = [];
6874
6898
  this.Lt = 0;
6875
- this.xt && (this.xt = []);
6876
6899
  this.Qt && (this.Qt = []);
6900
+ this.xt && (this.xt = []);
6877
6901
  }
6878
6902
  if (this.Mt && !this.kt[0]) {
6879
6903
  // an aborted fallback attempt leaves an owner without a mapping;
@@ -6886,8 +6910,8 @@ function updateKeyedMap() {
6886
6910
  else if (this.Lt === 0) {
6887
6911
  const u = new Array(t);
6888
6912
  const l = new Array(t);
6889
- i = this.xt && new Array(t);
6890
- o = this.Qt && new Array(t);
6913
+ i = this.Qt && new Array(t);
6914
+ o = this.xt && new Array(t);
6891
6915
  try {
6892
6916
  for (r = 0; r < t; r++) u[r] = runWithOwner(l[r] = createOwner(), s);
6893
6917
  } catch (e) {
@@ -6899,19 +6923,19 @@ function updateKeyedMap() {
6899
6923
  // previous fallback
6900
6924
  this.kt = u;
6901
6925
  this.Wt = l;
6902
- i && (this.xt = i);
6903
- o && (this.Qt = o);
6926
+ i && (this.Qt = i);
6927
+ o && (this.xt = o);
6904
6928
  this.Vt = e.slice(0);
6905
6929
  this.Lt = t;
6906
6930
  } else {
6907
6931
  let u, l, a, c, f, E, d, S, T;
6908
6932
  // skip common prefix
6909
- for (u = 0, l = Math.min(this.Lt, t); u < l && (this.Vt[u] === e[u] || this.xt && compare(this.Ft, this.Vt[u], e[u])); u++) {
6910
- if (this.xt) setSignal(this.xt[u], e[u]);
6933
+ for (u = 0, l = Math.min(this.Lt, t); u < l && (this.Vt[u] === e[u] || this.Qt && compare(this.Ft, this.Vt[u], e[u])); u++) {
6934
+ if (this.Qt) setSignal(this.Qt[u], e[u]);
6911
6935
  }
6912
6936
  // skip common suffix — counted only; retained entries land in one pass
6913
6937
  // at commit instead of being staged and copied twice
6914
- for (l = this.Lt - 1, a = t - 1; l >= u && a >= u && (this.Vt[l] === e[a] || this.xt && compare(this.Ft, this.Vt[l], e[a])); l--,
6938
+ for (l = this.Lt - 1, a = t - 1; l >= u && a >= u && (this.Vt[l] === e[a] || this.Qt && compare(this.Ft, this.Vt[l], e[a])); l--,
6915
6939
  a--) ;
6916
6940
  // no structural change (every position matched in place at equal
6917
6941
  // length — the common post-reconcile shape): keep the same mapped
@@ -6923,8 +6947,8 @@ function updateKeyedMap() {
6923
6947
  const O = t - this.Lt;
6924
6948
  const _ = new Array(t);
6925
6949
  const R = new Array(t);
6926
- i = this.xt ? new Array(t) : undefined;
6927
- o = this.Qt ? new Array(t) : undefined;
6950
+ i = this.Qt ? new Array(t) : undefined;
6951
+ o = this.xt ? new Array(t) : undefined;
6928
6952
  // 0) prepare a map of all indices in the changed window of newItems,
6929
6953
  // scanning backwards so we encounter them in natural order
6930
6954
  E = new Map;
@@ -6946,8 +6970,8 @@ function updateKeyedMap() {
6946
6970
  if (r !== undefined && r !== -1) {
6947
6971
  _[r] = this.kt[n];
6948
6972
  R[r] = this.Wt[n];
6949
- i && (i[r] = this.xt[n]);
6950
- o && (o[r] = this.Qt[n]);
6973
+ i && (i[r] = this.Qt[n]);
6974
+ o && (o[r] = this.xt[n]);
6951
6975
  r = d[r];
6952
6976
  E.set(f, r);
6953
6977
  } else (S ??= []).push(this.Wt[n]);
@@ -6969,8 +6993,8 @@ function updateKeyedMap() {
6969
6993
  for (n = 0; n < u; n++) {
6970
6994
  _[n] = this.kt[n];
6971
6995
  R[n] = this.Wt[n];
6972
- i && (i[n] = this.xt[n]);
6973
- o && (o[n] = this.Qt[n]);
6996
+ i && (i[n] = this.Qt[n]);
6997
+ o && (o[n] = this.xt[n]);
6974
6998
  }
6975
6999
  for (r = u; r <= a; r++) {
6976
7000
  if (i) setSignal(i[r], e[r]);
@@ -6980,18 +7004,18 @@ function updateKeyedMap() {
6980
7004
  _[r] = this.kt[r - O];
6981
7005
  R[r] = this.Wt[r - O];
6982
7006
  if (i) {
6983
- i[r] = this.xt[r - O];
7007
+ i[r] = this.Qt[r - O];
6984
7008
  setSignal(i[r], e[r]);
6985
7009
  }
6986
7010
  if (o) {
6987
- o[r] = this.Qt[r - O];
7011
+ o[r] = this.xt[r - O];
6988
7012
  if (O !== 0) setSignal(o[r], r);
6989
7013
  }
6990
7014
  }
6991
7015
  this.kt = _;
6992
7016
  this.Wt = R;
6993
- i && (this.xt = i);
6994
- o && (this.Qt = o);
7017
+ i && (this.Qt = i);
7018
+ o && (this.xt = o);
6995
7019
  this.Lt = t;
6996
7020
  // save a copy of the mapped items for the next update
6997
7021
  this.Vt = e.slice(0);
@@ -7106,12 +7130,12 @@ function boundaryComputed(e, t) {
7106
7130
  });
7107
7131
  n.yt = (e, t) => {
7108
7132
  // Use passed values if provided, otherwise read from node
7109
- const r = e !== undefined ? e : n.Qe;
7133
+ const r = e !== undefined ? e : n.xe;
7110
7134
  const i = t !== undefined ? t : n.me;
7111
7135
  // Notify both status dimensions like a render effect does; the queue chain
7112
7136
  // consumes this boundary's own type and forwards the remainder upward until
7113
7137
  // a boundary that handles it is found.
7114
- n.Qe &= ~n.Yt;
7138
+ n.xe &= ~n.Yt;
7115
7139
  const o = n.Ze.notify(n, STATUS_PENDING | STATUS_ERROR, r, i);
7116
7140
  // The queue is the only propagation channel: a foreign status must not stay
7117
7141
  // reader-visible on the tree, or reads re-throw it across the boundary and
@@ -7119,8 +7143,8 @@ function boundaryComputed(e, t) {
7119
7143
  // untouched, so the tree still recomputes when the foreign source settles.
7120
7144
  const s = r & ~n.Yt & (STATUS_PENDING | STATUS_ERROR);
7121
7145
  if (s) {
7122
- n.Qe &= ~s;
7123
- if (n.me === i && !(n.Qe & (STATUS_PENDING | STATUS_ERROR))) n.me = undefined;
7146
+ n.xe &= ~s;
7147
+ if (n.me === i && !(n.xe & (STATUS_PENDING | STATUS_ERROR))) n.me = undefined;
7124
7148
  }
7125
7149
  // An ERROR the chain could not deliver to any boundary is uncaught. The
7126
7150
  // scrub above already removed it from reader-visible state, so without
@@ -7388,11 +7412,11 @@ class CollectionQueue extends Queue {
7388
7412
  // mark's lifetime (the visual channel): the marked node carries no
7389
7413
  // status of its own, so the count is the liveness test. The release
7390
7414
  // sweep (finalizePureQueue after mark release) re-runs this check.
7391
- if (e.D & REACTIVE_DISPOSED || !e.W && !(e.Qe & this.Tn) && !(this.Tn & STATUS_ERROR && e.Qe & STATUS_PENDING)) this.Bt.delete(e);
7415
+ if (e.D & REACTIVE_DISPOSED || !e.W && !(e.xe & this.Tn) && !(this.Tn & STATUS_ERROR && e.xe & STATUS_PENDING)) this.Bt.delete(e);
7392
7416
  }
7393
7417
  if (!this.Bt.size) {
7394
7418
  if (this.Tn & STATUS_PENDING && this.Zt && !this.rn && this.On) {
7395
- this.Zt = !!(this.On.Qe & this.Tn);
7419
+ this.Zt = !!(this.On.xe & this.Tn);
7396
7420
  } else {
7397
7421
  this.Zt = false;
7398
7422
  }
@@ -7428,7 +7452,7 @@ function createCollectionBoundary(e, t, n, r) {
7428
7452
  } catch (e) {
7429
7453
  if (e instanceof NotReadyError) t = true; else throw e;
7430
7454
  }
7431
- o.Zt = t || !!(s.Qe & e) || s.me instanceof NotReadyError;
7455
+ o.Zt = t || !!(s.xe & e) || s.me instanceof NotReadyError;
7432
7456
  });
7433
7457
  const u = _revealUsed && e === STATUS_PENDING ? getContext(RevealControllerContext) : null;
7434
7458
  if (u) {
@@ -46,8 +46,8 @@ import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, schedule
46
46
  // joint root). resolveTransition prefers this over the lane's _transition,
47
47
  // which a shared subscriber can merge across transactions (#2912).
48
48
  e.sn = activeTransition;
49
- const o = getOrCreateLane(e);
50
- e.Me = o;
49
+ const r = getOrCreateLane(e);
50
+ e.Me = r;
51
51
  // Literal undefined must not land raw: the slot doubles as the optimistic
52
52
  // brand, and erasing it makes the write invisible and routes follow-up
53
53
  // writes off the optimistic path into permanent commits (#2898).
@@ -164,7 +164,19 @@ function cleanupCompletedLanes(e) {
164
164
  * read()'s value selection under a lane: return the committed `_value` for
165
165
  * optimistic/lane-assigned signals, stale-mode reads, and pending owners.
166
166
  */ function laneReadsCommitted(e, n, t) {
167
- return e.Ae !== undefined || !!e.Me || n === e && stale && t.nn !== e || !!(n.S & STATUS_PENDING);
167
+ if (e.Ae !== undefined || !!e.Me || !!(n.S & STATUS_PENDING)) return true;
168
+ if (n === e && stale && t.nn !== e) {
169
+ // The committed view can hide a same-tick ambient write (a lane member —
170
+ // even just an isPending companion flip — puts the reader "under a lane"
171
+ // against unrelated plain writes). With no transaction the write commits
172
+ // at THIS flush's end with no re-delivery, so record the reader for
173
+ // replay at commit — the same contract gatedRead provides when a
174
+ // transaction is active (#2963). If a transaction forms mid-flush,
175
+ // initTransition carries the recording over to it.
176
+ if (e.De !== NOT_PENDING && activeTransition === null) globalQueue.m.ln.add(t);
177
+ return true;
178
+ }
179
+ return false;
168
180
  }
169
181
 
170
182
  /**
@@ -218,8 +230,8 @@ function trackOptimisticStore(e) {
218
230
  if (GlobalQueue.ht !== null) return;
219
231
  GlobalQueue.ht = optimisticWrite;
220
232
  GlobalQueue.En = resolveOptimisticNodes;
221
- GlobalQueue.dn = transitionBlocked;
222
- GlobalQueue.Tn = cleanupCompletedLanes;
233
+ GlobalQueue.Tn = transitionBlocked;
234
+ GlobalQueue.dn = cleanupCompletedLanes;
223
235
  GlobalQueue.In = runLaneEffects;
224
236
  GlobalQueue.Ot = gatedRead;
225
237
  GlobalQueue.Ct = laneSuspends;
@@ -330,8 +330,8 @@ class GlobalQueue extends Queue {
330
330
  // once the gate holds.
331
331
  static ht=null;
332
332
  static En=null;
333
- static dn=null;
334
333
  static Tn=null;
334
+ static dn=null;
335
335
  static In=null;
336
336
  static Ot=null;
337
337
  static Ct=null;
@@ -486,6 +486,12 @@ class GlobalQueue extends Queue {
486
486
  }
487
487
  if (t.A.length) activeTransition.A.push(...t.A);
488
488
  for (const e of t.cn) activeTransition.cn.add(e);
489
+ // Gated readers recorded against the ambient batch move with it: their
490
+ // replay-at-commit now happens at the transaction's completion.
491
+ if (t.ln.size) {
492
+ for (const e of t.ln) activeTransition.ln.add(e);
493
+ t.ln.clear();
494
+ }
489
495
  currentBatch = this.m = activeTransition;
490
496
  }
491
497
  for (const e of activeLanes) {
@@ -579,13 +585,19 @@ function finalizePureQueue(e = null, t = false) {
579
585
  // which installed the engine's hooks.
580
586
  if (t.je.length) GlobalQueue.En(t.je);
581
587
  // Replay entanglement: subs recorded by the read-time gate get rescheduled
582
- // so they re-run with the now-committed values visible.
583
- if (e && e.ln.size) {
584
- for (const t of e.ln) {
585
- if (t.se & REACTIVE_DISPOSED) continue;
586
- enqueueSub(t);
588
+ // so they re-run with the now-committed values visible. The ambient batch
589
+ // replays too laneReadsCommitted records readers whose committed-view
590
+ // read hid a same-tick plain write that just committed above (#2963).
591
+ if (t.ln.size) {
592
+ for (const e of t.ln) {
593
+ if (e.se & REACTIVE_DISPOSED) continue;
594
+ enqueueSub(e);
587
595
  }
588
- e.ln.clear();
596
+ t.ln.clear();
597
+ // A completing transition keeps the outer flush loop alive by itself;
598
+ // the ambient batch needs the re-arm or the replay sits in the heap
599
+ // until the next unrelated write.
600
+ schedule();
589
601
  }
590
602
  // Declared motion ends with the transaction: settle (or plain flush end
591
603
  // for ambient marks) releases each registration's refcount. A non-empty
@@ -604,7 +616,7 @@ function finalizePureQueue(e = null, t = false) {
604
616
  if (t.cn.size) GlobalQueue.Dt(t.cn, e);
605
617
  sweepTransientStoreNodes();
606
618
  // Lanes only enter activeLanes through the engine's getOrCreateLane.
607
- if (activeLanes.size) GlobalQueue.Tn(e);
619
+ if (activeLanes.size) GlobalQueue.dn(e);
608
620
  }
609
621
  }
610
622
 
@@ -710,7 +722,7 @@ function transitionComplete(e) {
710
722
  // Override blockage lives with the engine (absent hook = "no optimistic
711
723
  // blockage"); the hook's loops over _optimisticNodes/_optimisticStores are
712
724
  // no-ops when the transition holds neither, so no pre-check is needed.
713
- if (t && GlobalQueue.dn?.(e)) t = false;
725
+ if (t && GlobalQueue.Tn?.(e)) t = false;
714
726
  t && (e.fn = true);
715
727
  return t;
716
728
  }
@@ -33,8 +33,8 @@ function createOptimisticStore(e, t, r) {
33
33
  // clobbering instead of composing). Optimistic state clears when truth
34
34
  // lands or its transaction ends — never mid-refetch. Wrapped here (engine
35
35
  // is already installed above) so store-free apps never carry the check.
36
- const e = GlobalQueue.dn;
37
- GlobalQueue.dn = t => {
36
+ const e = GlobalQueue.Tn;
37
+ GlobalQueue.Tn = t => {
38
38
  for (const e of t.cn) {
39
39
  if ((e[$TARGET]?.[STORE_FIREWALL]?.S ?? 0) & STATUS_PENDING) return true;
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/signals",
3
- "version": "2.0.0-beta.30",
3
+ "version": "2.0.0-beta.31",
4
4
  "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",