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

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 (58) hide show
  1. package/dist/dev.js +1779 -383
  2. package/dist/node.cjs +2058 -1359
  3. package/dist/prod/affects.js +16 -16
  4. package/dist/prod/boundaries.js +90 -90
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +74 -72
  7. package/dist/prod/core/constants.js +39 -1
  8. package/dist/prod/core/core.js +332 -224
  9. package/dist/prod/core/effect.js +56 -56
  10. package/dist/prod/core/external.js +4 -4
  11. package/dist/prod/core/graph.js +65 -54
  12. package/dist/prod/core/heap.js +52 -44
  13. package/dist/prod/core/invariants.js +3 -2
  14. package/dist/prod/core/lanes.js +41 -34
  15. package/dist/prod/core/optimistic.js +93 -68
  16. package/dist/prod/core/owner.js +97 -96
  17. package/dist/prod/core/scheduler.js +243 -194
  18. package/dist/prod/core/verdict.js +191 -77
  19. package/dist/prod/map.js +104 -104
  20. package/dist/prod/signals.js +1 -1
  21. package/dist/prod/store/next/optimistic.js +31 -23
  22. package/dist/prod/store/next/projection.js +109 -47
  23. package/dist/prod/store/next/reconcile.js +78 -74
  24. package/dist/prod/store/next/store.js +357 -90
  25. package/dist/prod/store/store.js +7 -7
  26. package/dist/types/core/attribution-hooks.d.ts +52 -0
  27. package/dist/types/core/attribution.d.ts +186 -0
  28. package/dist/types/core/constants.d.ts +26 -0
  29. package/dist/types/core/core.d.ts +8 -1
  30. package/dist/types/core/dev.d.ts +20 -3
  31. package/dist/types/core/graph.d.ts +1 -0
  32. package/dist/types/core/invariants.d.ts +1 -1
  33. package/dist/types/core/lanes.d.ts +4 -16
  34. package/dist/types/core/scheduler.d.ts +8 -0
  35. package/dist/types/core/types.d.ts +85 -41
  36. package/dist/types/store/next/projection.d.ts +0 -16
  37. package/dist/types/store/next/store.d.ts +6 -0
  38. package/dist/types/store/next/target.d.ts +18 -0
  39. package/dist/types-cjs/core/attribution-hooks.d.cts +52 -0
  40. package/dist/types-cjs/core/attribution.d.cts +186 -0
  41. package/dist/types-cjs/core/constants.d.cts +26 -0
  42. package/dist/types-cjs/core/core.d.cts +8 -1
  43. package/dist/types-cjs/core/dev.d.cts +20 -3
  44. package/dist/types-cjs/core/graph.d.cts +1 -0
  45. package/dist/types-cjs/core/invariants.d.cts +1 -1
  46. package/dist/types-cjs/core/lanes.d.cts +4 -16
  47. package/dist/types-cjs/core/scheduler.d.cts +8 -0
  48. package/dist/types-cjs/core/types.d.cts +85 -41
  49. package/dist/types-cjs/store/next/projection.d.cts +0 -16
  50. package/dist/types-cjs/store/next/store.d.cts +6 -0
  51. package/dist/types-cjs/store/next/target.d.cts +18 -0
  52. package/package.json +15 -15
  53. package/dist/types/store/optimistic.d.ts +0 -45
  54. package/dist/types/store/projection.d.ts +0 -70
  55. package/dist/types/store/reconcile.d.ts +0 -46
  56. package/dist/types-cjs/store/optimistic.d.cts +0 -45
  57. package/dist/types-cjs/store/projection.d.cts +0 -70
  58. package/dist/types-cjs/store/reconcile.d.cts +0 -46
@@ -1,20 +1,20 @@
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_ZOMBIE, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_MANUAL_WRITE, CONFIG_HAS_COMPANIONS, CONFIG_CHILD_COMPANIONS, STATUS_ERROR, REACTIVE_RECOMPUTING_DEPS } 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, prepareComputed, read, context, stale, currentOptimisticLane, tracking, ext, setLatestReadActive, setContextInternal, optimisticComputed, 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 { insertIntoHeap, queueFor, markHeap, enqueueSub } from "./heap.js";
10
10
 
11
11
  import "./invariants.js";
12
12
 
