@solidjs/signals 2.0.0-rc.1 → 2.0.0-rc.10

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 (150) hide show
  1. package/dist/dev-shared.js +7138 -0
  2. package/dist/dev.attribution.js +3307 -0
  3. package/dist/dev.js +5691 -4491
  4. package/dist/observe/affects.js +129 -0
  5. package/dist/observe/attribution.js +7 -0
  6. package/dist/observe/boundaries.js +793 -0
  7. package/dist/observe/core/action.js +177 -0
  8. package/dist/observe/core/async.js +849 -0
  9. package/dist/observe/core/attribution-costs.js +66 -0
  10. package/dist/observe/core/attribution-feedback.js +280 -0
  11. package/dist/observe/core/attribution-hooks.js +83 -0
  12. package/dist/observe/core/attribution-queries.js +43 -0
  13. package/dist/observe/core/attribution.js +2904 -0
  14. package/dist/observe/core/constants.js +283 -0
  15. package/dist/{types-cjs/core/context.d.cts → observe/core/context.js} +37 -12
  16. package/dist/observe/core/core.js +1999 -0
  17. package/dist/observe/core/dev.js +343 -0
  18. package/dist/observe/core/effect.js +200 -0
  19. package/dist/observe/core/error-hooks.js +71 -0
  20. package/dist/observe/core/error.js +80 -0
  21. package/dist/observe/core/external.js +98 -0
  22. package/dist/observe/core/graph.js +155 -0
  23. package/dist/observe/core/heap.js +147 -0
  24. package/dist/observe/core/invariants.js +45 -0
  25. package/dist/observe/core/lanes.js +205 -0
  26. package/dist/observe/core/optimistic.js +627 -0
  27. package/dist/observe/core/owner.js +353 -0
  28. package/dist/observe/core/scheduler.js +1547 -0
  29. package/dist/observe/core/verdict.js +674 -0
  30. package/dist/observe/index.js +59 -0
  31. package/dist/observe/map.js +484 -0
  32. package/dist/observe/signals.js +731 -0
  33. package/dist/observe/store/index.js +44 -0
  34. package/dist/observe/store/next/optimistic.js +717 -0
  35. package/dist/observe/store/next/projection.js +266 -0
  36. package/dist/observe/store/next/reconcile.js +375 -0
  37. package/dist/observe/store/next/store.js +2480 -0
  38. package/dist/observe/store/next/target.js +51 -0
  39. package/dist/observe/store/store.js +331 -0
  40. package/dist/observe/store/storePath.js +103 -0
  41. package/dist/observe/store/utils.js +1070 -0
  42. package/dist/prod/affects.js +10 -9
  43. package/dist/prod/attribution.js +45 -0
  44. package/dist/prod/boundaries.js +388 -178
  45. package/dist/prod/core/action.js +65 -37
  46. package/dist/prod/core/async.js +378 -163
  47. package/dist/prod/core/attribution-hooks.js +3 -0
  48. package/dist/prod/core/constants.js +186 -1
  49. package/dist/prod/core/context.js +10 -16
  50. package/dist/prod/core/core.js +1546 -416
  51. package/dist/prod/core/dev.js +18 -1
  52. package/dist/prod/core/effect.js +119 -79
  53. package/dist/prod/core/error-hooks.js +71 -0
  54. package/dist/prod/core/error.js +13 -1
  55. package/dist/prod/core/external.js +4 -4
  56. package/dist/prod/core/graph.js +105 -54
  57. package/dist/prod/core/heap.js +72 -65
  58. package/dist/prod/core/invariants.js +3 -2
  59. package/dist/prod/core/lanes.js +117 -52
  60. package/dist/prod/core/optimistic.js +458 -104
  61. package/dist/prod/core/owner.js +131 -84
  62. package/dist/prod/core/scheduler.js +998 -241
  63. package/dist/prod/core/verdict.js +417 -112
  64. package/dist/prod/index.js +20 -8
  65. package/dist/prod/map.js +312 -151
  66. package/dist/prod/signals.js +363 -32
  67. package/dist/prod/store/index.js +2 -0
  68. package/dist/prod/store/next/optimistic.js +530 -197
  69. package/dist/prod/store/next/projection.js +142 -57
  70. package/dist/prod/store/next/reconcile.js +149 -103
  71. package/dist/prod/store/next/store.js +1470 -346
  72. package/dist/prod/store/next/target.js +40 -9
  73. package/dist/prod/store/store.js +23 -19
  74. package/dist/prod/store/utils.js +997 -130
  75. package/dist/types/attribution.d.ts +19 -0
  76. package/dist/types/attribution.prod.d.ts +22 -0
  77. package/dist/types/boundaries.d.ts +78 -5
  78. package/dist/types/core/action.d.ts +12 -5
  79. package/dist/types/core/async.d.ts +3 -1
  80. package/dist/types/core/attribution-costs.d.ts +35 -0
  81. package/dist/types/core/attribution-feedback.d.ts +133 -0
  82. package/dist/types/core/attribution-hooks.d.ts +304 -0
  83. package/dist/types/core/attribution-queries.d.ts +24 -0
  84. package/dist/types/core/attribution.d.ts +892 -0
  85. package/dist/types/core/constants.d.ts +179 -0
  86. package/dist/types/core/core.d.ts +299 -25
  87. package/dist/types/core/dev.d.ts +421 -8
  88. package/dist/types/core/effect.d.ts +8 -1
  89. package/dist/types/core/error-hooks.d.ts +71 -0
  90. package/dist/types/core/error.d.ts +9 -0
  91. package/dist/types/core/graph.d.ts +23 -0
  92. package/dist/types/core/heap.d.ts +5 -3
  93. package/dist/types/core/index.d.ts +6 -3
  94. package/dist/types/core/invariants.d.ts +5 -1
  95. package/dist/types/core/lanes.d.ts +51 -19
  96. package/dist/types/core/owner.d.ts +15 -3
  97. package/dist/types/core/scheduler.d.ts +248 -1
  98. package/dist/types/core/types.d.ts +124 -41
  99. package/dist/types/index.d.ts +18 -5
  100. package/dist/types/map.d.ts +2 -0
  101. package/dist/types/signals.d.ts +145 -13
  102. package/dist/types/store/index.d.ts +7 -6
  103. package/dist/types/store/next/optimistic.d.ts +18 -13
  104. package/dist/types/store/next/projection.d.ts +5 -2
  105. package/dist/types/store/next/reconcile.d.ts +7 -0
  106. package/dist/types/store/next/store.d.ts +83 -11
  107. package/dist/types/store/next/target.d.ts +108 -11
  108. package/dist/types/store/store.d.ts +20 -11
  109. package/dist/types/store/utils.d.ts +188 -0
  110. package/package.json +31 -26
  111. package/dist/node.cjs +0 -8137
  112. package/dist/types/store/optimistic.d.ts +0 -45
  113. package/dist/types/store/projection.d.ts +0 -70
  114. package/dist/types/store/reconcile.d.ts +0 -46
  115. package/dist/types-cjs/affects.d.cts +0 -47
  116. package/dist/types-cjs/boundaries.d.cts +0 -175
  117. package/dist/types-cjs/core/action.d.cts +0 -64
  118. package/dist/types-cjs/core/async.d.cts +0 -21
  119. package/dist/types-cjs/core/constants.d.cts +0 -71
  120. package/dist/types-cjs/core/core.d.cts +0 -156
  121. package/dist/types-cjs/core/dev.d.cts +0 -64
  122. package/dist/types-cjs/core/effect.d.cts +0 -30
  123. package/dist/types-cjs/core/error.d.cts +0 -47
  124. package/dist/types-cjs/core/external.d.cts +0 -15
  125. package/dist/types-cjs/core/graph.d.cts +0 -5
  126. package/dist/types-cjs/core/heap.d.cts +0 -22
  127. package/dist/types-cjs/core/index.d.cts +0 -13
  128. package/dist/types-cjs/core/invariants.d.cts +0 -59
  129. package/dist/types-cjs/core/lanes.d.cts +0 -56
  130. package/dist/types-cjs/core/optimistic.d.cts +0 -6
  131. package/dist/types-cjs/core/owner.d.cts +0 -124
  132. package/dist/types-cjs/core/scheduler.d.cts +0 -172
  133. package/dist/types-cjs/core/types.d.cts +0 -148
  134. package/dist/types-cjs/core/verdict.d.cts +0 -2
  135. package/dist/types-cjs/index.d.cts +0 -10
  136. package/dist/types-cjs/map.d.cts +0 -69
  137. package/dist/types-cjs/package.json +0 -3
  138. package/dist/types-cjs/signals.d.cts +0 -624
  139. package/dist/types-cjs/store/index.d.cts +0 -19
  140. package/dist/types-cjs/store/next/optimistic.d.cts +0 -20
  141. package/dist/types-cjs/store/next/projection.d.cts +0 -24
  142. package/dist/types-cjs/store/next/reconcile.d.cts +0 -3
  143. package/dist/types-cjs/store/next/store.d.cts +0 -71
  144. package/dist/types-cjs/store/next/target.d.cts +0 -99
  145. package/dist/types-cjs/store/optimistic.d.cts +0 -45
  146. package/dist/types-cjs/store/projection.d.cts +0 -70
  147. package/dist/types-cjs/store/reconcile.d.cts +0 -46
  148. package/dist/types-cjs/store/store.d.cts +0 -138
  149. package/dist/types-cjs/store/storePath.d.cts +0 -58
  150. package/dist/types-cjs/store/utils.d.cts +0 -74
@@ -1,16 +1,18 @@
1
- import { EFFECT_RENDER, EFFECT_USER, STATUS_PENDING, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_DISPOSED, REACTIVE_ZOMBIE, NOT_PENDING, EFFECT_TRACKED, CONFIG_IN_SNAPSHOT_SCOPE, REACTIVE_SNAPSHOT_STALE, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_REASK, REACTIVE_MANUAL_WRITE, STATUS_UNINITIALIZED } from "./constants.js";
1
+ import { EFFECT_RENDER, EFFECT_USER, STATUS_PENDING, CONFIG_ADOPTED_UNFLUSHED, NOT_PENDING, REACTIVE_MANUAL_WRITE, STATUS_UNINITIALIZED, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_DISPOSED, CONFIG_HELD_TRUTH, CONFIG_HAS_COMPANIONS, EFFECT_TRACKED, CONFIG_INPUTS_PUBLISHED, REACTIVE_ZOMBIE, CONFIG_LANE_FRAME, CONFIG_HAS_LANE, CONFIG_HAS_SNAPSHOT, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MISSED_WAKE, CONFIG_IN_SNAPSHOT_SCOPE, REACTIVE_SNAPSHOT_STALE, CONFIG_SLOT_NODE, CONFIG_HELD_CHILDREN, REACTIVE_REASK, CONFIG_AUTHORITATIVE_READ } from "./constants.js";
2
2
 
3
- import { currentOptimisticLane } from "./core.js";
3
+ import { resyncUnflushedCompanions, currentOptimisticLane, slotUnobservedHook, markUnflushedStaged, hasActiveOverride } from "./core.js";
4
4
 
5
5
  import { DEV } from "./dev.js";
6
6
 
7
7
  import { NotReadyError } from "./error.js";
8
8
 
9
+ import { sweepDormant, trimStaleDeps } from "./graph.js";
10
+
9
11
  import { runHeap, deleteFromHeap, enqueueSub } from "./heap.js";
10
12
 
11
13
  import { activeLanes, assignOrMergeLane, findLane } from "./lanes.js";
12
14
 
13
- export { getOrCreateLane, hasActiveOverride, mergeLanes, resolveLane } from "./lanes.js";
15
+ export { getOrCreateLane, mergeLanes, resolveLane } from "./lanes.js";
14
16
 
15
17
  import { devCheckFlushStart, devCheckActiveOverrides, devCensusCompanions, devCheckQuiescent } from "./invariants.js";
16
18
 
@@ -19,26 +21,44 @@ const transitions = new Set;
19
21
  const dirtyQueue = {
20
22
  eE: new Array(2e3).fill(undefined),
21
23
  tE: false,
22
- Ve: 0,
24
+ ln: 0,
23
25
  EE: 0
24
26
  };
25
27
 
26
28
  const zombieQueue = {
27
29
  eE: new Array(2e3).fill(undefined),
28
30
  tE: false,
29
- Ve: 0,
31
+ ln: 0,
30
32
  EE: 0
31
33
  };
