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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/dev-shared.js +7138 -0
  2. package/dist/dev.attribution.js +3307 -0
  3. package/dist/dev.js +5691 -4491
  4. package/dist/observe/affects.js +129 -0
  5. package/dist/observe/attribution.js +7 -0
  6. package/dist/observe/boundaries.js +793 -0
  7. package/dist/observe/core/action.js +177 -0
  8. package/dist/observe/core/async.js +849 -0
  9. package/dist/observe/core/attribution-costs.js +66 -0
  10. package/dist/observe/core/attribution-feedback.js +280 -0
  11. package/dist/observe/core/attribution-hooks.js +83 -0
  12. package/dist/observe/core/attribution-queries.js +43 -0
  13. package/dist/observe/core/attribution.js +2904 -0
  14. package/dist/observe/core/constants.js +283 -0
  15. package/dist/{types-cjs/core/context.d.cts → observe/core/context.js} +37 -12
  16. package/dist/observe/core/core.js +1999 -0
  17. package/dist/observe/core/dev.js +343 -0
  18. package/dist/observe/core/effect.js +200 -0
  19. package/dist/observe/core/error-hooks.js +71 -0
  20. package/dist/observe/core/error.js +80 -0
  21. package/dist/observe/core/external.js +98 -0
  22. package/dist/observe/core/graph.js +155 -0
  23. package/dist/observe/core/heap.js +147 -0
  24. package/dist/observe/core/invariants.js +45 -0
  25. package/dist/observe/core/lanes.js +205 -0
  26. package/dist/observe/core/optimistic.js +627 -0
  27. package/dist/observe/core/owner.js +353 -0
  28. package/dist/observe/core/scheduler.js +1547 -0
  29. package/dist/observe/core/verdict.js +674 -0
  30. package/dist/observe/index.js +59 -0
  31. package/dist/observe/map.js +484 -0
  32. package/dist/observe/signals.js +731 -0
  33. package/dist/observe/store/index.js +44 -0
  34. package/dist/observe/store/next/optimistic.js +717 -0
  35. package/dist/observe/store/next/projection.js +266 -0
  36. package/dist/observe/store/next/reconcile.js +375 -0
  37. package/dist/observe/store/next/store.js +2480 -0
  38. package/dist/observe/store/next/target.js +51 -0
  39. package/dist/observe/store/store.js +331 -0
  40. package/dist/observe/store/storePath.js +103 -0
  41. package/dist/observe/store/utils.js +1070 -0
  42. package/dist/prod/affects.js +10 -9
  43. package/dist/prod/attribution.js +45 -0
  44. package/dist/prod/boundaries.js +388 -178
  45. package/dist/prod/core/action.js +65 -37
  46. package/dist/prod/core/async.js +378 -163
  47. package/dist/prod/core/attribution-hooks.js +3 -0
  48. package/dist/prod/core/constants.js +186 -1
  49. package/dist/prod/core/context.js +10 -16
  50. package/dist/prod/core/core.js +1546 -416
  51. package/dist/prod/core/dev.js +18 -1
  52. package/dist/prod/core/effect.js +119 -79
  53. package/dist/prod/core/error-hooks.js +71 -0
  54. package/dist/prod/core/error.js +13 -1
  55. package/dist/prod/core/external.js +4 -4
  56. package/dist/prod/core/graph.js +105 -54
  57. package/dist/prod/core/heap.js +72 -65
  58. package/dist/prod/core/invariants.js +3 -2
  59. package/dist/prod/core/lanes.js +117 -52
  60. package/dist/prod/core/optimistic.js +458 -104
  61. package/dist/prod/core/owner.js +131 -84
  62. package/dist/prod/core/scheduler.js +998 -241
  63. package/dist/prod/core/verdict.js +417 -112
  64. package/dist/prod/index.js +20 -8
  65. package/dist/prod/map.js +312 -151
  66. package/dist/prod/signals.js +363 -32
  67. package/dist/prod/store/index.js +2 -0
  68. package/dist/prod/store/next/optimistic.js +530 -197
  69. package/dist/prod/store/next/projection.js +142 -57
  70. package/dist/prod/store/next/reconcile.js +149 -103
  71. package/dist/prod/store/next/store.js +1470 -346
  72. package/dist/prod/store/next/target.js +40 -9
  73. package/dist/prod/store/store.js +23 -19
  74. package/dist/prod/store/utils.js +997 -130
  75. package/dist/types/attribution.d.ts +19 -0
  76. package/dist/types/attribution.prod.d.ts +22 -0
  77. package/dist/types/boundaries.d.ts +78 -5
  78. package/dist/types/core/action.d.ts +12 -5
  79. package/dist/types/core/async.d.ts +3 -1
  80. package/dist/types/core/attribution-costs.d.ts +35 -0
  81. package/dist/types/core/attribution-feedback.d.ts +133 -0
  82. package/dist/types/core/attribution-hooks.d.ts +304 -0
  83. package/dist/types/core/attribution-queries.d.ts +24 -0
  84. package/dist/types/core/attribution.d.ts +892 -0
  85. package/dist/types/core/constants.d.ts +179 -0
  86. package/dist/types/core/core.d.ts +299 -25
  87. package/dist/types/core/dev.d.ts +421 -8
  88. package/dist/types/core/effect.d.ts +8 -1
  89. package/dist/types/core/error-hooks.d.ts +71 -0
  90. package/dist/types/core/error.d.ts +9 -0
  91. package/dist/types/core/graph.d.ts +23 -0
  92. package/dist/types/core/heap.d.ts +5 -3
  93. package/dist/types/core/index.d.ts +6 -3
  94. package/dist/types/core/invariants.d.ts +5 -1
  95. package/dist/types/core/lanes.d.ts +51 -19
  96. package/dist/types/core/owner.d.ts +15 -3
  97. package/dist/types/core/scheduler.d.ts +248 -1
  98. package/dist/types/core/types.d.ts +124 -41
  99. package/dist/types/index.d.ts +18 -5
  100. package/dist/types/map.d.ts +2 -0
  101. package/dist/types/signals.d.ts +145 -13
  102. package/dist/types/store/index.d.ts +7 -6
  103. package/dist/types/store/next/optimistic.d.ts +18 -13
  104. package/dist/types/store/next/projection.d.ts +5 -2
  105. package/dist/types/store/next/reconcile.d.ts +7 -0
  106. package/dist/types/store/next/store.d.ts +83 -11
  107. package/dist/types/store/next/target.d.ts +108 -11
  108. package/dist/types/store/store.d.ts +20 -11
  109. package/dist/types/store/utils.d.ts +188 -0
  110. package/package.json +31 -26
  111. package/dist/node.cjs +0 -8137
  112. package/dist/types/store/optimistic.d.ts +0 -45
  113. package/dist/types/store/projection.d.ts +0 -70
  114. package/dist/types/store/reconcile.d.ts +0 -46
  115. package/dist/types-cjs/affects.d.cts +0 -47
  116. package/dist/types-cjs/boundaries.d.cts +0 -175
  117. package/dist/types-cjs/core/action.d.cts +0 -64
  118. package/dist/types-cjs/core/async.d.cts +0 -21
  119. package/dist/types-cjs/core/constants.d.cts +0 -71
  120. package/dist/types-cjs/core/core.d.cts +0 -156
  121. package/dist/types-cjs/core/dev.d.cts +0 -64
  122. package/dist/types-cjs/core/effect.d.cts +0 -30
  123. package/dist/types-cjs/core/error.d.cts +0 -47
  124. package/dist/types-cjs/core/external.d.cts +0 -15
  125. package/dist/types-cjs/core/graph.d.cts +0 -5
  126. package/dist/types-cjs/core/heap.d.cts +0 -22
  127. package/dist/types-cjs/core/index.d.cts +0 -13
  128. package/dist/types-cjs/core/invariants.d.cts +0 -59
  129. package/dist/types-cjs/core/lanes.d.cts +0 -56
  130. package/dist/types-cjs/core/optimistic.d.cts +0 -6
  131. package/dist/types-cjs/core/owner.d.cts +0 -124
  132. package/dist/types-cjs/core/scheduler.d.cts +0 -172
  133. package/dist/types-cjs/core/types.d.cts +0 -148
  134. package/dist/types-cjs/core/verdict.d.cts +0 -2
  135. package/dist/types-cjs/index.d.cts +0 -10
  136. package/dist/types-cjs/map.d.cts +0 -69
  137. package/dist/types-cjs/package.json +0 -3
  138. package/dist/types-cjs/signals.d.cts +0 -624
  139. package/dist/types-cjs/store/index.d.cts +0 -19
  140. package/dist/types-cjs/store/next/optimistic.d.cts +0 -20
  141. package/dist/types-cjs/store/next/projection.d.cts +0 -24
  142. package/dist/types-cjs/store/next/reconcile.d.cts +0 -3
  143. package/dist/types-cjs/store/next/store.d.cts +0 -71
  144. package/dist/types-cjs/store/next/target.d.cts +0 -99
  145. package/dist/types-cjs/store/optimistic.d.cts +0 -45
  146. package/dist/types-cjs/store/projection.d.cts +0 -70
  147. package/dist/types-cjs/store/reconcile.d.cts +0 -46
  148. package/dist/types-cjs/store/store.d.cts +0 -138
  149. package/dist/types-cjs/store/storePath.d.cts +0 -58
  150. package/dist/types-cjs/store/utils.d.cts +0 -74