13
- import { findLane, hasActiveOverride } from "./lanes.js";
13
+ import { findLane, hasActiveOverride, assignOrMergeLane } from "./lanes.js";
14
14
 
15
15
  import { installOptimisticEngine } from "./optimistic.js";
16
16
 
17
- import { GlobalQueue, clock, insertSubs, schedule, activeTransition, activeAffectsMarks } from "./scheduler.js";
17
+ import { GlobalQueue, insertSubs, schedule, activeTransition, currentTransition, activeAffectsMarks } from "./scheduler.js";
18
18
 
19
19
  /**
20
20
  * The isPending()/latest() verdict layer, moved out of core.ts. Importing this
@@ -28,19 +28,45 @@ installOptimisticEngine();
28
28
 
29
29
  let pendingProbe = null;
30
30
 
31
+ /**
32
+ * Probes whose verdict was suppressed by the fresh-read pairing rule while
33
+ * the held write's fate was still undecided (see recordFreshRead /
34
+ * wakeSuppressedProbes): held node → the wrapper computeds that probed it.
35
+ * Entries die with the hold — the commit/revert snap clears them.
36
+ */ const suppressedProbes = new Map;
37
+
31
38
  /**
32
39
  * Get or create the pending signal for a node (lazy).
33
40
  * Used by isPending() to track pending state reactively.
34
- */ function getPendingSignal(e) {
35
- if (!e.Oe) {
41
+ */
42
+ /** #3038: register a companion-carrying firewall child on its firewall's
43
+ * companion set and arm the post-recompute snap (CONFIG_CHILD_COMPANIONS is
44
+ * the one-load gate at the call sites). The snap then iterates exactly the
45
+ * children someone asked verdicts of — O(companions) — never the full
46
+ * `_child` chain, which carries one node per materialized leaf (the
47
+ * O(all-leaves-ever-read)-per-update pathology). Entries are permanent like
48
+ * the companions themselves; a store with no leaf-level isPending()/latest()
49
+ * reads never allocates the set or pays the walk. */ function markFirewallChildCompanions(e) {
50
+ const t = e.lt;
51
+ if (!t) return;
52
+ t.T |= CONFIG_CHILD_COMPANIONS;
53
+ (ext(t).Nt ??= new Set).add(e);
54
+ }
55
+
56
+ function getPendingSignal(e) {
57
+ let t = e.o?.Ge;
58
+ if (!t) {
36
59
  // Start false, write true if pending - ensures reversion returns to false
37
- e.Oe = optimisticSignal(false, {
60
+ t = optimisticSignal(false, {
38
61
  ownedWrite: true
39
62
  });
40
- e.Oe.nn = e;
41
- if (computePendingState(e)) setSignal(e.Oe, true);
63
+ ext(e).Ge = t;
64
+ e.T |= CONFIG_HAS_COMPANIONS;
65
+ markFirewallChildCompanions(e);
66
+ ext(t).It = e;
67
+ if (computePendingState(e)) setSignal(t, true);
42
68
  }
43
- return e.Oe;
69
+ return t;
44
70
  }
45
71
 
46
72
  function collectPendingSources(e) {
@@ -70,7 +96,7 @@ function collectPendingSources(e) {
70
96
  * (`_pendingObserver`) are skipped so an `isPending` wrapper memo never
71
97
  * inherits the coverage it reports on.
72
98
  */ function markWalk(e, t) {
73
- if (e.t) return true;
99
+ if (e.o?.t) return true;
74
100
  // A real error outranks an inherited mark (A16/A24c): an errored node
75
101
  // answers probes with its error, not a coverage verdict, and coverage does
76
102
  // not flow through it — matching the rails' behavior, where propagation
@@ -86,10 +112,10 @@ function collectPendingSources(e) {
86
112
  // pass's dependency set — walking past it would read dropped deps and
87
113
  // latch a stale verdict on the companion.
88
114
  const i = e;
89
- const r = i.se & REACTIVE_RECOMPUTING_DEPS ? i.Ye : undefined;
115
+ const r = i.ie & REACTIVE_RECOMPUTING_DEPS ? i.Ye : undefined;
90
116
  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;
117
+ for (let e = i.nt ?? null; e !== null; e = e.it) {
118
+ if (!e.me && markWalk(e.ut, t)) return true;
93
119
  if (e === r) break;
94
120
  }
95
121
  }
@@ -101,11 +127,11 @@ function collectPendingSources(e) {
101
127
  }
102
128
 
103
129
  function quietPending(e) {
104
- if (e.oe) {
105
- for (const t of e.oe) if (!t.pe) return false;
130
+ if (e.o?.le) {
131
+ for (const t of e.o.le) if (!t.o?.pe) return false;
106
132
  return true;
107
133
  }
108
- return e.pe;
134
+ return e.o?.pe ?? false;
109
135
  }
110
136
 
111
137
  // NOTE: a loadingValue node's open loading window (_loading) is verdict-quiet
@@ -121,47 +147,47 @@ function newQuestionInFlight(e) {
121
147
 
122
148
  function computePendingState(e) {
123
149
  const t = e;
124
- if (t.se & REACTIVE_DISPOSED) return false;
150
+ if (t.ie & REACTIVE_DISPOSED) return false;
125
151
  // Mark coverage is transitive by dep-graph reachability: a latest() shadow
126
152
  // reaches its owner (and a store leaf its firewall) through its own deps,
127
153
  // so the one walk covers direct marks, derivation, and companion chains.
128
154
  if (markCovered(e)) return true;
129
155
  const n = e.lt;
130
- if (e.nn) {
131
- const t = e.nn;
156
+ if (e.o?.It) {
157
+ const t = e.o?.It;
132
158
  const n = t.lt || t;
133
159
  return newQuestionInFlight(n);
134
160
  }
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);
161
+ if (n && e.Pe !== NOT_PENDING && !hasActiveOverride(e)) {
162
+ return !!(n.ie & REACTIVE_MANUAL_WRITE) || !n.o?.Ee && !(n.S & STATUS_PENDING) || !!(n.S & STATUS_PENDING) && quietPending(n);
137
163
  }
138
164
  // `!comp._loading`: a hold created while the loading window is still open is
139
165
  // the window's own landing in flight to its commit — verdict-quiet like the
140
166
  // rest of the window (the UNINITIALIZED check suppresses exactly this frame
141
167
  // 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));
168
+ if (e.Pe !== NOT_PENDING && !(t.S & STATUS_UNINITIALIZED) && !t.Ie) {
169
+ if (hasActiveOverride(e)) return !e.Ue || !e.Ue(e.Pe, unwrapOverride(e.o?.De));
144
170
  return true;
145
171
  }
146
172
  return newQuestionInFlight(t);
147
173
  }
148
174
 
149
175
  function syncCompanions(e, t) {
150
- if (e.Oe) updatePendingSignal(e);
151
- if (e.ge) setSignal(e.ge, t);
176
+ if (e.o?.Ge) updatePendingSignal(e);
177
+ if (e.o?.ge) setSignal(e.o?.ge, t);
152
178
  }
153
179
 
154
180
  function updatePendingSignal(e) {
155
- if (e.Oe) {
156
- setSignal(e.Oe, computePendingState(e));
181
+ if (e.o?.Ge) {
182
+ setSignal(e.o?.Ge, computePendingState(e));
157
183
  }
158
- if (e.ge) updatePendingSignal(e.ge);
184
+ if (e.o?.ge) updatePendingSignal(e.o?.ge);
159
185
  }
160
186
 
161
187
  function updateChildCompanions(e) {
162
- for (let t = e.u; t !== null; t = t.ae) {
163
- if (t.Oe || t.ge) updatePendingSignal(t);
164
- }
188
+ const t = e.o?.Nt;
189
+ if (t === undefined) return;
190
+ for (const e of t) updatePendingSignal(e);
165
191
  }
166
192
 
167
193
  /**
@@ -178,31 +204,65 @@ function updateChildCompanions(e) {
178
204
  const visit = e => {
179
205
  if (i.has(e)) return;
180
206
  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) {
207
+ if (e.o?.Ge || e.o?.ge) n(e);
208
+ for (let t = e.u; t !== null; t = t.fe) visit(t.ae);
209
+ for (let t = e.o?.l ?? null; t !== null; t = t.ce) {
184
210
  visit(t);
185
211
  }
186
212
  };
187
213
  visit(e);
188
214
  }
189
215
 
216
+ /**
217
+ * The correction half of the provisional fresh-read suppression (see
218
+ * collectPending): fired from the sanctioned async-registration site
219
+ * (GlobalQueue.notify) when a transaction gains an in-flight async blocker.
220
+ * Every probe that returned "not pending" purely because it read a held
221
+ * value belonging to that transaction re-runs — its re-probe now sees the
222
+ * live blocker through heldAwaitingAsync and lands the true verdict. The
223
+ * wake mirrors a companion write's own notification (optimistic-dirty on the
224
+ * companion's lane) so the corrected verdict commits and flushes immediately
225
+ * instead of being held with the transaction it reports on.
226
+ */ function wakeSuppressedProbes(e) {
227
+ if (suppressedProbes.size === 0) return;
228
+ let t = false;
229
+ for (const [n, i] of suppressedProbes) {
230
+ const r = n._e;
231
+ const s = r ? currentTransition(r) : null;
232
+ if (!s) {
233
+ suppressedProbes.delete(n);
234
+ continue;
235
+ }
236
+ if (s !== e) continue;
237
+ suppressedProbes.delete(n);
238
+ const o = n.o?.Ge?.o?.Be;
239
+ for (const e of i) {
240
+ if (e.ie & REACTIVE_DISPOSED) continue;
241
+ e.ie |= REACTIVE_OPTIMISTIC_DIRTY;
242
+ if (o) assignOrMergeLane(e, o); else if (e.o !== null) e.o.Be = undefined;
243
+ enqueueSub(e);
244
+ t = true;
245
+ }
246
+ }
247
+ if (t) schedule();
248
+ }
249
+
190
250
  function snapCompanionsToState(e) {
191
- const t = e.Oe;
192
- if (t && (t.De === undefined || t.De === NOT_PENDING)) {
251
+ suppressedProbes.size !== 0 && suppressedProbes.delete(e);
252
+ const t = e.o?.Ge;
253
+ if (t && (t.o?.De === undefined || t.o?.De === NOT_PENDING)) {
193
254
  const n = computePendingState(e);
194
- if (t.be !== n || t._e !== NOT_PENDING) {
255
+ if (t.be !== n || t.Pe !== NOT_PENDING) {
195
256
  t.be = n;
196
- t._e = NOT_PENDING;
197
- t.de = clock;
257
+ t.Pe = NOT_PENDING;
198
258
  insertSubs(t);
199
259
  schedule();
200
260
  }
201
261
  }
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;
262
+ const n = e.o?.ge;
263
+ if (n && !(n.ie & REACTIVE_DISPOSED)) {
264
+ if ((n.o?.De === undefined || n.o?.De === NOT_PENDING) && n.Pe === NOT_PENDING && !Object.is(n.be, e.be) && !(n.ie & (REACTIVE_DIRTY | REACTIVE_CHECK))) {
265
+ n.ie |= REACTIVE_DIRTY;
206
266
  insertIntoHeap(n, queueFor(n));
207
267
  insertSubs(n);
208
268
  schedule();
@@ -212,29 +272,45 @@ function snapCompanionsToState(e) {
212
272
  }
213
273
 
214
274
  function getLatestValueComputed(e) {
215
- if (!e.ge) {
216
- const t = latestReadActive;
275
+ let t = e.o?.ge;
276
+ // A shadow disposed while unobserved (its gated reader unmounted at a
277
+ // landing) is a corpse: sync writes into it equality-swallow against its
278
+ // frozen _value, and a later read revives it via recompute — clearing
279
+ // DISPOSED and re-deriving from the committed view, so the banner showed
280
+ // the previous transition's target (#3041 follow-up). Treat it as absent;
281
+ // recreation backfills from the in-flight write below.
282
+ if (t && t.ie & REACTIVE_DISPOSED) t = undefined;
283
+ if (!t) {
284
+ const n = latestReadActive;
217
285
  setLatestReadActive(false);
218
- const n = pendingCheckActive;
286
+ const i = pendingCheckActive;
219
287
  setPendingCheckActive(false);
220
- const i = context;
288
+ const r = context;
221
289
  setContextInternal(null);
222
290
  // Detach from owner so it isn't disposed with effects
223
- e.ge = optimisticComputed(() => read(e));
224
- e.ge.nn = e;
291
+ t = optimisticComputed(() => read(e));
292
+ ext(e).ge = t;
293
+ e.T |= CONFIG_HAS_COMPANIONS;
294
+ markFirewallChildCompanions(e);
295
+ ext(t).It = e;
225
296
  // Parent-child lane relationship
226
- setContextInternal(i);
227
- setPendingCheckActive(n);
228
- setLatestReadActive(t);
297
+ // Backfill an in-flight write (mirrors getPendingSignal): the companion is
298
+ // created lazily, possibly after the write was processed — syncCompanions
299
+ // only pushes into companions that already exist, so the first latest()
300
+ // read inside a held transition showed the committed value (#3041).
301
+ if (e.Pe !== NOT_PENDING && !hasActiveOverride(e)) setSignal(t, e.Pe);
302
+ setContextInternal(r);
303
+ setPendingCheckActive(i);
304
+ setLatestReadActive(n);
229
305
  }
230
- return e.ge;
306
+ return t;
231
307
  }
232
308
 
233
309
  /** The latest()-mode read path, installed as GlobalQueue._latestRead. */ function latestRead(e) {
234
310
  const t = getLatestValueComputed(e);
235
311
  const n = latestReadActive;
236
312
  setLatestReadActive(false);
237
- const i = e.De !== undefined && e.De !== NOT_PENDING ? unwrapOverride(e.De) : e.be;
313
+ const i = e.o?.De !== undefined && e.o?.De !== NOT_PENDING ? unwrapOverride(e.o?.De) : e.be;
238
314
  let r;
239
315
  try {
240
316
  // An untracked latest() read has no reading context, so read() never
@@ -243,7 +319,7 @@ function getLatestValueComputed(e) {
243
319
  // until the flush (#2922). Mirror the tracked-read pull here: mark the
244
320
  // queued staleness through the graph, then bring the shadow up to date.
245
321
  const e = queueFor(t);
246
- if (t.Le >= e.Ve && !(t.se & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
322
+ if (t.Le >= e.xe && !(t.ie & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
247
323
  markHeap(e);
248
324
  prepareComputed(t, true);
249
325
  }
@@ -255,8 +331,8 @@ function getLatestValueComputed(e) {
255
331
  setLatestReadActive(n);
256
332
  }
257
333
  if (t.S & STATUS_PENDING) return i;
258
- if (stale && currentOptimisticLane && t.Ke) {
259
- const e = findLane(t.Ke);
334
+ if (stale && currentOptimisticLane && t.o?.Be) {
335
+ const e = findLane(t.o?.Be);
260
336
  const n = findLane(currentOptimisticLane);
261
337
  if (e !== n && e.Ae.size > 0) {
262
338
  return i;
@@ -266,33 +342,54 @@ function getLatestValueComputed(e) {
266
342
  // speculative value in _pendingValue; a contextless read() only surfaces
267
343
  // _value. Overrides stay authoritative (A17), and stale readers keep the
268
344
  // 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;
345
+ if (t.Pe !== NOT_PENDING && !hasActiveOverride(t) && !(stale && t._e && activeTransition !== t._e)) return t.Pe;
270
346
  return r;
271
347
  }
272
348
 
273
349
  /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */ function pendingCheckRead(e, t, n, i) {
274
350
  setPendingCheckActive(false);
275
- if (typeof e.ce === "function") prepareComputed(e, true);
351
+ if (typeof e.Se === "function") prepareComputed(e, true);
276
352
  const r = n.S;
277
353
  if (t && r & STATUS_PENDING && r & STATUS_UNINITIALIZED) {
278
354
  if (tracking && e !== t) link(e, t);
279
355
  setPendingCheckActive(true);
280
- throw n._;
356
+ throw n.o?._;
281
357
  }
282
358
  collectPendingSources(e);
283
359
  if (i) collectPendingSources(i);
284
360
  setPendingCheckActive(true);
285
361
  }
286
362
 
363
+ /**
364
+ * A held node whose transaction still has an async question in flight. The
365
+ * probe's fresh-read pairing rule (#2831 — "a reader that sees the fresh
366
+ * value must not also be told it is pending") only applies to LANDED answers
367
+ * awaiting reveal; while the answer is still computing, the fresh value the
368
+ * reader saw is an input, and pending remains the truth for every reader
369
+ * (#3028).
370
+ */ function heldAwaitingAsync(e) {
371
+ const t = e._e;
372
+ const n = t ? currentTransition(t) : null;
373
+ if (!n || n.an) return false;
374
+ for (const [e, t] of n.Ne) {
375
+ if (t.size && e.S & STATUS_PENDING && e.o?._?.source === e) return true;
376
+ }
377
+ return false;
378
+ }
379
+
287
380
  function recordFreshRead(e, t) {
288
- if (pendingProbe !== null && e._e !== NOT_PENDING && t === e._e) pendingProbe.freshReads.add(e);
381
+ if (pendingProbe !== null && e.Pe !== NOT_PENDING && t === e.Pe) {
382
+ if (heldAwaitingAsync(e)) return;
383
+ pendingProbe.freshReads.add(e);
384
+ }
289
385
  }
290
386
 
291
387
  function applyReask(e, t) {
292
388
  const n = !!(e.S & STATUS_PENDING);
293
- const i = t && !(n && !e.pe);
294
- const r = n && e.pe !== i;
295
- e.pe = i;
389
+ const i = t && !(n && !e.o?.pe);
390
+ const r = n && (e.o?.pe ?? false) !== i;
391
+ // Allocation-free for the quiet case: false is the extension default.
392
+ if (i) ext(e).pe = true; else if (e.o !== null) e.o.pe = false;
296
393
  return r;
297
394
  }
298
395
 
@@ -313,17 +410,32 @@ function isPending(e) {
313
410
  const i = pendingProbe = {
314
411
  found: false,
315
412
  sources: new Set,
316
- freshReads: new Set
413
+ freshReads: new Set,
414
+ suppressed: []
317
415
  };
318
416
  const collectPending = () => {
319
417
  setPendingCheckActive(false);
320
418
  try {
321
419
  i.sources.forEach(e => {
322
- if (read(getPendingSignal(e)) && !i.freshReads.has(e)) i.found = true;
420
+ if (read(getPendingSignal(e))) {
421
+ if (!i.freshReads.has(e)) i.found = true; else i.suppressed.push(e);
422
+ }
323
423
  });
324
424
  } finally {
325
425
  setPendingCheckActive(true);
326
426
  }
427
+ // A "not pending" verdict that exists only because this reader saw the
428
+ // fresh held value is provisional: if the write turns out NOT to commit
429
+ // this flush (a downstream async pends and holds it), the suppression was
430
+ // wrong and the wrapper must re-ask (#3028). Remember who to wake — the
431
+ // async registration (GlobalQueue.notify) triggers wakeSuppressedProbes.
432
+ if (!i.found && i.suppressed.length && context && typeof context.Se === "function") {
433
+ for (const e of i.suppressed) {
434
+ let t = suppressedProbes.get(e);
435
+ if (!t) suppressedProbes.set(e, t = new Set);
436
+ t.add(context);
437
+ }
438
+ }
327
439
  };
328
440
  try {
329
441
  e();
@@ -346,24 +458,26 @@ function isPending(e) {
346
458
  // Hook installation (same late-binding pattern as GlobalQueue._update /
347
459
  // _propagateAffects): core call sites fire these behind the same guards the
348
460
  // direct calls used, so behavior is identical once this module loads.
349
- GlobalQueue.Pe = syncCompanions;
461
+ GlobalQueue.Oe = syncCompanions;
350
462
 
351
- GlobalQueue.Se = updatePendingSignal;
463
+ GlobalQueue.de = updatePendingSignal;
352
464
 
353
- GlobalQueue.Ge = updateChildCompanions;
465
+ GlobalQueue.ye = updateChildCompanions;
354
466
 
355
467
  GlobalQueue.un = snapCompanionsToState;
356
468
 
357
- GlobalQueue.At = latestRead;
469
+ GlobalQueue.Pt = latestRead;
470
+
471
+ GlobalQueue.Dt = pendingCheckRead;
358
472
 
359
- GlobalQueue.Ct = pendingCheckRead;
473
+ GlobalQueue.Ht = recordFreshRead;
360
474
 
361
- GlobalQueue.Pt = recordFreshRead;
475
+ GlobalQueue.Je = applyReask;
362
476
 
363
- GlobalQueue.$e = applyReask;
477
+ GlobalQueue.p = repollDownstreamVerdicts;
364
478
 
365
- GlobalQueue.k = repollDownstreamVerdicts;
479
+ GlobalQueue.Bt = witnessAffects;
366
480
 
367
- GlobalQueue.Lt = witnessAffects;
481
+ GlobalQueue.wt = wakeSuppressedProbes;
368
482
 
369
483
  export { isPending, latest };