32
34
 
33
35
  /** runHeap callback that discards a queued zombie recompute instead of running
34
36
  * it: unlink pure recompute entries; strip just the recompute bit from dirtied
35
- * height-adjust entries so their height work still happens. */ function cancelZombieRecompute(e) {
36
- if (e.se & REACTIVE_IN_HEAP_HEIGHT) e.se &= -12; else {
37
+ * height-adjust entries so their height work still happens. A zombie dirtied
38
+ * through the lane channel (OPTIMISTIC_DIRTY — an override or a `latest()`
39
+ * companion) runs instead (#3444): a zombie renders mainline until the commit
40
+ * disposes it, and the lane's values ARE the mainline frame — the still-visible
41
+ * branch a held `Show` is removing showed the old `latest(count)` beside the
42
+ * new one outside. Its pass runs under the lane and its run lands on the
43
+ * lane's effect queue, so a held lane defers it exactly as it defers every
44
+ * other reader's. */ function cancelZombieRecompute(e) {
45
+ if (e.oe & REACTIVE_OPTIMISTIC_DIRTY && !laneZombie(e)) return GlobalQueue.Ke(e);
46
+ if (e.oe & REACTIVE_IN_HEAP_HEIGHT) e.oe &= -140; else {
37
47
  deleteFromHeap(e, zombieQueue);
38
- e.se &= -4;
48
+ e.oe &= -132;
39
49
  }
40
50
  }
41
51
 
52
+ /** A member of a parked LANE frame (CONFIG_LANE_FRAME on the owner whose pass
53
+ * parked it, #3662). The #3444 exception is for transaction zombies; a lane
54
+ * frame's member is retired by the run that carries the lane's values, so its
55
+ * lane-channel recompute is cancelled with the rest — run, it republished the
56
+ * retired frame under those values. Mainline writes still reach it (#3463). */ function laneZombie(e) {
57
+ let t = e;
58
+ while (t !== null && t.oe & REACTIVE_ZOMBIE) t = t._parent;
59
+ return t !== null && (t.C & CONFIG_LANE_FRAME) !== 0;
60
+ }
61
+
42
62
  let clock = 0;
43
63
 
44
64
  let activeTransition = null;
@@ -53,32 +73,51 @@ let syncDepth = 0;
53
73
 
54
74
  let projectionWriteActive = false;
55
75
 
56
- // Store property nodes that were created solely to carry a pending write (no
57
- // subscribers at write time). Swept after each flush that commits pending
58
- // values — any still without subs get disposed via their `_unobserved` hook,
59
- // releasing the slot in the parent store's node map.
76
+ /** > 0 while an action's generator body is on the stack (the synchronous
77
+ * slice between yields). Maintained by action.ts around `it.next()`. */ let actionStepDepth = 0;
78
+
79
+ function enterActionStep() {
80
+ actionStepDepth++;
81
+ }
82
+
83
+ function exitActionStep() {
84
+ actionStepDepth--;
85
+ }
86
+
87
+ // Store property nodes whose last subscriber left while they carried state
88
+ // the backing cannot reconstruct — an optimistic override (overrides live on
89
+ // nodes, over a clone the setter discards) or a staged write. Releasing the
90
+ // slot then would drop the override: an optimistic store key read `0` the
91
+ // moment its only reader gated away while the action was live (S7). Swept
92
+ // after each flush — a node still without subs whose override and staging
93
+ // have resolved is released through the slot hook; one that regained a
94
+ // subscriber leaves the set.
60
95
  const transientStoreNodes = new Set;
61
96
 
97
+ /** Slot hook's deferral: release this node when its carried state resolves. */ function deferSlotRelease(e) {
98
+ transientStoreNodes.add(e);
99
+ }
100
+
62
101
  function canUseSimpleSyncFlush(e) {
63
102
  const t = e.m;
64
- return transitions.size === 0 && activeLanes.size === 0 && e.vt.length === 0 && t.Me.length === 0 && t.A.length === 0 && t.dn.size === 0 && transientStoreNodes.size === 0;
103
+ return transitions.size === 0 && activeLanes.size === 0 && e.ft.length === 0 && t.cn.length === 0 && t.A.length === 0 && t.ri.size === 0 && transientStoreNodes.size === 0 && pendingRearms.size === 0;
65
104
  }
66
105
 
67
106
  function sweepTransientStoreNodes() {
68
107
  if (transientStoreNodes.size === 0) return;
69
108
  for (const e of transientStoreNodes) {
70
- if (e.o !== null) {
109
+ if (e.u !== null) {
71
110
  transientStoreNodes.delete(e);
72
111
  continue;
73
112
  }
74
113
  if (e._e !== NOT_PENDING) continue;
75
- if (e.De !== undefined && e.De !== NOT_PENDING) continue;
114
+ if (e.o?.Fe !== undefined && e.o?.Fe !== NOT_PENDING) continue;
76
115
  // A live affects() mark keeps the node addressable: sweeping it would
77
116
  // detach the refcount from the slot (a fresh probe would upsert a new,
78
117
  // unmarked node for the same property).
79
- if (e.t) continue;
118
+ if (e.o?.t) continue;
80
119
  transientStoreNodes.delete(e);
81
- e.ut?.();
120
+ if (e.C & CONFIG_SLOT_NODE) slotUnobservedHook(e); else e.o?.Hn?.();
82
121
  }
83
122
  }
84
123
 
@@ -105,32 +144,34 @@ function setProjectionWriteActive(e) {
105
144
  * land there directly — no per-field aliasing.
106
145
  */ function createBatch() {
107
146
  return {
108
- de: clock,
109
- Qt: [],
110
- Ie: new Map,
111
- Me: [],
147
+ be: clock,
148
+ Gn: [],
149
+ le: new Map,
150
+ cn: [],
112
151
  A: [],
113
- dn: new Set,
114
- ie: [],
115
- yt: {
116
- bt: [ [], [] ],
117
- vt: []
152
+ ri: new Set,
153
+ pe: [],
154
+ dt: {
155
+ Tt: [ [], [] ],
156
+ ft: []
118
157
  },
119
- fn: false,
120
- ln: new Set
158
+ Rn: false,
159
+ In: new Set,
160
+ Dn: null
121
161
  };
122
162
  }
123
163
 
124
164
  function mergeTransitionState(e, t) {
125
- t.fn = e;
126
- e.ie.push(...t.ie);
127
- for (const i of activeLanes) if (i.Ne === t) i.Ne = e;
128
- if (t.Me.length) {
165
+ t.Rn = e;
166
+ e.pe.push(...t.pe);
167
+ e.Te ||= t.Te;
168
+ for (const n of activeLanes) if (n.we === t) n.we = e;
169
+ if (t.cn.length) {
129
170
  // Move (don't copy): the global queue's batch may still be the outgoing
130
171
  // transition, and the adoption pass in initTransition would re-push its
131
172
  // contents into the target — duplicating every entry.
132
- e.Me.push(...t.Me);
133
- t.Me.length = 0;
173
+ e.cn.push(...t.cn);
174
+ t.cn.length = 0;
134
175
  }
135
176
  if (t.A.length) {
136
177
  // Move (don't copy): the global queue's batch may still be the outgoing
@@ -139,15 +180,120 @@ function mergeTransitionState(e, t) {
139
180
  e.A.push(...t.A);
140
181
  t.A.length = 0;
141
182
  }
142
- for (const i of t.dn) e.dn.add(i);
143
- for (const [i, n] of t.Ie) {
144
- let t = e.Ie.get(i);
145
- if (!t) e.Ie.set(i, t = new Set);
146
- for (const e of n) t.add(e);
183
+ for (const n of t.ri) e.ri.add(n);
184
+ for (const [n, i] of t.le) {
185
+ let t = e.le.get(n);
186
+ if (!t) e.le.set(n, t = new Set);
187
+ for (const e of i) t.add(e);
188
+ }
189
+ for (const n of t.In) e.In.add(n);
190
+ if (t.Dn) (e.Dn ??= []).push(...t.Dn);
191
+ }
192
+
193
+ /**
194
+ * Flip-entanglement (#3164 follow-up): `until()` is a declaration of
195
+ * relatedness — the predicate names the condition that confirms the awaiting
196
+ * transaction. When the predicate settles truthy, every live foreign
197
+ * transition whose staged write it read IS the confirming event by the
198
+ * user's own definition, so it merges into the awaiting transaction and
199
+ * reveals at the joint settle — the cross-primitive twin of the family fold
200
+ * (a landing on an optimism-carrying family joins the retaining
201
+ * transaction). Non-flipping updates never pass through here: falsy
202
+ * evaluations don't entangle, so unrelated traffic on the watched sources
203
+ * reveals freely on its own schedule.
204
+ *
205
+ * Runs inside the predicate's compute (pure phase) — the confirming
206
+ * transition's stamps are still live and its commit decision hasn't run, so
207
+ * the merge lands before any reveal. Only the tree-shaken graphs that call
208
+ * `until()` retain this.
209
+ */ function entangleConfirmingTransitions(e, t) {
210
+ t = currentTransition(t);
211
+ if (t.Rn === true) return;
212
+ // The confirming evidence is a dep whose value is STAGED (pending,
213
+ // uncommitted) at flip evaluation — committed deps are public already and
214
+ // carry nothing to entangle. A staged dep lives in one of two carriers: a
215
+ // stamped transition, or the queue's current batch (ambient registrations
216
+ // don't stamp; "ambient work IS a transaction" — the batch is the
217
+ // carrier). The entangle STEALS the carrier's staged cargo — its pending
218
+ // nodes move (re-stamped) into the awaiting transaction and reveal at its
219
+ // settle — but never the carrier itself: its async reporters, actions,
220
+ // and stashes are its own future (a live stream's flight must not chain
221
+ // the awaiting transaction to landings that haven't happened; a merged
222
+ // reporter deadlocked exactly that way).
223
+ let n = false;
224
+ for (let i = e.Ie; i !== null; i = i.Ne) {
225
+ const r = i.Oe;
226
+ if (r._e !== NOT_PENDING) {
227
+ const e = r.we;
228
+ const i = e != null ? currentTransition(e) : null;
229
+ // Skip the awaiting transaction's own cargo (t === target: a
230
+ // fold-staged landing or a write the action itself issued — hold and
231
+ // reveal already correct) and dead carriers. Ambient-batch staging
232
+ // (t === null) must leave the batch NOW — it commits at this flush's
233
+ // end, which would reveal the confirmation under the live optimism
234
+ // it just confirmed.
235
+ const s = i === null ? currentBatch.Gn : i !== t && i.Rn !== true ? i.Gn : null;
236
+ if (s !== null) n = stealEntangledCargo(s, t) || n;
237
+ }
238
+ if (i === e.Nn) break;
239
+ }
240
+ // The steal never activates the awaiting transaction: the predicate can
241
+ // flip inside another transaction's finalize heap, and adopting the queue
242
+ // batch there hands the stolen cargo to that finalize's commit sweep — a
243
+ // premature reveal at a foreign settle. Subscribers that computed against
244
+ // the pre-steal world were re-dirtied by the steal itself, so this
245
+ // flush's applies paint the mid-hold view (committed for lane and stale
246
+ // readers; a deriving reader is held with the cargo); the cargo commits
247
+ // at the awaiting transaction's own settle.
248
+ }
249
+
250
+ /** Move a confirming carrier's staged nodes into the awaiting transaction:
251
+ * re-stamp and arm the held-truth mask (override-covered nodes skip it —
252
+ * the override already hides their staged value per A17, and is usually
253
+ * the very optimism this confirmation settles); the mask's commit
254
+ * registers the settle-side post-revert wake. The carrier's array is
255
+ * emptied so its own commit point commits none of the stolen cargo.
256
+ *
257
+ * EFFECT subs of stolen nodes re-run: any that recomputed against the
258
+ * staging BEFORE the steal (the carrier's landing notified them as a plain
259
+ * write) hold a private result derived from the carrier's world — one
260
+ * that the next paint gate (stash-point lane run, a foreign flush's
261
+ * completion drain) would apply as the carrier's. Re-running them now that
262
+ * the cargo is the awaiting transaction's re-routes each by its posture in
263
+ * this same heap pass: a lane pass keeps committed (the mask), a stale
264
+ * render effect keeps committed and registers for the reveal's replay
265
+ * (stale-of-foreign), and a deriving user effect is served the truth and
266
+ * held with the transaction (A29). PURE computeds are deliberately NOT
267
+ * re-run: a staged value of theirs is itself stolen cargo — held with the
268
+ * transaction, so ordinary readers already serve their committed value —
269
+ * while re-running them would re-derive the OLD world and re-stage it over
270
+ * the held truth. The reveal re-notifies (commitPendingNodes), which is
271
+ * when they re-derive for real. */ function stealEntangledCargo(e, t) {
272
+ if (e === t.Gn || e.length === 0) return false;
273
+ for (let n = 0; n < e.length; n++) {
274
+ const i = e[n];
275
+ i.we = t;
276
+ t.Gn.push(i);
277
+ // Override-covered nodes stay silent AND unmasked: the override is the
278
+ // display (A17 — its staging never notified, its revert will), so their
279
+ // subs saw nothing and re-running one would break the silence with a
280
+ // duplicate fire of an unchanged view.
281
+ if (!hasActiveOverride(i)) {
282
+ i.C |= CONFIG_HELD_TRUTH;
283
+ for (let e = i.u; e !== null; e = e.Ae) {
284
+ const t = e.ge;
285
+ if (t.je && !(t.C & CONFIG_AUTHORITATIVE_READ)) enqueueSub(t);
286
+ }
287
+ }
147
288
  }
148
- for (const i of t.ln) e.ln.add(i);
289
+ e.length = 0;
290
+ transitions.add(t);
291
+ return true;
149
292
  }
150
293
 
294
+ /** `schedule()` armed `scheduled` but withheld the microtask because a
295
+ * projection draft was writing (see below). Consumed by `scheduleWithheld`. */ let withheld = false;
296
+
151
297
  function schedule() {
152
298
  if (halted) {
153
299
  notifyHalted();
@@ -155,21 +301,112 @@ function schedule() {
155
301
  }
156
302
  if (scheduled) return;
157
303
  scheduled = true;
158
- if (!syncDepth && !globalQueue.cn && !projectionWriteActive) queueMicrotask(flush);
304
+ if (!syncDepth && !globalQueue.Jn) {
305
+ // A projection draft's writes withhold the microtask: a flight's landing
306
+ // drains them (asyncWrite's flush), and a drain BEFORE the landing tears
307
+ // the pre- and post-await halves of the run apart (createProjection.async
308
+ // "notifies only changed paths"). A draft write that no landing will
309
+ // follow re-arms through scheduleWithheld (proj R37).
310
+ withheld = projectionWriteActive;
311
+ if (!withheld) queueMicrotask(flush);
312
+ }
313
+ }
314
+
315
+ /** Arm the microtask `schedule()` withheld under projectionWriteActive. The
316
+ * projection draft calls this after a write made outside its run with no
317
+ * flight up (proj R37): nothing else will drain, and leaving `scheduled`
318
+ * armed with no microtask strands the whole scheduler — every later
319
+ * `schedule()` early-returns — until something calls `flush()` by hand.
320
+ * (`withheld` was set in the same synchronous slice as this call, so the
321
+ * syncDepth/_running gates it passed still hold; a stale mark left by a
322
+ * landing's own flush arms at worst one no-op drain.) */ function scheduleWithheld() {
323
+ if (withheld) {
324
+ withheld = false;
325
+ queueMicrotask(flush);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Parked transactions whose reporter set changed without a write. A
331
+ * transaction completes when nothing live reports a flight it waits on, but
332
+ * the flush only judges the ACTIVE transaction: a parked one is re-entered by
333
+ * a stamped node's landing or an action's resume. A reporter that stops
334
+ * counting for another reason — its loading boundary flipped to the fallback
335
+ * (#3375), or it was disposed by ambient work (#3372) — is neither: the
336
+ * pruning in `reporterBlocksSource` would drop it at the next check, but no
337
+ * check comes, and the writes held with it stay staged. Such sites record the
338
+ * transaction here (deduped: one idle pass per transaction, however many
339
+ * reporters changed); the flush re-enters it on an otherwise idle pass, so
340
+ * the re-evaluation adopts no unrelated ambient work.
341
+ */ const wokenTransitions = [];
342
+
343
+ /** Wake every parked transaction — for a site that knows a reporter stopped
344
+ * counting but not whose (a boundary reset). */ function wakeParked() {
345
+ for (const e of transitions) wokenTransitions.includes(e) || wokenTransitions.push(e);
346
+ schedule();
347
+ }
348
+
349
+ /**
350
+ * Boundaries whose `on` dependencies notified this flush (#3540; a Set:
351
+ * many notifications, one re-arm). The notification arrives inside a pass,
352
+ * at the height of the `on` reads — before the readers the write put in
353
+ * flight are registered — so the re-arm waits for the heap and runs before
354
+ * the verdict (GlobalQueue.run → drainRearms), still under the write's
355
+ * transaction: the release is seen by the verdict that follows, and the
356
+ * staged fallback swap lands with the write's frame.
357
+ */ const pendingRearms = new Set;
358
+
359
+ function queueRearm(e) {
360
+ pendingRearms.add(e);
361
+ schedule();
159
362
  }
160
363
 
364
+ /** The drain (see pendingRearms): each boundary decides for itself, from
365
+ * what is pending under it now, whether re-arming means its fallback or
366
+ * nothing. Snapshot first: a re-arm can queue another — a write it makes
367
+ * notifying an `on` that reads it — which is the next pass's. */ function drainRearms() {
368
+ const e = Array.from(pendingRearms);
369
+ pendingRearms.clear();
370
+ for (let t = 0; t < e.length; t++) e[t].se();
371
+ }
372
+
373
+ /** Transactions a mainline tick has PROPOSED against (A34, #3494): a write to a
374
+ * node one of them holds — the same value or another — is a second proposal
375
+ * on a contested node, and the tick reveals with the hold ("both are
376
+ * suggesting a value; if one finished before the other that would be odd").
377
+ * Entered at the next flush's start, where the ambient batch is adopted;
378
+ * never from the write itself, which left `activeTransition` set across the
379
+ * caller's block and made creation after the write the transaction's (A29). */ const batchJoins = [];
380
+
161
381
  /**
162
382
  * Permanently halts the reactive system. Called when a user error escapes
163
383
  * every boundary — app state is undefined at that point, so scheduling stops
164
384
  * entirely rather than limping along with a half-applied update.
165
- */ function haltReactivity(e) {
385
+ */
386
+ /**
387
+ * The key a root owner carries its client error hook under (`render`'s
388
+ * `onError`) — registered, so a runtime writes it with no import of the hook
389
+ * module (core/error-hooks.ts) and no property mangling in the way. Defined
390
+ * HERE, not there: a runtime that only writes the key must not retain the
391
+ * hook machinery (pay-for-use).
392
+ */ const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
393
+
394
+ function haltReactivity(e) {
166
395
  if (halted) return;
167
396
  halted = true;
168
397
  let t = "[REACTIVITY_HALTED]";
169
- // Log the cause here too: callers rethrow it, but a creation-time throw
398
+ // Surface the cause here too: callers rethrow it, but a creation-time throw
170
399
  // unwinds through ancestor recomputes that convert it to status instead of
171
400
  // surfacing it (#2884), so the rethrow alone cannot guarantee visibility.
172
- e === undefined ? console.error(t) : console.error(t, e);
401
+ // Where the platform has one, hand the cause to its uncaught-error channel
402
+ // (`reportError` → `error` event → window.onerror / error monitoring): a
403
+ // halt that only reaches console.error leaves a page that LOOKS alive with
404
+ // nothing an app or its telemetry can act on (#3338 — an uncaught throw
405
+ // during the hydration render). The rethrow may reach the top as well in
406
+ // the non-swallowed cases; a duplicate report beats a silent one.
407
+ const n = e !== undefined && globalThis.reportError;
408
+ n || e === undefined ? console.error(t) : console.error(t, e);
409
+ n && n(e);
173
410
  }
174
411
 
175
412
  // Logs on the first write after a halt so a frozen interaction is traceable.
@@ -189,30 +426,30 @@ function notifyHalted() {
189
426
  let queueRunToken = 0;
190
427
 
191
428
  class Queue {
192
- Fe=null;
193
- bt=[ [], [] ];
194
- vt=[];
195
- kt=0;
429
+ _parent=null;
430
+ Tt=[ [], [] ];
431
+ ft=[];
432
+ ht=0;
196
433
  created=clock;
197
434
  addChild(e) {
198
- this.vt.push(e);
199
- e.Fe = this;
435
+ this.ft.push(e);
436
+ e._parent = this;
200
437
  }
201
438
  removeChild(e) {
202
- const t = this.vt.indexOf(e);
439
+ const t = this.ft.indexOf(e);
203
440
  if (t >= 0) {
204
- this.vt.splice(t, 1);
205
- e.Fe = null;
441
+ this.ft.splice(t, 1);
442
+ e._parent = null;
206
443
  }
207
444
  }
208
- notify(e, t, i, n) {
209
- if (this.Fe) return this.Fe.notify(e, t, i, n);
445
+ notify(e, t, n, i) {
446
+ if (this._parent) return this._parent.notify(e, t, n, i);
210
447
  return false;
211
448
  }
212
449
  run(e) {
213
- if (this.bt[e - 1].length) {
214
- const t = this.bt[e - 1];
215
- this.bt[e - 1] = [];
450
+ if (this.Tt[e - 1].length) {
451
+ const t = this.Tt[e - 1];
452
+ this.Tt[e - 1] = [];
216
453
  runQueue(t, e);
217
454
  }
218
455
  // Effects run here can dispose owners, and disposal removes queues from
@@ -222,70 +459,70 @@ class Queue {
222
459
  // can be recovered by rescanning from the front and every child still runs
223
460
  // exactly once. Children appended mid-pass carry a stale stamp and run,
224
461
  // matching the previous live-array behaviour.
225
- const t = this.vt;
226
- const i = ++queueRunToken;
227
- for (let n = 0; n < t.length; ) {
228
- const s = t[n];
229
- if (s.kt !== i) {
230
- s.kt = i;
231
- s.run?.(e);
232
- if (t[n] !== s) {
233
- n = 0;
462
+ const t = this.ft;
463
+ const n = ++queueRunToken;
464
+ for (let i = 0; i < t.length; ) {
465
+ const r = t[i];
466
+ if (r.ht !== n) {
467
+ r.ht = n;
468
+ r.run?.(e);
469
+ if (t[i] !== r) {
470
+ i = 0;
234
471
  continue;
235
472
  }
236
473
  }
237
- n++;
474
+ i++;
238
475
  }
239
476
  }
240
477
  enqueue(e, t) {
241
478
  if (e) {
242
479
  // Route to lane's effect queue if we're in an optimistic recomputation
243
480
  if (currentOptimisticLane) {
244
- const i = findLane(currentOptimisticLane);
245
- i.rn[e - 1].push(t);
481
+ const n = findLane(currentOptimisticLane);
482
+ n.dn[e - 1].push(t);
246
483
  } else {
247
- this.bt[e - 1].push(t);
484
+ this.Tt[e - 1].push(t);
248
485
  }
249
486
  }
250
487
  schedule();
251
488
  }
252
489
  stashQueues(e) {
253
- e.bt[0].push(...this.bt[0]);
254
- e.bt[1].push(...this.bt[1]);
255
- this.bt = [ [], [] ];
256
- for (let t = 0; t < this.vt.length; t++) {
257
- let i = this.vt[t];
258
- let n = e.vt[t];
259
- if (!n) {
260
- n = {
261
- bt: [ [], [] ],
262
- vt: []
490
+ e.Tt[0].push(...this.Tt[0]);
491
+ e.Tt[1].push(...this.Tt[1]);
492
+ this.Tt = [ [], [] ];
493
+ for (let t = 0; t < this.ft.length; t++) {
494
+ let n = this.ft[t];
495
+ let i = e.ft[t];
496
+ if (!i) {
497
+ i = {
498
+ Tt: [ [], [] ],
499
+ ft: []
263
500
  };
264
- e.vt[t] = n;
501
+ e.ft[t] = i;
265
502
  }
266
- i.stashQueues(n);
503
+ n.stashQueues(i);
267
504
  }
268
505
  }
269
506
  restoreQueues(e) {
270
- this.bt[0].push(...e.bt[0]);
271
- this.bt[1].push(...e.bt[1]);
272
- for (let t = 0; t < e.vt.length; t++) {
273
- const i = e.vt[t];
274
- let n = this.vt[t];
275
- if (n) n.restoreQueues(i);
507
+ this.Tt[0].push(...e.Tt[0]);
508
+ this.Tt[1].push(...e.Tt[1]);
509
+ for (let t = 0; t < e.ft.length; t++) {
510
+ const n = e.ft[t];
511
+ let i = this.ft[t];
512
+ if (i) i.restoreQueues(n);
276
513
  }
277
514
  }
278
515
  }
279
516
 
280
517
  class GlobalQueue extends Queue {
281
- cn=false;
518
+ Jn=false;
282
519
  // The current transaction-shaped batch: a plain ambient batch while no
283
520
  // transition is active, the active transition itself after initTransition.
284
521
  m=createBatch();
285
- static Ce;
286
- static me;
287
- static et;
288
- static gt=null;
522
+ static Ke;
523
+ static en;
524
+ static Cn;
525
+ static _t=null;
289
526
  // Store-side hook: drops a keyless affects() mark's identity scope when the
290
527
  // carrier node's last registration releases (wired by store.ts, mirroring
291
528
  // _clearOptimisticStore).
@@ -296,27 +533,30 @@ class GlobalQueue extends Queue {
296
533
  // once the gate holds.
297
534
  static G=null;
298
535
  static M=null;
299
- static h=null;
536
+ static N=null;
300
537
  // External-source bridge (wired by enableExternalSource(); null while no
301
538
  // config is active — including after _resetExternalSourceConfig()).
302
- static dt=null;
303
- static St=null;
539
+ static Zn=null;
540
+ static jn=null;
304
541
  // Verdict-layer hooks (wired by verdict.ts when isPending()/latest() are
305
542
  // imported; null in apps that never use them). Call sites either guard for
306
543
  // null or sit behind state only the verdict layer can create (`!` is safe
307
544
  // there: `_pendingSignal`/`_latestValueComputed` are only ever assigned by
308
545
  // verdict.ts, and `pendingCheckActive`/`latestReadActive` only flip inside
309
546
  // isPending()/latest()).
310
- static Pe=null;
311
- static Se=null;
547
+ static Ve=null;
312
548
  static Ge=null;
313
- static un=null;
314
- static At=null;
315
- static Ct=null;
316
- static Pt=null;
317
- static $e=null;
549
+ static We=null;
550
+ static ti=null;
551
+ static et=null;
552
+ static nt=null;
553
+ static rt=null;
554
+ static On=null;
318
555
  static k=null;
319
- static Lt=null;
556
+ static Et=null;
557
+ // Re-asks probes whose verdict was provisionally suppressed by a fresh read
558
+ // of a held value, once the transaction gains an async blocker (#3028).
559
+ static Nt=null;
320
560
  // Optimistic-engine hooks (wired by core/optimistic.ts via
321
561
  // installOptimisticEngine(), called from verdict.ts / createOptimistic /
322
562
  // createOptimisticStore — every module that can create optimistic state).
@@ -324,28 +564,145 @@ class GlobalQueue extends Queue {
324
564
  // `_overrideValue` slot, a lane in `activeLanes`, an `_optimisticNodes`
325
565
  // entry, or a non-null `currentOptimisticLane`, so `!` invocations are safe
326
566
  // once the gate holds.
327
- static Gt=null;
328
- static En=null;
567
+ static ot=null;
568
+ static oi=null;
569
+ static ai=null;
570
+ static li=null;
571
+ static si=null;
572
+ /** Patch-channel optimistic drain (next/patch.ts): optimistic emissions
573
+ * apply at lane-effect timing — visible in flight, unlike the regular
574
+ * effect queues an action stashes. Injected; null when unused. */
575
+ static ui=null;
576
+ static lt=null;
577
+ static it=null;
578
+ /** Is the node routed through a LIVE lane (`resolveLane`)? read()'s reveal
579
+ * carve-out asks before showing a foreign-held pending node's committed
580
+ * value: a lane-derived flight's inputs are already revealed through the
581
+ * lane (#3334). Gated on CONFIG_HAS_LANE, which only the engine sets. */
582
+ static tt=null;
583
+ static $n=null;
584
+ static an=null;
585
+ static Sn=null;
586
+ /** Authoritative-view reader wakeup: installed by until() and refresh() before
587
+ * their first read. Call sites are gated by CONFIG_AUTHORITATIVE_OBSERVED, which
588
+ * only such a reader's carve-out read can set, so `!` invocations are safe once
589
+ * the gate holds (#3303). */
590
+ static zn=null;
329
591
  static Tn=null;
330
- static In=null;
331
- static Nn=null;
332
- static Rt=null;
333
- static Ot=null;
334
- static Dt=null;
335
- static je=null;
336
- static ze=null;
337
- static Be=null;
338
- static Qn=null;
592
+ /** A18 supersession (#3331): own-source truth `value` landed under an active
593
+ * override. The engine decides whether the graph re-derives — the value
594
+ * differs from the override and is not a stale (older-action) answer (mark
595
+ * the node, demote its lane cascade, notify), or returns to it after an
596
+ * earlier differing arrival (clear the mark, notify) — and owns the
597
+ * authoritative-observer wake for a silent confirm. Installed with the
598
+ * optimistic engine; only reachable on a node that has an override. */
599
+ static ke=null;
600
+ /** The flush's pre-verdict step (#3427): once the transaction's action
601
+ * bodies have all ended and nothing authoritative is left in flight, the
602
+ * engine supersedes every override still in force with the truth it
603
+ * reverts to, so the graph re-derives from it now, as the transaction's
604
+ * held work, instead of after the flights the overrides fed have landed.
605
+ * True when it superseded something: the caller re-runs the heap ahead of
606
+ * the verdict. The engine owns every gate (acted, actions drained, has
607
+ * overrides, no store edits, no authoritative flight); null without it. */
608
+ static Ei=null;
609
+ /** read()'s value for a TRACKED reader of a superseded node (#3331): the
610
+ * staged truth, unless the reader is a stale (render) reader of another
611
+ * transaction — then the displayed override, as it keeps a foreign
612
+ * transaction's committed value over its staged write. */
613
+ /** A tracked read of an active override: the lane outside-view rule
614
+ * (#3460) and the A18 supersession selection (#3331) — see optimistic.ts. */
615
+ static ut=null;
616
+ /** A lane pass's publish for a memo (#3479, lanes stage): the speculative
617
+ * result becomes a DERIVED override, `_value` stays committed — see
618
+ * optimistic.ts laneOverride. Set with the engine, which a lane implies. */
619
+ static Me=null;
620
+ /** Verdict-layer recompute in progress (companion creation, latest()/
621
+ * isPending() pulls): never born held — see core.ts enterStagedRead. */
622
+ static Xn=false;
623
+ /** setSignal's authoritative (projection-write) landing on an override-
624
+ * covered node (#3331 store twin): stage the truth for its transaction's
625
+ * commit whatever its relation to the committed value — a landing equal to
626
+ * committed still differs from the override — then _supersedeOverride
627
+ * decides. Installed with the optimistic engine; only reachable on a node
628
+ * that has an override. */
629
+ static st=null;
630
+ static fi=null;
339
631
  flush() {
340
- if (this.cn) return;
341
- this.cn = true;
342
- try {
632
+ if (this.Jn) return;
633
+ // Fast drain: nothing in flight but plain pending commits — no dirty
634
+ // computeds, no queued effects, no child queues, no transitions/lanes/
635
+ // optimistic state. Commit and go; anything a commit hook schedules
636
+ // (companion snaps, store folds notifying subs) re-arms `scheduled`
637
+ // below and the outer drain loop takes the full spine next round.
638
+ if (activeTransition === null && dirtyQueue.EE < dirtyQueue.ln && this.Tt[0].length === 0 && this.Tt[1].length === 0 && this.ft.length === 0 && !wokenTransitions.length && !batchJoins.length && // a join must drain in its own tick (#3519 review)
639
+ canUseSimpleSyncFlush(this)) {
640
+ this.Jn = true;
641
+ try {
642
+ // A28: companions of nodes written since the last flush mirror the
643
+ // flushed world — re-synced inside the running window (the write is
644
+ // "flushed" from here on).
645
+ resyncUnflushedCompanions();
646
+ // Sweep first: unobserved() pulls swept nodes out of the dirty heap,
647
+ // so a dormant memo dirtied in the same tick is reclaimed instead of
648
+ // recomputed (matching the old inline dispose-on-read counts).
649
+ sweepDormant();
650
+ commitPendingNodes();
651
+ } finally {
652
+ this.Jn = false;
653
+ }
654
+ clock++;
655
+ scheduled = dirtyQueue.EE >= dirtyQueue.ln || this.Tt[0].length !== 0 || this.Tt[1].length !== 0 || this.m.Gn.length !== 0;
656
+ return;
657
+ }
658
+ this.Jn = true;
659
+ resyncUnflushedCompanions();
660
+ // A28, see above
661
+ try {
662
+ // The tick proposed against a hold (#3494): adopt its batch into it.
663
+ // Inside the try: the adoption runs user comparators (the no-proposal
664
+ // drop), and a throw there must not leave `_running` set.
665
+ while (batchJoins.length) this.initTransition(batchJoins.pop());
343
666
  if (false) ;
344
- runHeap(dirtyQueue, GlobalQueue.Ce);
667
+ // Before runHeap for the same reason as the fast drain above; late
668
+ // subscribers (an effect reading a swept memo this flush) revive it,
669
+ // which is the pay-for-use contract.
670
+ sweepDormant();
671
+ runHeap(dirtyQueue, GlobalQueue.Ke);
672
+ // The action bodies are over: the overrides they leave in force
673
+ // revert at this settle, and the correction is this transaction's
674
+ // held work — re-derived here, under it, ahead of the verdict — not a
675
+ // waterfall after the flights the overrides fed (#3427). After the
676
+ // heap, not before: a synchronous body's own writes (a refresh that
677
+ // puts an override node's source in flight) are judged applied.
678
+ if (activeTransition && GlobalQueue.Ei?.(activeTransition)) runHeap(dirtyQueue, GlobalQueue.Ke);
679
+ // Re-arm the boundaries whose `on` notified this pass (pendingRearms):
680
+ // after the heap — what the notification put in flight is registered —
681
+ // and before the verdict, under the transaction that carried it. The
682
+ // boundary releases its hold and stages its fallback swap with the
683
+ // frame; the heap re-runs so its output pass is staged too, ahead of
684
+ // the verdict that commits or parks it (#3540).
685
+ if (pendingRearms.size) {
686
+ drainRearms();
687
+ runHeap(dirtyQueue, GlobalQueue.Ke);
688
+ }
345
689
  if (activeTransition) {
690
+ // A boundary whose fallback read something not ready is judged before
691
+ // the verdict, under the transaction (boundaries.ts `_judgeHeld`,
692
+ // #3540): its output is pending on that read and holds the frame,
693
+ // and only a sweep re-runs it — the commit sweep, after the verdict
694
+ // its own read keeps parking. Ready, it stages `_disabled` false with
695
+ // the frame; the heap re-runs so the output drops the read ahead of
696
+ // the verdict.
697
+ if (this.ft.length) {
698
+ checkBoundaryChildren(this, true);
699
+ if (dirtyQueue.EE >= dirtyQueue.ln) runHeap(dirtyQueue, GlobalQueue.Ke);
700
+ }
346
701
  const e = transitionComplete(activeTransition);
347
702
  if (!e) {
348
703
  const e = activeTransition;
704
+ // Parked: the unchanged passes' inputs are held; their tails stay (A30).
705
+ heldTrims.length = 0;
349
706
  // When the parking batch IS the transition, all of its writes commit
350
707
  // only with it — every zombie recompute they queued would run against
351
708
  // a world the zombie never displays (zombies render mainline until
@@ -354,7 +711,7 @@ class GlobalQueue extends Queue {
354
711
  // zombies here. Height-adjust entries still process normally: a
355
712
  // dirtied one keeps its height flag and falls through to runHeap's
356
713
  // adjustHeight path on the next pass of the bucket.
357
- runHeap(zombieQueue, this.m === e ? cancelZombieRecompute : GlobalQueue.Ce);
714
+ runHeap(zombieQueue, this.m === e ? cancelZombieRecompute : GlobalQueue.Ke);
358
715
  // Detach: the stashed transition keeps its batch; ambient work that
359
716
  // follows lands in a fresh one. If the batch is already a separate
360
717
  // ambient one — action done() restored activeTransition without
@@ -364,83 +721,126 @@ class GlobalQueue extends Queue {
364
721
  if (this.m === e) currentBatch = this.m = createBatch();
365
722
  // Run lane effects immediately (before stashing) - lanes with no pending async
366
723
  if (activeLanes.size) {
367
- GlobalQueue.Nn(EFFECT_RENDER);
368
- GlobalQueue.Nn(EFFECT_USER);
724
+ GlobalQueue.si(EFFECT_RENDER);
725
+ GlobalQueue.si(EFFECT_USER);
369
726
  }
370
- this.stashQueues(e.yt);
727
+ this.stashQueues(e.dt);
371
728
  clock++;
372
729
  // A kept ambient batch may hold pending nodes (#2916): stay
373
730
  // scheduled so the outer drain loop commits them via the plain
374
731
  // flush path instead of leaving them until the next natural flush.
375
- scheduled = dirtyQueue.EE >= dirtyQueue.Ve || this.m.Qt.length > 0;
376
- reassignPendingTransition(e.Qt);
732
+ scheduled = dirtyQueue.EE >= dirtyQueue.ln || this.m.Gn.length > 0;
733
+ reassignPendingTransition(e.Gn);
377
734
  activeTransition = null;
378
735
  finalizePureQueue(null, true);
379
736
  return;
380
737
  }
381
738
  const t = activeTransition;
382
- const i = this.m;
383
- i !== t && i.Qt.push(...t.Qt);
384
- this.restoreQueues(t.yt);
739
+ const n = this.m;
740
+ n !== t && n.Gn.push(...t.Gn);
741
+ this.restoreQueues(t.dt);
385
742
  transitions.delete(t);
386
743
  activeTransition = null;
387
- reassignPendingTransition(i.Qt);
744
+ reassignPendingTransition(n.Gn);
388
745
  finalizePureQueue(t);
389
- if (i === t) {
746
+ if (n === t) {
390
747
  // Drop the dead Transition wrapper but keep its (drained) containers
391
748
  // as the ambient batch — late registrations during finalization live
392
749
  // there and must survive to the next flush.
393
750
  const e = createBatch();
394
- e.Qt = i.Qt;
395
- e.Me = i.Me;
396
- e.A = i.A;
397
- e.dn = i.dn;
751
+ e.Gn = n.Gn;
752
+ e.cn = n.cn;
753
+ e.A = n.A;
754
+ e.ri = n.ri;
398
755
  currentBatch = this.m = e;
399
756
  }
400
757
  } else {
401
758
  if (canUseSimpleSyncFlush(this)) {
402
759
  commitPendingNodes();
403
- if (dirtyQueue.EE >= dirtyQueue.Ve) {
404
- runHeap(dirtyQueue, GlobalQueue.Ce);
760
+ if (dirtyQueue.EE >= dirtyQueue.ln) {
761
+ runHeap(dirtyQueue, GlobalQueue.Ke);
405
762
  commitPendingNodes();
406
763
  }
407
764
  } else {
408
- if (transitions.size) runHeap(zombieQueue, GlobalQueue.Ce);
765
+ // Parked transactions elsewhere: their owners' zombies render
766
+ // mainline until the commit that disposes them, so a mainline write
767
+ // reaches them here (#2916, #3463). Commit THIS flush's pending nodes
768
+ // first (#3546): a zombie whose owner commits now is disposed by that
769
+ // commit and never reruns — the same fate it has when no transaction
770
+ // is parked, where this queue is not run at all. Run before the
771
+ // commit, it reran and notified its owner through the previous
772
+ // pass's dependency tail (kept by A30 until the commit trims it),
773
+ // and the owner recomputed a second time with identical inputs,
774
+ // creating and disposing one more child per write. Only the zombies
775
+ // of actually parked owners survive the commit and rerun; the
776
+ // finalize's own commit picks up whatever those reruns stage.
777
+ if (transitions.size) {
778
+ commitPendingNodes();
779
+ runHeap(zombieQueue, GlobalQueue.Ke);
780
+ }
409
781
  finalizePureQueue();
410
782
  }
411
783
  }
412
784
  clock++;
413
- // Check if finalization added items to the heap (from optimistic reversion)
414
- scheduled = dirtyQueue.EE >= dirtyQueue.Ve;
785
+ // Check if finalization added items to the heap (from optimistic reversion).
786
+ // Finalization may also have ENTERED a transaction (a commit hook, boundary
787
+ // sweep or recompute wrote a node it owns): effects computed under it
788
+ // since are its to apply, not this flush's — runEffect leaves them queued
789
+ // and the next pass parks them with it (#3319). Everything computed
790
+ // mainline applies now.
791
+ scheduled = dirtyQueue.EE >= dirtyQueue.ln || activeTransition !== null;
415
792
  // Run lane effects first (for ready lanes), then regular effects
416
- activeLanes.size && GlobalQueue.Nn(EFFECT_RENDER);
793
+ activeLanes.size && GlobalQueue.si(EFFECT_RENDER);
417
794
  this.run(EFFECT_RENDER);
418
- activeLanes.size && GlobalQueue.Nn(EFFECT_USER);
795
+ activeLanes.size && GlobalQueue.si(EFFECT_USER);
419
796
  this.run(EFFECT_USER);
420
797
  if (false) ;
421
798
  if (false && !scheduled && !activeTransition && transitions.size === 0 && activeLanes.size === 0) ;
422
799
  if (false) ;
423
800
  } finally {
424
- this.cn = false;
801
+ // Re-enter a woken transaction (see wokenTransitions) only from an
802
+ // idle pass: entering adopts the ambient batch, and staged or dirty
803
+ // ambient work would be held behind flights it never read. `scheduled`
804
+ // is that test here — after the park exit as well as the normal one:
805
+ // it was recomputed from the heap this pass, every write since re-armed
806
+ // it, and optimistic ambient nodes reverted with the finalize — so a
807
+ // wake in a pass with work simply falls to the next. Entering re-arms
808
+ // it itself; a dead (completed) wake is a bare return in
809
+ // initTransition, and the loop moves on to the next.
810
+ while (!scheduled && !activeTransition && wokenTransitions.length) this.initTransition(wokenTransitions.pop());
811
+ this.Jn = false;
425
812
  }
426
813
  }
427
- notify(e, t, i, n) {
814
+ notify(e, t, n, i) {
428
815
  // Only track async if the boundary is propagating STATUS_PENDING (not caught by boundary)
429
816
  if (t & STATUS_PENDING) {
430
- if (i & STATUS_PENDING) {
431
- const t = n !== undefined ? n : e._;
817
+ if (n & STATUS_PENDING) {
818
+ // Callers pass either nothing or this node's own `_x._error`, so `??`
819
+ // is exact (a null error falls back to the same null).
820
+ const t = i ?? e.o?._;
432
821
  // A visibility-only mark notification (the affects() boundary
433
822
  // channel) updates display state on its way up but must be invisible
434
823
  // to completion accounting BY CONSTRUCTION: it never registers a
435
824
  // reporter and never counts toward the loading-boundary diagnostic.
436
825
  if (t?.l) return true;
437
- if (activeTransition && t) {
438
- const i = t.source;
439
- let n = activeTransition.Ie.get(i);
440
- if (!n) activeTransition.Ie.set(i, n = new Set);
441
- const s = n.size;
442
- n.add(e);
443
- if (n.size !== s) schedule();
826
+ if (t) {
827
+ // A reveal can discover a flight started in an earlier flush. Hold
828
+ // the staged writes with that reader (A15), even if the reader is
829
+ // new. Fresh/reset loading boundaries consume pending before it
830
+ // reaches here. A reader already parked in a transition must not
831
+ // open a second one.
832
+ if (!activeTransition && !e.we && currentBatch.Gn.length) this.initTransition();
833
+ if (activeTransition) {
834
+ const n = t.source;
835
+ let i = activeTransition.le.get(n);
836
+ if (!i) activeTransition.le.set(n, i = new Set);
837
+ const r = i.size;
838
+ i.add(e);
839
+ if (i.size !== r) {
840
+ schedule();
841
+ GlobalQueue.Nt?.(activeTransition);
842
+ }
843
+ }
444
844
  }
445
845
  }
446
846
  return true;
@@ -448,19 +848,33 @@ class GlobalQueue extends Queue {
448
848
  return false;
449
849
  }
450
850
  initTransition(e) {
451
- if (e) e = currentTransition(e);
452
- if (e && e === activeTransition) return;
453
- if (!e && activeTransition && activeTransition.de === clock) return;
851
+ if (e) {
852
+ e = currentTransition(e);
853
+ // A finished transaction cannot be re-entered: its state is committed
854
+ // or reverted, so "rejoining" it (A26) is meaningless and re-activating
855
+ // it spins the drain loop (#3140). The refusal must be a bare return —
856
+ // redirecting the caller to a fresh batch would re-arm the loop with a
857
+ // new transaction identity each pass. Stamps are cleared at commit, so
858
+ // this is a belt for paths that hand over a chased-dead reference
859
+ // (merged chains, async settles racing completion).
860
+ if (e.Rn === true || e === activeTransition) return;
861
+ }
862
+ if (!e && activeTransition && activeTransition.be === clock) return;
454
863
  if (!activeTransition) {
455
864
  activeTransition = e ?? createBatch();
456
865
  } else if (e) {
457
866
  const t = activeTransition;
458
867
  mergeTransitionState(e, t);
868
+ // Effects the outgoing transaction parked belong to the surviving one
869
+ // now: back onto the live queue, where this flush parks them under
870
+ // `transition` or runs them at its completion. The outgoing stash is
871
+ // never read again — the transaction is dead (#3310).
872
+ this.restoreQueues(t.dt);
459
873
  transitions.delete(t);
460
874
  activeTransition = e;
461
875
  }
462
876
  transitions.add(activeTransition);
463
- activeTransition.de = clock;
877
+ activeTransition.be = clock;
464
878
  const t = this.m;
465
879
  if (t !== activeTransition) {
466
880
  // Adopt the ambient batch into the transaction, then make the
@@ -470,34 +884,74 @@ class GlobalQueue extends Queue {
470
884
  // must not entangle unrelated writes to it; the same rule holds one hop
471
885
  // downstream: propagation never queues pended subscribers as pending
472
886
  // nodes, see propagateAffectsMark, #2893.
473
- for (let e = 0; e < t.Qt.length; e++) {
474
- const i = t.Qt[e];
475
- i.Ne = activeTransition;
476
- activeTransition.Qt.push(i);
887
+ // Adopted outside a flush: the staging is still unflushed — the stamp
888
+ // must not make it read as held-and-carried (CONFIG_ADOPTED_UNFLUSHED;
889
+ // the carrying flush clears it in reassignPendingTransition).
890
+ const e = this.Jn ? 0 : CONFIG_ADOPTED_UNFLUSHED;
891
+ for (let n = 0; n < t.Gn.length; n++) {
892
+ const i = t.Gn[n];
893
+ // A tick that nets to the committed value proposed nothing (A34, #3494):
894
+ // `setShow(false); setShow(true)` beside a write that opens a hold
895
+ // left `show` staged at its own value, stamped, pending to the
896
+ // verdict, and its next mainline write held by a flight it never
897
+ // derived from. Unstage it here — its subscribers were walked at the
898
+ // write and re-derive the same value. Writes only: a signal's staging
899
+ // is always one, a computed's only under REACTIVE_MANUAL_WRITE
900
+ // (`createSignal(fn)`'s setter, #3519 review) — otherwise it is its
901
+ // pass's result, which may equal an uninitialized `undefined` (a
902
+ // born-held first pass). `_equals: false` opts out. The unstaging is
903
+ // the commit's own path (commitPendingNode with nothing staged): the
904
+ // manual-write flag, companions and the rest are cleaned up as a
905
+ // commit would, and the node is stamped nowhere. Unstamped
906
+ // only: a node already a transaction's — arriving here as a parked
907
+ // batch folds into a merge — carries a FLUSHED proposal a later
908
+ // rewrite brought back to the committed value; it is held, not
909
+ // proposal-free. Dropped, it kept the dead stamp, and the next write
910
+ // to it queued under the merged transaction a value the commit then
911
+ // skipped as another's (fuzzer latest-2 #1470, S3).
912
+ if (i.we === null && i._e !== NOT_PENDING && (!i.he || i.oe & REACTIVE_MANUAL_WRITE && !(i.h & STATUS_UNINITIALIZED)) && i.xe && i.xe(i.ce, i._e)) {
913
+ i._e = NOT_PENDING;
914
+ commitPendingNode(i);
915
+ continue;
916
+ }
917
+ i.we = activeTransition;
918
+ i.C |= e;
919
+ activeTransition.Gn.push(i);
477
920
  }
478
- for (let e = 0; e < t.Me.length; e++) {
479
- const i = t.Me[e];
480
- i.Ne = activeTransition;
481
- activeTransition.Me.push(i);
921
+ for (let e = 0; e < t.cn.length; e++) {
922
+ const n = t.cn[e];
923
+ n.we = activeTransition;
924
+ activeTransition.cn.push(n);
482
925
  }
483
926
  if (t.A.length) activeTransition.A.push(...t.A);
484
- for (const e of t.dn) activeTransition.dn.add(e);
927
+ for (const e of t.ri) activeTransition.ri.add(e);
485
928
  // Gated readers recorded against the ambient batch move with it: their
486
929
  // replay-at-commit now happens at the transaction's completion.
487
- if (t.ln.size) {
488
- for (const e of t.ln) activeTransition.ln.add(e);
489
- t.ln.clear();
930
+ if (t.In.size) {
931
+ for (const e of t.In) activeTransition.In.add(e);
932
+ t.In.clear();
490
933
  }
491
934
  currentBatch = this.m = activeTransition;
492
935
  }
493
936
  for (const e of activeLanes) {
494
- if (!e.Ne) e.Ne = activeTransition;
937
+ if (!e.we) e.we = activeTransition;
495
938
  }
939
+ // A transaction's ambient window is one flush. Entering must therefore
940
+ // guarantee a flush: a transaction opened with no writes (an action whose
941
+ // first statements only await) otherwise leaves activeTransition and the
942
+ // adopted batch armed across the async gap, and the next unrelated work
943
+ // to arrive — an optimistic store's authoritative landing, a plain async
944
+ // settle — is adopted into a transaction it has nothing to do with
945
+ // (#3141). The scheduled flush parks the incomplete transaction through
946
+ // the normal machinery and detaches the ambient slots first.
947
+ schedule();
496
948
  }
497
949
  }
498
950
 
499
951
  function queuePendingNode(e) {
500
- currentBatch.Qt.push(e);
952
+ currentBatch.Gn.push(e);
953
+ if (!globalQueue.Jn) markUnflushedStaged();
954
+ // A28
501
955
  }
502
956
 
503
957
  // Sticky: flips true on the first refresh() ever (the only setter of
@@ -505,60 +959,129 @@ function queuePendingNode(e) {
505
959
  // clear entirely in apps that never refresh.
506
960
  let reaskArmed = false;
507
961
 
962
+ /** §12d: bumped by every recompute and every new subscriber edge. A node's
963
+ * staged-rewrite skip is sound only while NOTHING recomputed or linked since
964
+ * its last notify — a mid-batch pull can clean a marked subscriber, and a
965
+ * skipped re-write would leave it stale. */ let notifyEpoch = 0;
966
+
967
+ function bumpNotifyEpoch() {
968
+ notifyEpoch++;
969
+ }
970
+
508
971
  function armReaskClear() {
509
972
  reaskArmed = true;
510
973
  }
511
974
 
975
+ /** Provenance of the work currently running (A18 supersession, #3331): the
976
+ * invocation sequence of the action whose ambient window this is — set by
977
+ * action() for each slice; the flush that ends the window clears it — or,
978
+ * inside an async landing, the sequence captured when that flight was
979
+ * registered (asyncWrite sets it for the landing's synchronous propagation,
980
+ * so a sync recompute downstream of the landing — an optimistic wrapper over
981
+ * the async source — derives under the flight's provenance, and flights it
982
+ * registers inherit it). 0 is mainline: no action, always the current
983
+ * question. An override stamps this at its write (`_overrideStamp`); an
984
+ * answer whose flight an OLDER action issued is a stale question the user
985
+ * has since changed — it holds silently to commit instead of superseding. A
986
+ * slow source must not leak back in over a newer intent. Transactions merge,
987
+ * so the transition object cannot say WHICH action asked; this can. */ let origin = 0;
988
+
989
+ function setOrigin(e) {
990
+ const t = origin;
991
+ origin = e;
992
+ return t;
993
+ }
994
+
512
995
  function insertSubs(e, t = false) {
996
+ // §12d: stamp before walking — setSignal's staged-rewrite fast path skips
997
+ // the next walk for this node while the epoch holds (marking is idempotent).
998
+ e.vn = notifyEpoch;
513
999
  // Get source lane: prefer node's own lane over current context
514
1000
  // This is important for isPending signals which need their own lane to flush immediately
515
- const i = e.Ke || currentOptimisticLane;
516
- const n = e.xe !== undefined;
1001
+ // Presence bits gate the optional-slot probes (see constants.ts): one
1002
+ // masked read of the always-present _config instead of missing-property
1003
+ // lookups in the hottest notify loop. Bits are sticky — the field read
1004
+ // stays authoritative when a bit is set.
1005
+ const n = e.C;
1006
+ const i = (n & CONFIG_HAS_LANE ? e.o?.me : undefined) || currentOptimisticLane;
1007
+ const r = (n & CONFIG_HAS_SNAPSHOT) !== 0 && e.o?.sn !== undefined;
517
1008
  const s = reaskArmed;
518
- for (let r = e.o; r !== null; r = r.ue) {
1009
+ for (let n = e.u; n !== null; n = n.Ae) {
1010
+ const e = n.ge;
519
1011
  // A value-change notification is a new question for the subscriber: any
520
1012
  // pending re-ask mark (refresh) it carried is superseded.
521
- if (s) r.fe.se &= ~REACTIVE_REASK;
522
- if (n && r.fe.T & CONFIG_IN_SNAPSHOT_SCOPE) {
523
- r.fe.se |= REACTIVE_SNAPSHOT_STALE;
1013
+ if (s) e.oe &= ~REACTIVE_REASK;
1014
+ // Missed-wake latch (#3037): this write is landing while the subscriber
1015
+ // is mid-recompute (a nested pull committing beneath its reads), and the
1016
+ // heap refuses RECOMPUTING nodes. A gen-current link means the pass
1017
+ // already validated this dep — the value it read is now stale — so latch
1018
+ // for recompute's tail to reschedule. Untouched links need no latch (the
1019
+ // pass either re-reads them fresh or trims them), and neither does the
1020
+ // tail link: it is the read IN FLIGHT — read() links before it pulls, so
1021
+ // this very commit is what that read returns.
1022
+ if (e.oe & REACTIVE_RECOMPUTING_DEPS && n.Be === e.Ye && n !== e.Nn) e.oe |= REACTIVE_MISSED_WAKE;
1023
+ if (r && e.C & CONFIG_IN_SNAPSHOT_SCOPE) {
1024
+ e.oe |= REACTIVE_SNAPSHOT_STALE;
524
1025
  continue;
525
1026
  }
526
1027
  if (t && i) {
527
- r.fe.se |= REACTIVE_OPTIMISTIC_DIRTY;
528
- assignOrMergeLane(r.fe, i);
1028
+ e.oe |= REACTIVE_OPTIMISTIC_DIRTY;
1029
+ assignOrMergeLane(e, i);
529
1030
  } else if (t) {
530
- r.fe.se |= REACTIVE_OPTIMISTIC_DIRTY;
1031
+ e.oe |= REACTIVE_OPTIMISTIC_DIRTY;
531
1032
  // No source lane means reversion - clear subscriber's lane so effects go to regular queue
532
- r.fe.Ke = undefined;
1033
+ if (e.o) e.o.me = undefined;
533
1034
  }
534
- enqueueSub(r.fe);
1035
+ enqueueSub(e);
535
1036
  }
536
1037
  }
537
1038
 
538
1039
  function commitPendingNode(e) {
539
1040
  const t = e;
540
- if (!t.ce) {
1041
+ if (!t.he) {
541
1042
  if (e._e !== NOT_PENDING) {
542
- e.be = e._e;
1043
+ e.ce = e._e;
543
1044
  e._e = NOT_PENDING;
544
1045
  }
545
- if (e.Oe || e.ge) GlobalQueue.un(e);
1046
+ if (e.C & CONFIG_HAS_COMPANIONS) GlobalQueue.ti(e);
546
1047
  return;
547
1048
  }
548
1049
  if (e._e !== NOT_PENDING) {
549
- e.be = e._e;
1050
+ e.ce = e._e;
550
1051
  e._e = NOT_PENDING;
1052
+ // A node born held (recompute) initializes at this commit.
1053
+ t.h &= ~STATUS_UNINITIALIZED;
551
1054
  // Set _modified for effects, but not for tracked effects (they handle their own scheduling)
552
- if (e.Re && e.Re !== EFFECT_TRACKED) e.Je = true;
1055
+ if (e.je && e.je !== EFFECT_TRACKED) e.Xe = true;
1056
+ // A quiet re-ask classification preserved through a held landing dies
1057
+ // with the value commit — the commit IS the reveal (#3178). Gated on the
1058
+ // staged value: status propagation queues pending nodes whose windows
1059
+ // are still OPEN (no staged value), and their live classification must
1060
+ // survive this sweep.
1061
+ if (e.o) e.o.Le = false;
553
1062
  }
554
1063
  // The committed hold is the first observable answer for a loading-window
555
1064
  // node — the window closes here, not at compute time (#2990). Unconditional
556
1065
  // store to an always-present computed slot.
557
- t.Ee = false;
558
- t.se &= ~REACTIVE_MANUAL_WRITE;
559
- if (!(t.S & STATUS_PENDING)) t.S &= ~STATUS_UNINITIALIZED;
560
- if (t.We !== null || t.Qe !== null) GlobalQueue.me(t, false, true);
561
- if (e.Oe || e.ge) GlobalQueue.un(e);
1066
+ t.ve = false;
1067
+ t.oe &= ~REACTIVE_MANUAL_WRITE;
1068
+ // The children this commit publishes are the frame's now (#3404) — and so
1069
+ // are the dependencies of the pass that produced the value: the previous
1070
+ // frame's tail goes (A30, #3410; `recompute` left it for a staged pass). Only
1071
+ // after a clean pass: `_error` is cleared by a clean pass or by the node's
1072
+ // own landing (whose pass was clean), so a set `_error` means the last pass
1073
+ // threw, kept its full list, and `_depsTail` marks where it stopped.
1074
+ if (t.o?._ == null) trimStaleDeps(t);
1075
+ // A LANE frame still parked here (#3662) rode a hold that stashed the run
1076
+ // that would have retired it: this commit applies that run, so it goes too.
1077
+ t.C &= -68157441;
1078
+ if (!(t.h & STATUS_PENDING)) t.h &= ~STATUS_UNINITIALIZED;
1079
+ // A flight this commit leaves in the air (unobserved, or observed only by
1080
+ // a boundary) now has PUBLISHED inputs: its committed value is stale
1081
+ // against the frame. read()'s reveal carve-out keys on the mark (#3305).
1082
+ else e.C |= CONFIG_INPUTS_PUBLISHED;
1083
+ if (t.o != null && (t.o._n !== null || t.o.fn !== null)) GlobalQueue.en(t, false, true);
1084
+ if (e.C & CONFIG_HAS_COMPANIONS) GlobalQueue.ti(e);
562
1085
  }
563
1086
 
564
1087
  // Store commit hook (INTERNALS-STORE-STATE.md §3): installed by the store
@@ -572,10 +1095,44 @@ function setStoreCommitHook(e) {
572
1095
  storeCommitHook = e;
573
1096
  }
574
1097
 
1098
+ /** Held truth committed this finalize, awaiting its post-revert wake (see
1099
+ * finalizePureQueue): the commit IS the reveal, but subscribers must not
1100
+ * re-derive until the settling transaction's optimistic overrides have
1101
+ * reverted — a commit-time wake recomputes them in the window where
1102
+ * confirming truth is committed and the override still displays, a torn
1103
+ * frame no timeline contains. */ const heldRevealed = [];
1104
+
1105
+ /** Unchanged passes with a stale dependency tail, waiting on this flush's
1106
+ * verdict (A30, #3469). A pass that changed nothing replaced nothing either —
1107
+ * and cannot know at its own tail whether the flush that ran it will park:
1108
+ * parked, its inputs are held and the committed frame still derives from the
1109
+ * tail (`b() ? b() : a()` computed `1` from the held `b`, equal to the `1` it
1110
+ * had from `a` — with `a` trimmed, the mainline `a = 2` never reached it).
1111
+ * Trimmed when the flush commits; dropped with a park, the tail stays linked
1112
+ * until a committing pass trims it (one spurious recompute at most). */ const heldTrims = [];
1113
+
575
1114
  function commitPendingNodes() {
576
- const e = currentBatch.Qt;
1115
+ while (heldTrims.length) trimStaleDeps(heldTrims.pop());
1116
+ const e = currentBatch.Gn;
577
1117
  for (let t = 0; t < e.length; t++) {
578
- commitPendingNode(e[t]);
1118
+ const n = e[t];
1119
+ commitPendingNode(n);
1120
+ // The stamp dies with the commit (#3143) — symmetric with
1121
+ // resolveOptimisticNodes clearing optimistic stamps. A stamp outliving
1122
+ // its transaction let any later write (even a value-equal no-op, which
1123
+ // re-opens before the equality bail) resurrect the finished transaction;
1124
+ // a boundary flag rewritten every finalize pass then spun the drain loop
1125
+ // forever (#3140). The held-truth mark dies the same death — the commit
1126
+ // IS the reveal — but its wake defers to the post-revert pass: ordinary
1127
+ // subscribers were masked to committed all hold (some re-derived against
1128
+ // that old view and cached it), and commits are otherwise silent
1129
+ // (staging already notified), so without a wake they'd hold the old
1130
+ // world forever.
1131
+ n.we = null;
1132
+ if (n.C & CONFIG_HELD_TRUTH) {
1133
+ n.C &= ~CONFIG_HELD_TRUTH;
1134
+ heldRevealed.push(n);
1135
+ }
579
1136
  }
580
1137
  e.length = 0;
581
1138
  storeCommitHook?.();
@@ -584,28 +1141,59 @@ function commitPendingNodes() {
584
1141
  function finalizePureQueue(e = null, t = false) {
585
1142
  // For incomplete transitions, skip pending resolution and optimistic reversion
586
1143
  // For completing transitions or no-transition, resolve pending and revert optimistic
1144
+ const n = currentBatch;
587
1145
  const i = !t;
588
1146
  if (i) commitPendingNodes();
589
- if (!t && globalQueue.vt.length) checkBoundaryChildren(globalQueue);
590
- const n = dirtyQueue.EE >= dirtyQueue.Ve;
591
- if (n) runHeap(dirtyQueue, GlobalQueue.Ce);
1147
+ // A parked finalize sweeps nothing: the boundaries' staged swaps are the
1148
+ // transaction's, and a boundary whose own output parks the verdict was
1149
+ // judged under it in run(), ahead of the verdict (#3540).
1150
+ if (!t && globalQueue.ft.length) checkBoundaryChildren(globalQueue);
1151
+ // Contested effects (#3322) re-derive from the world this commit just
1152
+ // produced. Ahead of the heap run — not the post-heap gated replay — so
1153
+ // the recompute and the effect phase land in this same pass and the value
1154
+ // the other transaction wrote into the slot is never published.
1155
+ // (No clear: a completed transition is never finalized again.)
1156
+ // A transaction whose settle reverts optimism re-derives them post-revert
1157
+ // instead (below, with the gated replay): between commitPendingNodes and
1158
+ // _resolveOptimistic the truth is committed but the overrides still
1159
+ // display, and a re-derive here would compose the two — the #3164 tear,
1160
+ // one window later. The slot meanwhile holds the frame that is on screen.
1161
+ const r = e?.Dn;
1162
+ const s = i && (e ?? n).cn.length !== 0;
1163
+ if (r && !s) for (const e of r) if (!(e.oe & REACTIVE_DISPOSED)) enqueueSub(e);
1164
+ const o = dirtyQueue.EE >= dirtyQueue.ln;
1165
+ if (o) runHeap(dirtyQueue, GlobalQueue.Ke);
592
1166
  if (i) {
593
- if (n) commitPendingNodes();
1167
+ // Boundary checks, commit hooks and recomputes can enter a transaction,
1168
+ // which adopts the batch this finalize was settling: nothing batch-derived
1169
+ // may be committed or reverted here — the entered transaction owns it now
1170
+ // (#3319). A completing transaction's OWN containers are a different
1171
+ // matter: when the ambient batch was separate from it (the #2916 shape),
1172
+ // adoption never touched them and it must still settle them; when the
1173
+ // batch WAS the completing transaction, adoption re-stamped its contents
1174
+ // into the entered one and there is nothing left to settle.
1175
+ if (currentBatch !== n) {
1176
+ if (e === null || e === n) return;
1177
+ } else if (o) commitPendingNodes();
594
1178
  // The settling batch: the completing transaction's, or the ambient one.
595
- const t = e ?? globalQueue.m;
1179
+ const t = e ?? n;
596
1180
  // Optimistic reversion: a non-empty batch means _optimisticWrite ran,
597
1181
  // which installed the engine's hooks.
598
- if (t.Me.length) GlobalQueue.En(t.Me);
1182
+ if (t.cn.length) GlobalQueue.oi(t.cn);
1183
+ if (r && s) {
1184
+ for (const e of r) if (!(e.oe & REACTIVE_DISPOSED)) enqueueSub(e);
1185
+ schedule();
1186
+ }
599
1187
  // Replay entanglement: subs recorded by the read-time gate get rescheduled
600
1188
  // so they re-run with the now-committed values visible. The ambient batch
601
1189
  // replays too — laneReadsCommitted records readers whose committed-view
602
1190
  // read hid a same-tick plain write that just committed above (#2963).
603
- if (t.ln.size) {
604
- for (const e of t.ln) {
605
- if (e.se & REACTIVE_DISPOSED) continue;
1191
+ if (t.In.size) {
1192
+ for (const e of t.In) {
1193
+ if (e.oe & REACTIVE_DISPOSED) continue;
606
1194
  enqueueSub(e);
607
1195
  }
608
- t.ln.clear();
1196
+ t.In.clear();
609
1197
  // A completing transition keeps the outer flush loop alive by itself;
610
1198
  // the ambient batch needs the re-arm or the replay sits in the heap
611
1199
  // until the next unrelated write.
@@ -619,23 +1207,41 @@ function finalizePureQueue(e = null, t = false) {
619
1207
  // (the earlier sweep above ran while the marks were still live).
620
1208
  if (t.A.length) {
621
1209
  GlobalQueue.G(t.A);
622
- if (globalQueue.vt.length) checkBoundaryChildren(globalQueue);
1210
+ if (globalQueue.ft.length) checkBoundaryChildren(globalQueue);
623
1211
  }
624
1212
  // A non-empty set means trackOptimisticStore ran, which installed the
625
1213
  // hook; the hook iterates, clears, and schedules (keeping the loop out of
626
1214
  // core lets esbuild shake it — rollup already folds the null guard). The
627
1215
  // completing transition scopes the clear to its own layer keys (#2899).
628
- if (t.dn.size) GlobalQueue.gt(t.dn, e);
1216
+ if (t.ri.size) GlobalQueue._t(t.ri, e);
1217
+ // Held-truth reveal wake (#3164), post-revert by construction: this
1218
+ // finalize committed confirming truth whose subscribers were masked all
1219
+ // hold — some re-derived against the committed view (the staging, or a
1220
+ // confirming carrier's landing, notified them as a plain write) and
1221
+ // cached it, and stash-restored applies may carry those torn values.
1222
+ // Waking and recomputing HERE — after _resolveOptimistic and the store
1223
+ // clears above — means every apply paints the settled view; a wake at
1224
+ // commit time would recompute them in the window where truth is
1225
+ // committed but the settling transaction's overrides still display.
1226
+ if (heldRevealed.length !== 0) {
1227
+ while (heldRevealed.length) insertSubs(heldRevealed.pop());
1228
+ if (dirtyQueue.EE >= dirtyQueue.ln) {
1229
+ runHeap(dirtyQueue, GlobalQueue.Ke);
1230
+ commitPendingNodes();
1231
+ }
1232
+ }
629
1233
  sweepTransientStoreNodes();
630
1234
  // Lanes only enter activeLanes through the engine's getOrCreateLane.
631
- if (activeLanes.size) GlobalQueue.In(e);
1235
+ if (activeLanes.size) GlobalQueue.li(e);
632
1236
  }
633
1237
  }
634
1238
 
635
- function checkBoundaryChildren(e) {
636
- for (const t of e.vt) {
637
- t.ne?.();
638
- checkBoundaryChildren(t);
1239
+ /** The boundary sweep: the commit's (`_checkSources`), or — `held`, before
1240
+ * the verdict (#3540) — the one for a collecting boundary whose output is
1241
+ * pending on its fallback's read (`_judgeHeld`). */ function checkBoundaryChildren(e, t) {
1242
+ for (const n of e.ft) {
1243
+ t ? n.Re?.() : n.Ee?.();
1244
+ checkBoundaryChildren(n, t);
639
1245
  }
640
1246
  }
641
1247
 
@@ -657,8 +1263,10 @@ function checkBoundaryChildren(e) {
657
1263
 
658
1264
  function reassignPendingTransition(e) {
659
1265
  for (let t = 0; t < e.length; t++) {
660
- e[t].Ne = activeTransition;
661
- }
1266
+ e[t].we = activeTransition;
1267
+ e[t].C &= ~CONFIG_ADOPTED_UNFLUSHED;
1268
+ // this flush carried it
1269
+ }
662
1270
  }
663
1271
 
664
1272
  const globalQueue = new GlobalQueue;
@@ -671,6 +1279,16 @@ const globalQueue = new GlobalQueue;
671
1279
  let currentBatch = globalQueue.m;
672
1280
 
673
1281
  function flush(e) {
1282
+ // Inside an action body the drain is incoherent (#3333): the action's
1283
+ // writes are held by its transaction until it settles, so a drain can't
1284
+ // reveal them — and the loop below only exits once `activeTransition` is
1285
+ // null, so it would PARK the transaction mid-slice and every write after it
1286
+ // in the body would land as a plain, committed write. The reporter's
1287
+ // "leading flush()" workaround was exactly that leak. Prod: run `fn` if
1288
+ // given (its writes stay in the transaction) and skip the drain.
1289
+ if (actionStepDepth > 0) {
1290
+ return e ? e() : undefined;
1291
+ }
674
1292
  if (e) {
675
1293
  syncDepth++;
676
1294
  try {
@@ -685,7 +1303,7 @@ function flush(e) {
685
1303
  }
686
1304
  }
687
1305
  }
688
- if (globalQueue.cn) {
1306
+ if (globalQueue.Jn) {
689
1307
  return;
690
1308
  }
691
1309
  if (halted) return;
@@ -694,39 +1312,123 @@ function flush(e) {
694
1312
  while (scheduled || activeTransition) {
695
1313
  globalQueue.flush();
696
1314
  }
1315
+ // Provenance ends with the drain: every ambient window (an action's first
1316
+ // slice, a landing's propagation) runs to this flush.
1317
+ origin = 0;
697
1318
  }
698
1319
 
699
1320
  function runQueue(e, t) {
700
- for (let i = 0; i < e.length; i++) e[i](t);
1321
+ for (let n = 0; n < e.length; n++) e[n](t);
701
1322
  }
702
1323
 
703
- function reporterBlocksSource(e, t) {
704
- if (e.se & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false;
705
- if (e.oe?.has(t)) return true;
706
- for (let i = e.tt; i; i = i.nt) {
707
- let e = i.it;
1324
+ /** Does `reporter` still hold the transaction waiting on `source` — live,
1325
+ * routed to no collecting loading boundary, and deriving from the source in
1326
+ * its current pass? The verdict's per-reporter test (sourceObserved), also
1327
+ * a boundary re-arm's (boundaries.ts `_rearm` runs before the verdict prunes
1328
+ * registrations that stopped counting). */ function reporterBlocksSource(e, t, n) {
1329
+ const i = e.oe;
1330
+ if (i & REACTIVE_DISPOSED) return false;
1331
+ // A zombie renders until the commit that disposes it (#3463): while its
1332
+ // removal is staged in a live transaction it is still on screen, and what
1333
+ // it displays must stay consistent with the frame — a held `Show`'s
1334
+ // `Details: 0` beside the lane's `Value: 1` otherwise. Its say is moot for
1335
+ // the verdict of the transaction that stages the removal (`verdict`): done,
1336
+ // and the commit disposes it; not done, and it stays parked regardless. A
1337
+ // zombie whose removal commits this flush (owner's pass not held) is dead.
1338
+ // The owner is stamped when the flush parks; held in this flush, its
1339
+ // staging transaction is the active one.
1340
+ if (i & REACTIVE_ZOMBIE) {
1341
+ let t = e;
1342
+ while (t && t.oe & REACTIVE_ZOMBIE) t = t._parent;
1343
+ let i = t && (t.we || (t.C & CONFIG_HELD_CHILDREN ? activeTransition : null));
1344
+ // A LANE frame's member (#3662) is displayed until its owner's run
1345
+ // applies, and the lane's transaction is what applies it (its completion
1346
+ // runs the lane's queue): moot for that verdict, live for every other.
1347
+ if (!i && t && t.C & CONFIG_LANE_FRAME && t.o?.me) i = findLane(t.o.me).we;
1348
+ if (!i || (i = currentTransition(i)).Rn === true || i === n) return false;
1349
+ }
1350
+ // Fallback-caught async holds nothing. A collecting loading boundary
1351
+ // consumes the notification, so a reader under a fallback never registers —
1352
+ // but a reader registered while its boundary showed content stays
1353
+ // registered when the boundary's `on` later changes and it flips to the
1354
+ // fallback. The reader is behind the fallback now; if nothing outside the
1355
+ // boundary consumes the flight, the hold is over (A33, ruled 2026-09-12, #3375).
1356
+ for (let t = e.T; t; t = t._parent) if (t.te & STATUS_PENDING && !t.q) return false;
1357
+ // "Still derives from the source" is a question about THIS pass's reads:
1358
+ // the deps up to `_depsTail`. Past it lie the committed frame's — kept
1359
+ // linked by A30 until the commit trims them (a staged pass, an errored
1360
+ // one). Reading them here made a reporter whose pass had stopped reading
1361
+ // the source (a gate closed in the same flush as the write) look live, and
1362
+ // the hold it kept was the commit that would have trimmed the dep that
1363
+ // kept it (spec O3, same-flush form; fuzzer #3446 P1 cases 21/79). A
1364
+ // trimmed list ends at `_depsTail`, so the bound is free there; a pass
1365
+ // that read nothing has a null tail and derives from nothing.
1366
+ // The registration is trusted: a pending mark rides only this pass's links
1367
+ // (notifyStatus skips the kept tail), so a registered reporter read the
1368
+ // source, or threw on it.
1369
+ if (e.o?.ue?.has(t)) return true;
1370
+ const r = e.Nn;
1371
+ for (let n = r === null ? null : e.Ie; n; n = n === r ? null : n.Ne) {
1372
+ let e = n.Oe;
708
1373
  while (e) {
709
- if (e === t || e.lt === t) return true;
710
- e = e.nn;
1374
+ // Or through a memo pending on the flight (#3494): a stale reader
1375
+ // served a held memo's committed value never turned pending itself, so
1376
+ // its only trace of the flight is the memo between them — `copy` of
1377
+ // `details`. Judged dead, its transaction released `count=1` beside
1378
+ // the `Copy: 0` it displays. `_pendingSources` is transitive, so one
1379
+ // hop covers any depth.
1380
+ if (e === t || e.De === t || e.o?.ue?.has(t)) return true;
1381
+ e = e.o?.kn;
711
1382
  }
712
1383
  }
713
- return !!(e.S & STATUS_PENDING && e._ instanceof NotReadyError && e._.source === t);
1384
+ return !!(e.h & STATUS_PENDING && e.o?._ instanceof NotReadyError && e.o?._.source === t);
1385
+ }
1386
+
1387
+ /**
1388
+ * Does a live reporter of `transition` still observe `source` pending? Dead
1389
+ * reporters (disposed, behind a fallback, no longer reading the source) are
1390
+ * pruned as they are found, and the source's entry with them. Shared by the
1391
+ * settle verdict and the lane's hold check (`waitingTransition`): a live
1392
+ * action parks its transaction without a verdict, so this prune is the only
1393
+ * one an optimistic lane whose last async reader unmounted mid-action ever
1394
+ * gets — without it the lane held on the dead reporter's registration until
1395
+ * the flight it no longer observed landed (#3426).
1396
+ */ function sourceObserved(e, t, n) {
1397
+ const i = e.le.get(t);
1398
+ let r = false;
1399
+ for (const e of i ?? []) {
1400
+ if (reporterBlocksSource(e, t, n)) return true;
1401
+ // A zombie the verdict passes over is kept, not pruned (#3463): moot for
1402
+ // this verdict, it still holds a lane's reveal while the transaction
1403
+ // stays parked on something else.
1404
+ if (n && e.oe & REACTIVE_ZOMBIE) r = true; else i.delete(e);
1405
+ }
1406
+ if (!r) e.le.delete(t);
1407
+ return false;
714
1408
  }
715
1409
 
716
1410
  function transitionComplete(e) {
717
- if (e.fn) return true;
718
- if (e.ie.length) return false;
1411
+ if (e.Rn) return true;
1412
+ if (e.pe.length) {
1413
+ return false;
1414
+ }
719
1415
  let t = true;
720
- for (const [i, n] of e.Ie) {
721
- let s = false;
722
- for (const e of n) {
723
- if (reporterBlocksSource(e, i)) {
724
- s = true;
725
- break;
726
- }
727
- n.delete(e);
728
- }
729
- if (!s) e.Ie.delete(i); else if (i.S & STATUS_PENDING && i._?.source === i) {
1416
+ for (const n of e.le.keys()) {
1417
+ // The source blocks while its OWN flight is up — the self entry in its
1418
+ // pending sources (added by notifyStatus's source path, with status;
1419
+ // retired by the landing and the supersede sweep, so it implies
1420
+ // STATUS_PENDING). `_error.source` is not that test: propagation from an
1421
+ // input that went pending later overwrites it with the input (#3375 — a
1422
+ // boundary-consumed load re-asked under a held derivation), and the
1423
+ // still-flying source read as settled, committing the writes it was
1424
+ // asked with ahead of its answer.
1425
+ // Not the self entry alone: a source whose own flight an upstream re-ask
1426
+ // superseded is still pending — on that re-ask (#3462). Its reader cannot
1427
+ // render until the chain lands, and the landing folds this transaction in
1428
+ // (enterWaiting). Judged complete instead, a re-entry between the two (a
1429
+ // repeated write to a held signal) committed the held writes beside the
1430
+ // reader's stale frame.
1431
+ if (sourceObserved(e, n, e) && n.o?.ue?.size) {
730
1432
  t = false;
731
1433
  break;
732
1434
  }
@@ -734,24 +1436,79 @@ function transitionComplete(e) {
734
1436
  // Override blockage lives with the engine (absent hook = "no optimistic
735
1437
  // blockage"); the hook's loops over _optimisticNodes/_optimisticStores are
736
1438
  // no-ops when the transition holds neither, so no pre-check is needed.
737
- if (t && GlobalQueue.Tn?.(e)) t = false;
738
- t && (e.fn = true);
1439
+ if (t && GlobalQueue.ai?.(e)) t = false;
1440
+ t && (e.Rn = true);
739
1441
  return t;
740
1442
  }
741
1443
 
1444
+ /** A fresh, unentered transaction (#3146): the optimistic store's truth
1445
+ * flight DECLARES an owned transaction instead of relying on whatever the
1446
+ * ambient adoption machinery stamped on its firewall. Activate it with
1447
+ * initTransition; it is a plain batch until then. */ function createTransition() {
1448
+ return createBatch();
1449
+ }
1450
+
742
1451
  function currentTransition(e) {
743
- while (e.fn && typeof e.fn === "object") e = e.fn;
1452
+ while (e.Rn && typeof e.Rn === "object") e = e.Rn;
744
1453
  return e;
745
1454
  }
746
1455
 
1456
+ /**
1457
+ * The live transition blocked on `source` — the one whose render reader
1458
+ * observed it pending (INV-3 records the observation in whichever transaction
1459
+ * was active when the reader was notified). The observation is a fact about
1460
+ * the node, so a hold check must not assume it was recorded in the transaction
1461
+ * it happens to hold — lanes merge across transactions (#2912), and a merged
1462
+ * root's transaction knows nothing of the async its members' transactions
1463
+ * observed (#3335). Null when nobody is waiting — a registration whose every
1464
+ * reporter has since died is nobody (#3426).
1465
+ */ function waitingTransition(e) {
1466
+ for (const t of transitions) if (sourceObserved(t, e)) return t;
1467
+ return null;
1468
+ }
1469
+
1470
+ /** A landing enters EVERY parked transaction still waiting on `source`, folding
1471
+ * them into the active one (A15: each reveal that discovered the flight
1472
+ * completes at its landing). The fold used to happen as the waiters' stamped
1473
+ * readers recomputed under the landing — recompute re-entering an effect's
1474
+ * stamp — which also folded in writes those readers merely shared a hole
1475
+ * with (#3407); effects no longer re-enter, so the landing folds explicitly.
1476
+ * Live iteration is safe: a merge deletes the outgoing (active) entry and
1477
+ * re-adds the visited one. */ function enterWaiting(e) {
1478
+ for (const t of transitions) if (sourceObserved(t, e)) globalQueue.initTransition(t);
1479
+ }
1480
+
747
1481
  function runInTransition(e, t) {
748
- const i = activeTransition;
1482
+ const n = activeTransition;
1483
+ try {
1484
+ activeTransition = currentTransition(e);
1485
+ return t();
1486
+ } finally {
1487
+ activeTransition = n;
1488
+ }
1489
+ }
1490
+
1491
+ /** Run `fn` with `transition` as BOTH the ambient transaction and the
1492
+ * registration batch, restoring both after. runInTransition alone is not
1493
+ * enough for code that WRITES on behalf of a transaction from inside someone
1494
+ * else's window (optimistic replay re-arming a still-open action's edits
1495
+ * during a landing commit, #3123): registrations route through the queue's
1496
+ * batch pointer, and a bare activeTransition swap leaves them in the ambient
1497
+ * batch — a plain batch "completes" at the next flush and reverts optimistic
1498
+ * registrations that were supposed to live with the transaction.
1499
+ * initTransition is the wrong tool here: it MERGES the currently ambient
1500
+ * transaction into the target, entangling whatever the interrupted window
1501
+ * belonged to. */ function runAsTransitionBatch(e, t) {
1502
+ const n = activeTransition;
1503
+ const i = globalQueue.m;
749
1504
  try {
750
1505
  activeTransition = currentTransition(e);
1506
+ currentBatch = globalQueue.m = activeTransition;
751
1507
  return t();
752
1508
  } finally {
753
- activeTransition = i;
1509
+ activeTransition = n;
1510
+ currentBatch = globalQueue.m = i;
754
1511
  }
755
1512
  }
756
1513
 
757
- export { GlobalQueue, Queue, activeAffectsMarks, activeLanes, activeTransition, armReaskClear, assignOrMergeLane, clock, currentTransition, dirtyQueue, enforceLoadingBoundary, finalizePureQueue, findLane, flush, globalQueue, haltReactivity, insertSubs, projectionWriteActive, queuePendingNode, resetErrorHalt, runInTransition, schedule, setProjectionWriteActive, setStoreCommitHook, shiftAffectsMarks, storeCommitHook, zombieQueue };
1514
+ export { GlobalQueue, Queue, ROOT_ERROR_HOOK, actionStepDepth, activeAffectsMarks, activeLanes, activeTransition, armReaskClear, assignOrMergeLane, batchJoins, bumpNotifyEpoch, clock, createTransition, currentTransition, deferSlotRelease, dirtyQueue, enforceLoadingBoundary, entangleConfirmingTransitions, enterActionStep, enterWaiting, exitActionStep, finalizePureQueue, findLane, flush, globalQueue, haltReactivity, hasActiveOverride, heldTrims, insertSubs, notifyEpoch, origin, pendingRearms, projectionWriteActive, queuePendingNode, queueRearm, reaskArmed, reporterBlocksSource, resetErrorHalt, runAsTransitionBatch, runInTransition, schedule, scheduleWithheld, setOrigin, setProjectionWriteActive, setStoreCommitHook, shiftAffectsMarks, sourceObserved, storeCommitHook, transitions, waitingTransition, wakeParked, wokenTransitions, zombieQueue };