@@ -1,20 +1,22 @@
1
- import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, REACTIVE_ZOMBIE, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_MANUAL_WRITE, STATUS_ERROR, REACTIVE_RECOMPUTING_DEPS } from "./constants.js";
1
+ import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, REACTIVE_RECOMPUTING_DEPS, STATUS_PENDING, STATUS_UNINITIALIZED, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_MANUAL_WRITE, CONFIG_OVERRIDE_SUPERSEDED, CONFIG_HAS_COMPANIONS, CONFIG_ADOPTED_UNFLUSHED, CONFIG_CHILD_COMPANIONS, STATUS_ERROR } from "./constants.js";
2
2
 
3
- import { setSignal, prepareComputed, read, context, stale, currentOptimisticLane, tracking, setLatestReadActive, setContextInternal, optimisticComputed, setPendingCheckActive, latestReadActive, optimisticSignal, pendingCheckActive } from "./core.js";
3
+ import { setSignal, visibleOverride, unflushedValue, context, markLateLinker, read, stale, hasActiveOverride, prepareComputed, tracking, ext, setLatestReadActive, setContextInternal, optimisticComputed, unflushed, unflushedCompanions, setPendingCheckActive, latestReadActive, optimisticSignal, pendingCheckActive } from "./core.js";
4
4
 
5
5
  import { NotReadyError } from "./error.js";
6
6
 
7
7
  import { link } from "./graph.js";
8
8
 
9
- import { insertIntoHeap, queueFor, markHeap } from "./heap.js";
9
+ import { dispose } from "./owner.js";
10
+
11
+ import { insertIntoHeap, queueFor, enqueueSub } from "./heap.js";
10
12
 
11
13
  import "./invariants.js";
12
14
 
13
- import { findLane, hasActiveOverride } from "./lanes.js";
15
+ import { readsHeldCommitted, assignOrMergeLane } from "./lanes.js";
14
16
 
15
17
  import { installOptimisticEngine } from "./optimistic.js";
16
18
 
17
- import { GlobalQueue, clock, insertSubs, schedule, activeTransition, activeAffectsMarks } from "./scheduler.js";
19
+ import { GlobalQueue, insertSubs, schedule, activeTransition, currentTransition, runAsTransitionBatch, activeAffectsMarks } from "./scheduler.js";
18
20
 
