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