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

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