19
21
  /**
20
22
  * The isPending()/latest() verdict layer, moved out of core.ts. Importing this
@@ -28,25 +30,92 @@ installOptimisticEngine();
28
30
 
29
31
  let pendingProbe = null;
30
32
 
33
+ /**
34
+ * Probes whose verdict was suppressed by the fresh-read pairing rule while
35
+ * the held write's fate was still undecided (see recordFreshRead /
36
+ * wakeSuppressedProbes): held node → the wrapper computeds that probed it.
37
+ * Entries die with the hold — the commit/revert snap clears them.
38
+ */ const suppressedProbes = new Map;
39
+
31
40
  /**
32
41
  * Get or create the pending signal for a node (lazy).
33
42
  * Used by isPending() to track pending state reactively.
34
- */ function getPendingSignal(e) {
35
- if (!e.Oe) {
43
+ */
44
+ /** #3038: register a companion-carrying firewall child on its firewall's
45
+ * companion set and arm the post-recompute snap (CONFIG_CHILD_COMPANIONS is
46
+ * the one-load gate at the call sites). The snap then iterates exactly the
47
+ * children someone asked verdicts of — O(companions) — never the full
48
+ * `_child` chain, which carries one node per materialized leaf (the
49
+ * O(all-leaves-ever-read)-per-update pathology). Entries live as long as the
50
+ * store addresses the leaf — the unobserved sweep's `unlinkFirewallChild`
51
+ * drops them (#3503); a store with no leaf-level isPending()/latest() reads
52
+ * never allocates the set or pays the walk. */ function markFirewallChildCompanions(e) {
53
+ const t = e.De;
54
+ if (!t) return;
55
+ t.C |= CONFIG_CHILD_COMPANIONS;
56
+ (ext(t).Un ??= new Set).add(e);
57
+ }
58
+
59
+ function getPendingSignal(e) {
60
+ let t = e.o?.Ze;
61
+ if (!t) {
36
62
  // Start false, write true if pending - ensures reversion returns to false
37
- e.Oe = optimisticSignal(false, {
63
+ t = optimisticSignal(false, {
38
64
  ownedWrite: true
39
65
  });
40
- e.Oe.nn = e;
41
- if (computePendingState(e)) setSignal(e.Oe, true);
66
+ ext(e).Ze = t;
67
+ e.C |= CONFIG_HAS_COMPANIONS;
68
+ markFirewallChildCompanions(e);
69
+ ext(t).kn = e;
70
+ if (computePendingState(e)) backfillCompanion(e, t, true);
71
+ joinUnflushedResync(e);
42
72
  }
43
- return e.Oe;
73
+ return t;
74
+ }
75
+
76
+ /**
77
+ * A lazily created companion's first write mirrors state the owner already
78
+ * carries — a held write, a pending verdict. The companion is created wherever
79
+ * the first latest()/isPending() read happens to run, but the write belongs
80
+ * to whatever HOLDS that state: written in the ambient window, the override
81
+ * would register in the ambient batch and revert when that flush's round ends
82
+ * — the shadow re-derived from the committed view, the verdict flipped false —
83
+ * while the owner's hold was still on (#3336: A and B differing only in
84
+ * whether a companion existed before the hold). A companion created lazily
85
+ * answers as if it had always existed: its backfill is registered with the
86
+ * owner's transaction and lives and reverts with it. A hold with no
87
+ * transaction (a pending async, a same-flush staged write) is ambient and the
88
+ * write stays ambient. (A28 (3), lifted from #3337.)
89
+ */ function backfillCompanion(e, t, n) {
90
+ const i = e.we;
91
+ if (i) runAsTransitionBatch(i, () => setSignal(t, n)); else setSignal(t, n);
92
+ }
93
+
94
+ /** A28: a companion created while its source carries an UNFLUSHED write joins
95
+ * the flush-start re-sync like a companion that existed at the write —
96
+ * syncCompanions only reaches companions that exist at write time. Without
97
+ * this the flush brings it current by a plain recompute instead of the
98
+ * optimistic write: its readers are then staged under whatever transaction
99
+ * the round entered (a memo over latest() of a held source was held with the
100
+ * source, and its untracked reads answered the previous value until the hold
101
+ * committed) rather than direct-committed as the optimistic view they are. */ function joinUnflushedResync(e) {
102
+ if (unflushed(e)) unflushedCompanions.push(e);
103
+ }
104
+
105
+ /** The staged value the verdict channels answer for (A28): while a node
106
+ * carries an unflushed write its `_pendingValue` is not yet part of any
107
+ * flushed world, so the channels answer for the value the last flush left
108
+ * staged (a held node's stash) or for nothing (NOT_PENDING). */ function flushedStaged(e) {
109
+ if (!unflushed(e)) return e._e;
110
+ // Ambient, or adopted before any flush (CONFIG_ADOPTED_UNFLUSHED): nothing
111
+ // a flush carried is staged for it. A held rewrite: the stash.
112
+ return e.we === null || e.C & CONFIG_ADOPTED_UNFLUSHED ? NOT_PENDING : e.o.bn;
44
113
  }
45
114
 
46
115
  function collectPendingSources(e) {
47
116
  if (!pendingProbe) return;
48
117
  pendingProbe.sources.add(e);
49
- const t = e.lt || e;
118
+ const t = e.De || e;
50
119
  if (t !== e) pendingProbe.sources.add(t);
51
120
  }
52
121
 
@@ -70,42 +139,38 @@ function collectPendingSources(e) {
70
139
  * (`_pendingObserver`) are skipped so an `isPending` wrapper memo never
71
140
  * inherits the coverage it reports on.
72
141
  */ function markWalk(e, t) {
73
- if (e.t) return true;
142
+ if (e.o?.t) return true;
74
143
  // A real error outranks an inherited mark (A16/A24c): an errored node
75
144
  // answers probes with its error, not a coverage verdict, and coverage does
76
145
  // not flow through it — matching the rails' behavior, where propagation
77
146
  // stopped at errored nodes. A DIRECT mark on an errored node still reads
78
147
  // pending (the count check above), also matching.
79
- if (e.S & STATUS_ERROR) return false;
148
+ if (e.h & STATUS_ERROR) return false;
80
149
  if (t.has(e)) return false;
81
150
  t.add(e);
82
- const n = e.lt;
151
+ const n = e.De;
83
152
  if (n && markWalk(n, t)) return true;
84
153
  // Mid-recompute (the clearStatus companion poke runs before
85
154
  // trimStaleDeps), only the validated prefix [_deps.._depsTail] is this
86
155
  // pass's dependency set — walking past it would read dropped deps and
87
156
  // latch a stale verdict on the companion.
88
157
  const i = e;
89
- const r = i.se & REACTIVE_RECOMPUTING_DEPS ? i.Ye : undefined;
158
+ const r = i.oe & REACTIVE_RECOMPUTING_DEPS ? i.Nn : undefined;
90
159
  if (r !== null) {
91
- for (let e = i.tt ?? null; e !== null; e = e.nt) {
92
- if (!e.ye && markWalk(e.it, t)) return true;
160
+ for (let e = i.Ie ?? null; e !== null; e = e.Ne) {
161
+ if (!e.ze && markWalk(e.Oe, t)) return true;
93
162
  if (e === r) break;
94
163
  }
95
164
  }
96
165
  return false;
97
166
  }
98
167
 
99
- /** Gated entry: apps with no live mark pay one integer compare. */ function markCovered(e) {
100
- return activeAffectsMarks !== 0 && markWalk(e, new Set);
101
- }
102
-
103
168
  function quietPending(e) {
104
- if (e.oe) {
105
- for (const t of e.oe) if (!t.pe) return false;
169
+ if (e.o?.ue) {
170
+ for (const t of e.o.ue) if (!t.o?.Le) return false;
106
171
  return true;
107
172
  }
108
- return e.pe;
173
+ return e.o?.Le ?? false;
109
174
  }
110
175
 
111
176
  // NOTE: a loadingValue node's open loading window (_loading) is verdict-quiet
@@ -116,52 +181,75 @@ function quietPending(e) {
116
181
  // verdict fully correlated with transition-class machinery and keeps server
117
182
  // (always false) and client hydration trivially consistent.
118
183
  function newQuestionInFlight(e) {
119
- return !!(e.S & STATUS_PENDING) && !(e.S & STATUS_UNINITIALIZED) && !quietPending(e);
184
+ return !!(e.h & STATUS_PENDING) && !(e.h & STATUS_UNINITIALIZED) && !quietPending(e);
120
185
  }
121
186
 
122
187
  function computePendingState(e) {
123
188
  const t = e;
124
- if (t.se & REACTIVE_DISPOSED) return false;
189
+ if (t.oe & REACTIVE_DISPOSED) return false;
125
190
  // Mark coverage is transitive by dep-graph reachability: a latest() shadow
126
191
  // reaches its owner (and a store leaf its firewall) through its own deps,
127
192
  // so the one walk covers direct marks, derivation, and companion chains.
128
- if (markCovered(e)) return true;
129
- const n = e.lt;
130
- if (e.nn) {
131
- const t = e.nn;
132
- const n = t.lt || t;
193
+ // Gated: apps with no live mark pay one integer compare.
194
+ if (activeAffectsMarks !== 0 && markWalk(e, new Set)) return true;
195
+ const n = e.De;
196
+ if (e.o?.kn) {
197
+ const t = e.o?.kn;
198
+ const n = t.De || t;
133
199
  return newQuestionInFlight(n);
134
200
  }
135
- if (n && e._e !== NOT_PENDING && !hasActiveOverride(e)) {
136
- return !!(n.se & REACTIVE_MANUAL_WRITE) || !n.Te && !(n.S & STATUS_PENDING) || !!(n.S & STATUS_PENDING) && quietPending(n);
201
+ const i = flushedStaged(e);
202
+ if (n && i !== NOT_PENDING && !hasActiveOverride(e)) {
203
+ return !!(n.oe & REACTIVE_MANUAL_WRITE) || !n.o?.Ce && !(n.h & STATUS_PENDING) || !!(n.h & STATUS_PENDING) && quietPending(n);
137
204
  }
138
205
  // `!comp._loading`: a hold created while the loading window is still open is
139
206
  // the window's own landing in flight to its commit — verdict-quiet like the
140
207
  // rest of the window (the UNINITIALIZED check suppresses exactly this frame
141
208
  // for windowless first loads; born-committed nodes need their own gate, #2990).
142
- if (e._e !== NOT_PENDING && !(t.S & STATUS_UNINITIALIZED) && !t.Ee) {
143
- if (hasActiveOverride(e)) return !e.Ue || !e.Ue(e._e, unwrapOverride(e.De));
144
- return true;
209
+ // A18 (d) for a body-end supersession (#3427): the truth at hand is the
210
+ // COMMITTED value — nothing staged — yet the display still shows the
211
+ // override; pending iff they differ, as for a staged arrival below.
212
+ if (e.C & CONFIG_OVERRIDE_SUPERSEDED && e._e === NOT_PENDING && visibleOverride(e)) return !e.xe || !e.xe(e.ce, unwrapOverride(e.o?.Fe));
213
+ // A28 (2): an unflushed write is not yet observable — the verdict answers
214
+ // for the flushed staged value.
215
+ if (i !== NOT_PENDING && !t.ve) {
216
+ // A18 (d): under a displayed override the observable value is the
217
+ // override, so the verdict is "the arrived truth differs from it" —
218
+ // even before the node's first commit. The UNINITIALIZED suppression
219
+ // below is A19 exception (1), "no observable value exists to be
220
+ // non-final"; an override is one (a node whose first landing was held
221
+ // by a reveal it never got to commit, then superseded under its
222
+ // override, read false here).
223
+ if (visibleOverride(e)) return !e.xe || !e.xe(i, unwrapOverride(e.o?.Fe));
224
+ // A quiet re-ask's held landing still answers the same question: the
225
+ // classification survives the landing (asyncWrite) and dies with the
226
+ // commit (commitPendingNode) — verdict-quiet through the reveal, like
227
+ // the loading window above (#3178).
228
+ // A staged value equal to the committed one is no proposal (A34, #3494): the
229
+ // observable value IS final (A19). The coalesced `setShow(false);
230
+ // setShow(true)` read pending through the flush that carried it — and,
231
+ // stamped into a hold that flush opened, until the hold settled.
232
+ if (!(t.h & STATUS_UNINITIALIZED) && !t.o?.Le && (!e.xe || !e.xe(e.ce, i))) return true;
145
233
  }
146
234
  return newQuestionInFlight(t);
147
235
  }
148
236
 
149
237
  function syncCompanions(e, t) {
150
- if (e.Oe) updatePendingSignal(e);
151
- if (e.ge) setSignal(e.ge, t);
238
+ if (e.o?.Ze) updatePendingSignal(e);
239
+ if (e.o?.He) setSignal(e.o?.He, t);
152
240
  }
153
241
 
154
242
  function updatePendingSignal(e) {
155
- if (e.Oe) {
156
- setSignal(e.Oe, computePendingState(e));
243
+ if (e.o?.Ze) {
244
+ setSignal(e.o?.Ze, computePendingState(e));
157
245
  }
158
- if (e.ge) updatePendingSignal(e.ge);
246
+ if (e.o?.He) updatePendingSignal(e.o?.He);
159
247
  }
160
248
 
161
249
  function updateChildCompanions(e) {
162
- for (let t = e.u; t !== null; t = t.ae) {
163
- if (t.Oe || t.ge) updatePendingSignal(t);
164
- }
250
+ const t = e.o?.Un;
251
+ if (t === undefined) return;
252
+ for (const e of t) updatePendingSignal(e);
165
253
  }
166
254
 
167
255
  /**
@@ -178,31 +266,77 @@ function updateChildCompanions(e) {
178
266
  const visit = e => {
179
267
  if (i.has(e)) return;
180
268
  i.add(e);
181
- if (e.Oe || e.ge) n(e);
182
- for (let t = e.o; t !== null; t = t.ue) visit(t.fe);
183
- for (let t = e.u ?? null; t !== null; t = t.ae) {
269
+ if (e.o?.Ze || e.o?.He) n(e);
270
+ for (let t = e.u; t !== null; t = t.Ae) visit(t.ge);
271
+ for (let t = e.o?.i ?? null; t !== null; t = t.Ue) {
184
272
  visit(t);
185
273
  }
186
274
  };
187
275
  visit(e);
188
276
  }
189
277
 
278
+ /**
279
+ * The correction half of the provisional fresh-read suppression (see
280
+ * collectPending): fired from the sanctioned async-registration site
281
+ * (GlobalQueue.notify) when a transaction gains an in-flight async blocker.
282
+ * Every probe that returned "not pending" purely because it read a held
283
+ * value belonging to that transaction re-runs — its re-probe now sees the
284
+ * live blocker through heldAwaitingAsync and lands the true verdict. The
285
+ * wake mirrors a companion write's own notification (optimistic-dirty on the
286
+ * companion's lane) so the corrected verdict commits and flushes immediately
287
+ * instead of being held with the transaction it reports on.
288
+ */ function wakeSuppressedProbes(e) {
289
+ if (suppressedProbes.size === 0) return;
290
+ let t = false;
291
+ for (const [n, i] of suppressedProbes) {
292
+ const r = n.we;
293
+ const s = r ? currentTransition(r) : null;
294
+ if (!s) {
295
+ suppressedProbes.delete(n);
296
+ continue;
297
+ }
298
+ if (s !== e) continue;
299
+ suppressedProbes.delete(n);
300
+ const o = n.o?.Ze?.o?.me;
301
+ for (const e of i) {
302
+ if (e.oe & REACTIVE_DISPOSED) continue;
303
+ e.oe |= REACTIVE_OPTIMISTIC_DIRTY;
304
+ if (o) assignOrMergeLane(e, o); else if (e.o !== null) e.o.me = undefined;
305
+ enqueueSub(e);
306
+ t = true;
307
+ }
308
+ }
309
+ if (t) schedule();
310
+ }
311
+
190
312
  function snapCompanionsToState(e) {
191
- const t = e.Oe;
192
- if (t && (t.De === undefined || t.De === NOT_PENDING)) {
313
+ suppressedProbes.size !== 0 && suppressedProbes.delete(e);
314
+ const t = e.o?.Ze;
315
+ if (t && (t.o?.Fe === undefined || t.o?.Fe === NOT_PENDING)) {
193
316
  const n = computePendingState(e);
194
- if (t.be !== n || t._e !== NOT_PENDING) {
195
- t.be = n;
317
+ if (t.ce !== n || t._e !== NOT_PENDING) {
318
+ t.ce = n;
196
319
  t._e = NOT_PENDING;
197
- t.de = clock;
198
320
  insertSubs(t);
199
321
  schedule();
200
322
  }
201
323
  }
202
- const n = e.ge;
203
- if (n && !(n.se & REACTIVE_DISPOSED)) {
204
- if ((n.De === undefined || n.De === NOT_PENDING) && n._e === NOT_PENDING && !Object.is(n.be, e.be) && !(n.se & (REACTIVE_DIRTY | REACTIVE_CHECK))) {
205
- n.se |= REACTIVE_DIRTY;
324
+ const n = e.o?.He;
325
+ if (n && !(n.oe & REACTIVE_DISPOSED)) {
326
+ // A leaf whose firewall is disposed (the projection's teardown snaps its
327
+ // companion-bearing leaves): the shadow's compute reads through a
328
+ // disposed, possibly still-pending projection and would sit
329
+ // NotReady/uninitialized forever — never derived, its backfilled override
330
+ // dropped at the settle — against a leaf whose committed value differs
331
+ // (INV-4 at the next quiescence; spec O5). It dies with its source;
332
+ // getLatestValueComputed treats a disposed shadow as absent, so a later
333
+ // read recreates it from the committed view.
334
+ if (e.De?.oe & REACTIVE_DISPOSED) {
335
+ dispose(n);
336
+ return;
337
+ }
338
+ if ((n.o?.Fe === undefined || n.o?.Fe === NOT_PENDING) && n._e === NOT_PENDING && !Object.is(n.ce, e.ce) && !(n.oe & (REACTIVE_DIRTY | REACTIVE_CHECK))) {
339
+ n.oe |= REACTIVE_DIRTY;
206
340
  insertIntoHeap(n, queueFor(n));
207
341
  insertSubs(n);
208
342
  schedule();
@@ -212,87 +346,224 @@ function snapCompanionsToState(e) {
212
346
  }
213
347
 
214
348
  function getLatestValueComputed(e) {
215
- if (!e.ge) {
216
- const t = latestReadActive;
349
+ let t = e.o?.He;
350
+ // A shadow disposed while unobserved (its gated reader unmounted at a
351
+ // landing) is a corpse: sync writes into it equality-swallow against its
352
+ // frozen _value, and a later read revives it via recompute — clearing
353
+ // DISPOSED and re-deriving from the committed view, so the banner showed
354
+ // the previous transition's target (#3041 follow-up). Treat it as absent;
355
+ // recreation backfills from the in-flight write below.
356
+ if (t && t.oe & REACTIVE_DISPOSED) t = undefined;
357
+ if (!t) {
358
+ const n = latestReadActive;
217
359
  setLatestReadActive(false);
218
- const n = pendingCheckActive;
360
+ const i = pendingCheckActive;
219
361
  setPendingCheckActive(false);
220
- const i = context;
362
+ const r = context;
221
363
  setContextInternal(null);
222
364
  // Detach from owner so it isn't disposed with effects
223
- e.ge = optimisticComputed(() => read(e));
224
- e.ge.nn = e;
365
+ GlobalQueue.Xn = true;
366
+ try {
367
+ t = optimisticComputed(() => read(e), {
368
+ ownedWrite: true
369
+ });
370
+ } finally {
371
+ GlobalQueue.Xn = false;
372
+ }
373
+ ext(e).He = t;
374
+ e.C |= CONFIG_HAS_COMPANIONS;
375
+ markFirewallChildCompanions(e);
376
+ ext(t).kn = e;
225
377
  // Parent-child lane relationship
226
- setContextInternal(i);
227
- setPendingCheckActive(n);
228
- setLatestReadActive(t);
378
+ // Backfill an in-flight write (mirrors getPendingSignal): the companion is
379
+ // created lazily, possibly after the write was processed — syncCompanions
380
+ // only pushes into companions that already exist, so the first latest()
381
+ // read inside a held transition showed the committed value (#3041).
382
+ const s = flushedStaged(e);
383
+ if (s !== NOT_PENDING && !hasActiveOverride(e)) backfillCompanion(e, t, s);
384
+ joinUnflushedResync(e);
385
+ setContextInternal(r);
386
+ setPendingCheckActive(i);
387
+ setLatestReadActive(n);
229
388
  }
230
- return e.ge;
389
+ return t;
231
390
  }
232
391
 
233
- /** The latest()-mode read path, installed as GlobalQueue._latestRead. */ function latestRead(e) {
392
+ /** The latest()-mode read path, installed as GlobalQueue._latestRead. */
393
+ /** A7: the source has no visible value yet — judged on the OWNER, as read()
394
+ * does: a store leaf behind a projection's firewall is a plain signal whose
395
+ * `_value` is the seed (A25: a draft, never a value), and read() routes a
396
+ * latest() read here before its own firewall/status logic. An override
397
+ * displays a value even before the first commit (A17). */ function uninitializedSource(e) {
398
+ const t = e.De || e;
399
+ return !!(t.h & STATUS_UNINITIALIZED) && !hasActiveOverride(e);
400
+ }
401
+
402
+ function latestRead(e) {
403
+ // A leaf of a DISPOSED projection has no flushed world left to mirror: a
404
+ // shadow created for it now would read through the dead firewall, sit
405
+ // NotReady/uninitialized forever, and no teardown would ever retire it (the
406
+ // firewall's already ran — spec O5). Serve the committed value; create
407
+ // nothing. (A read of a disposed node freezes at its last commit, #3024.)
408
+ if (e.De?.oe & REACTIVE_DISPOSED) return e.ce;
234
409
  const t = getLatestValueComputed(e);
235
410
  const n = latestReadActive;
236
411
  setLatestReadActive(false);
237
- const i = e.De !== undefined && e.De !== NOT_PENDING ? unwrapOverride(e.De) : e.be;
238
- let r;
239
- try {
240
- // An untracked latest() read has no reading context, so read() never
241
- // performs its mid-tick pull — a plain write queued between two latest()
242
- // calls left a still-subscribed shadow at its previous speculative value
243
- // until the flush (#2922). Mirror the tracked-read pull here: mark the
244
- // queued staleness through the graph, then bring the shadow up to date.
245
- const e = queueFor(t);
246
- if (t.Le >= e.Ve && !(t.se & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
247
- markHeap(e);
248
- prepareComputed(t, true);
412
+ const i = visibleOverride(e) ? unwrapOverride(e.o?.Fe) : e.ce;
413
+ // A28: an unflushed write is not the staged value latest() serves. The
414
+ // shadow was written at the source's write to mirror it (A8) — consult it
415
+ // only once a flush has carried the write.
416
+ const r = unflushedValue(e);
417
+ if (r !== NOT_PENDING) {
418
+ // The reader derived from the flushed world because of an unflushed
419
+ // write: it runs again in the flush that carries it (late linker) —
420
+ // a pull may have cleared the mark the write's walk set.
421
+ if (context !== null && context.oe & REACTIVE_RECOMPUTING_DEPS) markLateLinker(context);
422
+ // Link the reader to the shadow so the flush that carries the write
423
+ // updates it (the shadow itself already mirrors the write, A8).
424
+ try {
425
+ read(t);
426
+ } catch {
427
+ /* the flushed value answers */} finally {
428
+ setLatestReadActive(n);
249
429
  }
250
- r = read(t);
430
+ // An ambient write: the visible value (override or committed). A rewrite
431
+ // of a held node: the staged value the last flush left.
432
+ return e.we === null ? i : r;
433
+ }
434
+ let s;
435
+ try {
436
+ // No mid-tick pull: writes become visible at flush (A28), so before the
437
+ // flush the shadow is exactly as current as the flushed world — the read
438
+ // below serves it as is. (#2922's pull, which brought the shadow current
439
+ // against the unflushed write, is superseded: `flush()` first to read
440
+ // your own write.)
441
+ s = read(t);
251
442
  } catch (t) {
252
- if (t instanceof NotReadyError && (!context || !(e.S & STATUS_UNINITIALIZED))) return i;
443
+ // A NotReady from the shadow of an INITIALIZED source means the shadow
444
+ // is mid-flight: serve the visible (committed / override) value. An
445
+ // uninitialized source has no visible value — latest() throws in every
446
+ // scope rather than fabricate `undefined` for a `T` that excludes it
447
+ // (A7; the unowned scope used to return undefined here).
448
+ if (t instanceof NotReadyError && !uninitializedSource(e)) return i;
253
449
  throw t;
254
450
  } finally {
255
451
  setLatestReadActive(n);
256
452
  }
257
- if (t.S & STATUS_PENDING) return i;
258
- if (stale && currentOptimisticLane && t.Ke) {
259
- const e = findLane(t.Ke);
260
- const n = findLane(currentOptimisticLane);
261
- if (e !== n && e.Ae.size > 0) {
262
- return i;
263
- }
453
+ if (t.h & STATUS_PENDING) {
454
+ if (uninitializedSource(e)) throw new NotReadyError(e);
455
+ return i;
264
456
  }
457
+ // A render effect off the shadow's HELD lane sees the committed value and
458
+ // re-runs at the release (#3460; lanes mirror transitions — see
459
+ // readsHeldCommitted). Was: only a reader under ANOTHER lane; a mainline
460
+ // reader, mounted or re-run by a sync write mid-hold, showed the
461
+ // speculative value beside the lane's deferred readers.
462
+ if (stale && context !== null && readsHeldCommitted(t, context)) return e.ce;
265
463
  // A shadow recomputed by the pull above (not at creation) holds its fresh
266
464
  // speculative value in _pendingValue; a contextless read() only surfaces
267
465
  // _value. Overrides stay authoritative (A17), and stale readers keep the
268
466
  // other transition's committed view, matching read()'s own selection.
269
- if (t._e !== NOT_PENDING && !hasActiveOverride(t) && !(stale && t.Ne && activeTransition !== t.Ne)) return t._e;
270
- return r;
467
+ if (t._e !== NOT_PENDING && !hasActiveOverride(t) && !(stale && t.we && activeTransition !== t.we)) return t._e;
468
+ return s;
469
+ }
470
+
471
+ /**
472
+ * A latest() shadow that is uninitialized only because it was CREATED during
473
+ * an active flight — its parent source already has a committed value, so
474
+ * latest() serves that as the visible value and a tracked reader has
475
+ * something to pair a verdict with (#3166). Same parent resolution as
476
+ * computePendingState. The pending signal companion also carries
477
+ * `_parentSource` but is a plain signal (no `_fn`) that never goes pending,
478
+ * so the `_fn` check is belt-and-braces for this call site.
479
+ */ function latestShadowWithInitializedParent(e) {
480
+ if (typeof e.he !== "function") return false;
481
+ const t = e.o?.kn;
482
+ if (t === undefined) return false;
483
+ const n = t.De || t;
484
+ return !(n.h & STATUS_UNINITIALIZED);
271
485
  }
272
486
 
273
487
  /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */ function pendingCheckRead(e, t, n, i) {
274
488
  setPendingCheckActive(false);
275
- if (typeof e.ce === "function") prepareComputed(e, true);
276
- const r = n.S;
277
- if (t && r & STATUS_PENDING && r & STATUS_UNINITIALIZED) {
489
+ if (typeof e.he === "function") {
490
+ GlobalQueue.Xn = true;
491
+ try {
492
+ prepareComputed(e, true);
493
+ } finally {
494
+ GlobalQueue.Xn = false;
495
+ }
496
+ }
497
+ const r = n.h;
498
+ if (t && r & STATUS_PENDING && r & STATUS_UNINITIALIZED &&
499
+ // The suspend-throw is for a genuinely-first-load source: the tracked
500
+ // reader has nothing to pair a verdict with, so it parks on the source.
501
+ // A latest() SHADOW created lazily mid-flight is born uninitialized even
502
+ // though its parent has a committed value latest() will serve — throwing
503
+ // here (swallowed by latestRead's fallback) dropped the shadow from the
504
+ // probe, so a tracked latest(isPending()) probe created during a
505
+ // new-question flight cached `false` for that whole flight (#3166).
506
+ // Defer to the PARENT's initialization state and fall through to normal
507
+ // collection; the plain pending throw downstream still links the reader.
508
+ !latestShadowWithInitializedParent(n)) {
278
509
  if (tracking && e !== t) link(e, t);
279
510
  setPendingCheckActive(true);
280
- throw n._;
511
+ throw n.o?._;
281
512
  }
282
513
  collectPendingSources(e);
283
514
  if (i) collectPendingSources(i);
284
515
  setPendingCheckActive(true);
285
516
  }
286
517
 
518
+ /**
519
+ * A held node whose transaction still has an async question in flight. The
520
+ * probe's fresh-read pairing rule (#2831 — "a reader that sees the fresh
521
+ * value must not also be told it is pending") only applies to LANDED answers
522
+ * awaiting reveal; while the answer is still computing, the fresh value the
523
+ * reader saw is an input, and pending remains the truth for every reader
524
+ * (#3028).
525
+ */ function heldAwaitingAsync(e) {
526
+ const t = e.we;
527
+ const n = t ? currentTransition(t) : activeTransition;
528
+ if (!n || n.Rn) return false;
529
+ // A plain staged write (a signal/store leaf — no _fn) held while an action
530
+ // is still running is an INPUT to a computation still in flight (#3078):
531
+ // the pairing rule must not suppress the verdict, or a memo recomputing
532
+ // mid-action reads the staged value, gets told "not pending", and
533
+ // disagrees with a direct isPending() probe for the whole action window.
534
+ // A computed's staged value is the opposite case — a LANDED answer
535
+ // awaiting reveal — where the pairing rule stands even inside an open
536
+ // action (#2831: a reader that saw the new value must not also see
537
+ // pending); still-computing answers are covered by the reporter scan.
538
+ if (n.pe.length && !e.he) return true;
539
+ // The reporter scan runs for an unstamped node too (#3457): a node staged
540
+ // AFTER the transaction opened is pushed straight into the transaction's
541
+ // batch (queuePendingNode, once initTransition adopted it) and only gets
542
+ // its `_transition` stamp when the flush stashes the hold, but its staged
543
+ // value is already the transaction's, and `t` resolved to that very
544
+ // transaction above. Gating on the stamp let a memo whose recompute read
545
+ // a sync memo's fresh staged value mid-flush pair "not pending" with it
546
+ // (A10) while the transaction's async source was still computing, so a
547
+ // memo-wrapped isPending() read false where a direct probe read true.
548
+ for (const [e, t] of n.le) {
549
+ if (t.size && e.h & STATUS_PENDING && e.o?._?.source === e) return true;
550
+ }
551
+ return false;
552
+ }
553
+
287
554
  function recordFreshRead(e, t) {
288
- if (pendingProbe !== null && e._e !== NOT_PENDING && t === e._e) pendingProbe.freshReads.add(e);
555
+ if (pendingProbe !== null && e._e !== NOT_PENDING && t === e._e) {
556
+ if (heldAwaitingAsync(e)) return;
557
+ pendingProbe.freshReads.add(e);
558
+ }
289
559
  }
290
560
 
291
561
  function applyReask(e, t) {
292
- const n = !!(e.S & STATUS_PENDING);
293
- const i = t && !(n && !e.pe);
294
- const r = n && e.pe !== i;
295
- e.pe = i;
562
+ const n = !!(e.h & STATUS_PENDING);
563
+ const i = t && !(n && !e.o?.Le);
564
+ const r = n && (e.o?.Le ?? false) !== i;
565
+ // Allocation-free for the quiet case: false is the extension default.
566
+ if (i) ext(e).Le = true; else if (e.o !== null) e.o.Le = false;
296
567
  return r;
297
568
  }
298
569
 
@@ -313,17 +584,49 @@ function isPending(e) {
313
584
  const i = pendingProbe = {
314
585
  found: false,
315
586
  sources: new Set,
316
- freshReads: new Set
587
+ freshReads: new Set,
588
+ suppressed: []
317
589
  };
318
590
  const collectPending = () => {
319
591
  setPendingCheckActive(false);
592
+ // Companion reads are mode-neutral plumbing: under an outer latest()
593
+ // (isPending inside a latest window — #3104's memo shape) leaving latest
594
+ // mode active dispatched these reads through latestRead, which built a
595
+ // SHADOW OF THE PENDING SIGNAL itself. The next updatePendingSignal then
596
+ // wrote that companion-on-companion from inside a recompute
597
+ // (syncCompanions → setSignal on a shadow created without ownedWrite)
598
+ // and halted dev with the owned-scope write guard. The creation paths
599
+ // (getLatestValueComputed / getPendingSignal) already suspend both
600
+ // modes; this read site must too.
601
+ const e = latestReadActive;
602
+ setLatestReadActive(false);
320
603
  try {
321
604
  i.sources.forEach(e => {
322
- if (read(getPendingSignal(e)) && !i.freshReads.has(e)) i.found = true;
605
+ if (read(getPendingSignal(e))) {
606
+ if (!i.freshReads.has(e)) i.found = true; else i.suppressed.push(e);
607
+ }
323
608
  });
324
609
  } finally {
610
+ setLatestReadActive(e);
325
611
  setPendingCheckActive(true);
326
612
  }
613
+ // A "not pending" verdict that exists only because this reader saw the
614
+ // fresh held value is provisional: if the write turns out NOT to commit
615
+ // this flush (a downstream async pends and holds it), the suppression was
616
+ // wrong and the wrapper must re-ask (#3028). Remember who to wake — the
617
+ // async registration (GlobalQueue.notify) triggers wakeSuppressedProbes.
618
+ // A TRACKED probe only (#3648): an `untrack(() => isPending(x))` inside a
619
+ // memo asked for a one-shot answer and declined the re-run its tracked
620
+ // form would get — enrolling its host anyway marked the memo OPT-dirty on
621
+ // the companion's lane and re-ran it (a router `query()` refetched, its
622
+ // first flight abandoned mid-air) for a verdict it never depended on.
623
+ if (tracking && !i.found && i.suppressed.length && context && typeof context.he === "function") {
624
+ for (const e of i.suppressed) {
625
+ let t = suppressedProbes.get(e);
626
+ if (!t) suppressedProbes.set(e, t = new Set);
627
+ t.add(context);
628
+ }
629
+ }
327
630
  };
328
631
  try {
329
632
  e();
@@ -332,7 +635,7 @@ function isPending(e) {
332
635
  } catch (e) {
333
636
  collectPending();
334
637
  if (e instanceof NotReadyError) {
335
- const t = !!(e.source?.S & STATUS_UNINITIALIZED);
638
+ const t = !!(e.source?.h & STATUS_UNINITIALIZED);
336
639
  if (i.found && !t) return true;
337
640
  if (context && t) throw e;
338
641
  }
@@ -346,24 +649,26 @@ function isPending(e) {
346
649
  // Hook installation (same late-binding pattern as GlobalQueue._update /
347
650
  // _propagateAffects): core call sites fire these behind the same guards the
348
651
  // direct calls used, so behavior is identical once this module loads.
349
- GlobalQueue.Pe = syncCompanions;
652
+ GlobalQueue.Ve = syncCompanions;
350
653
 
351
- GlobalQueue.Se = updatePendingSignal;
654
+ GlobalQueue.Ge = updatePendingSignal;
352
655
 
353
- GlobalQueue.Ge = updateChildCompanions;
656
+ GlobalQueue.We = updateChildCompanions;
354
657
 
355
- GlobalQueue.un = snapCompanionsToState;
658
+ GlobalQueue.ti = snapCompanionsToState;
356
659
 
357
- GlobalQueue.At = latestRead;
660
+ GlobalQueue.et = latestRead;
358
661
 
359
- GlobalQueue.Ct = pendingCheckRead;
662
+ GlobalQueue.nt = pendingCheckRead;
360
663
 
361
- GlobalQueue.Pt = recordFreshRead;
664
+ GlobalQueue.rt = recordFreshRead;
362
665
 
363
- GlobalQueue.$e = applyReask;
666
+ GlobalQueue.On = applyReask;
364
667
 
365
668
  GlobalQueue.k = repollDownstreamVerdicts;
366
669
 
367
- GlobalQueue.Lt = witnessAffects;
670
+ GlobalQueue.Et = witnessAffects;
671
+
672
+ GlobalQueue.Nt = wakeSuppressedProbes;
368
673
 
369
674
  export { isPending, latest };