@solidjs/signals 2.0.0-rc.14 → 2.0.0-rc.16

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 (98) hide show
  1. package/dist/dev-shared.js +882 -405
  2. package/dist/dev.attribution.js +172 -66
  3. package/dist/dev.js +1529 -745
  4. package/dist/observe/affects-marks.js +86 -0
  5. package/dist/observe/affects.js +32 -80
  6. package/dist/observe/boundaries.js +202 -166
  7. package/dist/observe/core/async.js +192 -141
  8. package/dist/observe/core/attribution-queries.js +1 -1
  9. package/dist/observe/core/attribution.js +315 -214
  10. package/dist/observe/core/constants.js +23 -4
  11. package/dist/observe/core/context.js +3 -3
  12. package/dist/observe/core/core.js +532 -360
  13. package/dist/observe/core/dev.js +63 -35
  14. package/dist/observe/core/effect.js +47 -47
  15. package/dist/observe/core/error.js +5 -3
  16. package/dist/observe/core/external.js +5 -6
  17. package/dist/observe/core/graph.js +56 -42
  18. package/dist/observe/core/heap.js +33 -33
  19. package/dist/observe/core/identities.js +62 -0
  20. package/dist/observe/core/lanes.js +242 -229
  21. package/dist/observe/core/owner.js +62 -50
  22. package/dist/observe/core/scheduler.js +322 -168
  23. package/dist/observe/core/truth.js +45 -0
  24. package/dist/observe/core/verdict.js +114 -64
  25. package/dist/observe/deferred.js +184 -0
  26. package/dist/observe/index.js +7 -1
  27. package/dist/observe/map.js +339 -254
  28. package/dist/observe/reveal.js +77 -77
  29. package/dist/observe/signals.js +54 -33
  30. package/dist/observe/store/affects.js +76 -69
  31. package/dist/observe/store/edges.js +39 -0
  32. package/dist/observe/store/hooks.js +13 -0
  33. package/dist/observe/store/index.js +0 -2
  34. package/dist/observe/store/optimistic.js +82 -44
  35. package/dist/observe/store/projection.js +83 -64
  36. package/dist/observe/store/reconcile.js +32 -30
  37. package/dist/observe/store/store.js +219 -123
  38. package/dist/prod/affects-marks.js +86 -0
  39. package/dist/prod/affects.js +33 -79
  40. package/dist/prod/boundaries.js +185 -149
  41. package/dist/prod/core/async.js +176 -135
  42. package/dist/prod/core/constants.js +23 -4
  43. package/dist/prod/core/context.js +3 -3
  44. package/dist/prod/core/core.js +521 -353
  45. package/dist/prod/core/effect.js +66 -66
  46. package/dist/prod/core/error.js +5 -3
  47. package/dist/prod/core/external.js +5 -6
  48. package/dist/prod/core/graph.js +56 -42
  49. package/dist/prod/core/heap.js +38 -38
  50. package/dist/prod/core/identities.js +62 -0
  51. package/dist/prod/core/lanes.js +237 -224
  52. package/dist/prod/core/owner.js +62 -51
  53. package/dist/prod/core/scheduler.js +312 -158
  54. package/dist/prod/core/truth.js +45 -0
  55. package/dist/prod/core/verdict.js +113 -63
  56. package/dist/prod/deferred.js +182 -0
  57. package/dist/prod/index.js +7 -1
  58. package/dist/prod/map.js +366 -281
  59. package/dist/prod/reveal.js +77 -77
  60. package/dist/prod/signals.js +59 -38
  61. package/dist/prod/store/affects.js +76 -69
  62. package/dist/prod/store/edges.js +39 -0
  63. package/dist/prod/store/hooks.js +13 -0
  64. package/dist/prod/store/index.js +0 -2
  65. package/dist/prod/store/optimistic.js +121 -84
  66. package/dist/prod/store/projection.js +46 -34
  67. package/dist/prod/store/reconcile.js +32 -30
  68. package/dist/prod/store/store.js +205 -116
  69. package/dist/types/affects-marks.d.ts +14 -0
  70. package/dist/types/boundaries.d.ts +5 -1
  71. package/dist/types/core/async.d.ts +10 -0
  72. package/dist/types/core/attribution.d.ts +6 -2
  73. package/dist/types/core/constants.d.ts +18 -1
  74. package/dist/types/core/core.d.ts +77 -5
  75. package/dist/types/core/dev.d.ts +43 -13
  76. package/dist/types/core/graph.d.ts +11 -0
  77. package/dist/types/core/identities.d.ts +17 -0
  78. package/dist/types/core/index.d.ts +2 -1
  79. package/dist/types/core/lanes.d.ts +2 -4
  80. package/dist/types/core/owner.d.ts +5 -3
  81. package/dist/types/core/scheduler.d.ts +59 -34
  82. package/dist/types/core/truth.d.ts +16 -0
  83. package/dist/types/core/types.d.ts +21 -6
  84. package/dist/types/core/verdict.d.ts +32 -0
  85. package/dist/types/deferred.d.ts +28 -0
  86. package/dist/types/index.d.ts +6 -0
  87. package/dist/types/map.d.ts +2 -2
  88. package/dist/types/signals.d.ts +28 -7
  89. package/dist/types/store/affects.d.ts +11 -0
  90. package/dist/types/store/edges.d.ts +9 -0
  91. package/dist/types/store/hooks.d.ts +48 -0
  92. package/dist/types/store/index.d.ts +10 -4
  93. package/dist/types/store/optimistic.d.ts +3 -2
  94. package/dist/types/store/projection.d.ts +4 -3
  95. package/dist/types/store/store.d.ts +4 -39
  96. package/dist/types/store/target.d.ts +16 -8
  97. package/dist/types/store/types.d.ts +14 -0
  98. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ import { CONFIG_GUESS, NOT_PENDING, CONFIG_AUTHORITATIVE, CONFIG_OVERRIDE } from "./constants.js";
2
+
3
+ import { NotReadyError } from "./error.js";
4
+
5
+ import { enqueueSub } from "./heap.js";
6
+
7
+ // Truth reads (`until`, `refresh`; CONFIG_AUTHORITATIVE) over lane nodes.
8
+ // Side-effect free and apart from lanes.ts so a truth reader alone does not
9
+ // retain the lane engine: its reads reach these only through `laneRead`,
10
+ // which exists once something installs lanes (`createOptimistic`, verdicts).
11
+ /** A lane node's current value: its guess, or the latest it derived — the
12
+ * staging a shown lane holds for its next reveal, else its slot; the
13
+ * committed value while it has none (its first lane pass in flight). */
14
+ function laneValueOf(r) {
15
+ if (!(r.l & CONFIG_GUESS) && r.ue !== NOT_PENDING) return r.ue;
16
+ const t = r.T.V;
17
+ return t !== NOT_PENDING ? t : r.Ie;
18
+ }
19
+
20
+ /** Truth readers (`until`, `refresh`; CONFIG_AUTHORITATIVE) — pay-for-use:
21
+ * the first one installs these on GlobalQueue (signals.ts `watch`).
22
+ *
23
+ * A truth reader's read of a lane node: the lane's value when it is the
24
+ * truth (#3943), else the truth staged beneath a guess, else the committed
25
+ * value. */ function truthRead(r) {
26
+ if (truth(r)) return laneValueOf(r);
27
+ if (!(r.l & CONFIG_GUESS)) throw new NotReadyError(null);
28
+ return r.ue !== NOT_PENDING ? r.ue : r.Ie;
29
+ }
30
+
31
+ /** A guess confirmed: its lane's truth readers re-read — of the lane's
32
+ * derivations too, which may be the truth now (#3943). */ function truthConfirmed(r) {
33
+ for (let t = 0; t < r.je.length; t++) for (let e = r.je[t].o; e !== null; e = e.t) if (e.u.l & CONFIG_AUTHORITATIVE) enqueueSub(e.u);
34
+ }
35
+
36
+ /** A lane node's value is the truth when every guess beneath it is
37
+ * confirmed — its source staged a value equal to the guess — and nothing
38
+ * beneath it is held in the parent: same inputs, same value. */ function truth(r) {
39
+ if (r.l & CONFIG_GUESS) return r.ue !== NOT_PENDING && r.Ye && r.Ye(r.T.V, r.ue);
40
+ if (r.ue !== NOT_PENDING) return;
41
+ if (r.l & CONFIG_OVERRIDE) for (let t = r.C; t; t = t.G) if (!truth(t.S)) return;
42
+ return 1;
43
+ }
44
+
45
+ export { laneValueOf, truthConfirmed, truthRead };
@@ -1,6 +1,6 @@
1
- import { CONFIG_VERDICT, REACTIVE_PROBED, CONFIG_INPUTS_PUBLISHED, CONFIG_HELD, REACTIVE_JOINED, REACTIVE_STAGED_READ, REACTIVE_LANE_READ, CONFIG_CHILDREN_FORBIDDEN, CONFIG_OVERRIDE, STATUS_PENDING, CONFIG_GUESS, NOT_PENDING, STATUS_UNINITIALIZED, EFFECT_RENDER, STATUS_ERROR, REACTIVE_PROBE_UNANSWERED } from "./constants.js";
1
+ import { CONFIG_VERDICT, REACTIVE_PROBED, CONFIG_CHILDREN_FORBIDDEN, CONFIG_OVERRIDE, STATUS_ERROR, CONFIG_GUESS, STATUS_PENDING, NOT_PENDING, STATUS_UNINITIALIZED, CONFIG_HELD, EFFECT_RENDER, CONFIG_STAGED_ERROR, REACTIVE_STAGED_READ, REACTIVE_PROBE_UNANSWERED, CONFIG_INPUTS_PUBLISHED, REACTIVE_JOINED, REACTIVE_LANE_READ } from "./constants.js";
2
2
 
3
- import { context, setVerdict, verdict, tracking, pullComputed, markLateLinker, spectating, unflushedValue } from "./core.js";
3
+ import { context, tracking, setVerdict, verdict, pullComputed, joinPass, markLateLinker, spectating, unflushedValue, bornInto } from "./core.js";
4
4
 
5
5
  import "./dev.js";
6
6
 
@@ -8,9 +8,11 @@ import { NotReadyError } from "./error.js";
8
8
 
9
9
  import { link } from "./graph.js";
10
10
 
11
- import { verdictLane, laneValueOf, display } from "./lanes.js";
11
+ import { display, verdictLane } from "./lanes.js";
12
12
 
13
- import { GlobalQueue, globalQueue, joinFuture, queuePendingNode, staleReader, passLane, setPassLane, flushTransaction, txOf, stagedReaders } from "./scheduler.js";
13
+ import { installVerdictHooks, GlobalQueue, flushTransaction, txOf, passLane, staleReader, globalQueue, joinFuture, stagedReaders, queuePendingNode, setPassLane } from "./scheduler.js";
14
+
15
+ import { laneValueOf } from "./truth.js";
14
16
 
15
17
  // Verdicts (plan sec. 28 replay, 2026-10-02; maintainer, 2026-10-01: verdicts are
16
18
  // optimistic state the system supplies). `isPending(fn)` asks whether what
@@ -61,11 +63,13 @@ function watchVerdict(e, t = false) {
61
63
  * every flush (fuzzer F12). True when the reader is served the committed
62
64
  * value — not over a flight committed beneath inputs already on screen (A15
63
65
  * reveal corollary, #3305): that value would tear against them, and the
64
- * reveal observes the flight. */ function observeFlight(e, t) {
65
- if (t.u & CONFIG_INPUTS_PUBLISHED) return false;
66
- if (globalQueue.ee) {
66
+ * reveal observes the flight. Nor for a reader born into the flight's
67
+ * transaction (`bornInto`, #4010): it has no screen — it waits on the flight
68
+ * with the rest of its frame. */ function observeFlight(e, t) {
69
+ if (t.l & CONFIG_INPUTS_PUBLISHED || t.l & CONFIG_HELD && bornInto(e, txOf(t))) return false;
70
+ if (globalQueue.q) {
67
71
  joinFuture(null);
68
- if (!(t.u & CONFIG_HELD)) queuePendingNode(t);
72
+ if (!(t.l & CONFIG_HELD)) queuePendingNode(t);
69
73
  verdictRead(e, flushTransaction, true);
70
74
  }
71
75
  return true;
@@ -88,9 +92,9 @@ let probeFound = false;
88
92
  * value notifying anyone (a held write's commit; a held landing's): its
89
93
  * reader re-derives there. `latest` shows the proposal that lands — a
90
94
  * rewrite or a landing reaches its reader through the link it holds — so a
91
- * re-run would only repeat the frame. */ function verdictRead(e, t, n = probing) {
95
+ * re-run would only repeat the frame. */ function verdictRead(e, t, n) {
92
96
  route(e, t);
93
- if (n) staleReader(e, t);
97
+ if (n) staleReader(e, t); else if (probing && t.En[t.En.length - 1] !== e) t.En.push(e);
94
98
  }
95
99
 
96
100
  /** A verdict read of a node staged or in flight but not held yet: whether it
@@ -133,13 +137,13 @@ let probeFound = false;
133
137
  /** A19 exc. 2 / A24: a flight that re-asks the question already answered
134
138
  * (a `refresh()`, a poll) is quiet; a node pending through others is quiet
135
139
  * when every flight it waits on is. */ function quietPending(e) {
136
- const t = e.m;
140
+ const t = e.T;
137
141
  if (t === null) return false;
138
- if (t.Se !== undefined) {
139
- for (const e of t.Se) if (!e.m?.It) return false;
142
+ if (t.fe !== undefined) {
143
+ for (const e of t.fe) if (!e.T?.ut) return false;
140
144
  return true;
141
145
  }
142
- return t.It;
146
+ return t.ut;
143
147
  }
144
148
 
145
149
  /** `isPending`'s answer for a held node (A19), relative to what the reader
@@ -154,29 +158,32 @@ let probeFound = false;
154
158
  * frame with neither is final. A24: pending iff a value change is in
155
159
  * flight — a staging equal to what is displayed changes nothing when it
156
160
  * lands: final now (maintainer, 2026-10-02). */ function heldNotFinal(e) {
157
- if ((e.G & STATUS_PENDING) !== 0 && !quietPending(e)) return true;
158
- return e.Ae !== NOT_PENDING && !latestActive && !e.m.It && !(e.G & STATUS_UNINITIALIZED) && (!e.tn || !e.tn(e.pe, e.Ae));
161
+ if ((e.m & STATUS_PENDING) !== 0 && !quietPending(e)) return true;
162
+ return e.ue !== NOT_PENDING && !latestActive && !e.T.ut && !(e.m & STATUS_UNINITIALIZED) && (!!e.T.de || !e.Ye || !e.Ye(e.Ie, e.ue));
159
163
  }
160
164
 
161
165
  /** `isPending`'s answer for an unheld node whose flight is up (cause ii). */ function pendingVerdict(e, t) {
162
- if (t.G & STATUS_UNINITIALIZED) return;
166
+ if (t.m & STATUS_UNINITIALIZED) return;
163
167
  if (probing && !quietPending(t)) probeFound = true;
164
168
  if (e !== null && tracking) {
165
- if (t.u & CONFIG_HELD) verdictRead(e, txOf(t)); else if (globalQueue.ee) provisionalVerdict(e, false);
169
+ if (t.l & CONFIG_HELD) verdictRead(e, txOf(t)); else if (globalQueue.q) provisionalVerdict(e, false);
166
170
  // decided above
167
171
  }
168
172
  }
169
173
 
170
174
  /** The pass entering a verdict window is a verdict reader (CONFIG_VERDICT):
171
- * a dependency going pending re-derives it, the verdict having changed. */ function markVerdictReader(e) {
175
+ * a dependency going pending re-derives it, the verdict having changed. Not
176
+ * an untracked window: that is a snapshot, like any untracked read (#3946 —
177
+ * a router's link claim probing inside the computation that creates the
178
+ * anchor). A throw still reaches the pass through `untrack`. */ function markVerdictReader(e) {
172
179
  let t = context;
173
- if (t?.Ht) t = t.gt;
174
- if (t !== null) {
175
- t.u |= CONFIG_VERDICT;
180
+ if (t?.Pt) t = t.Ht;
181
+ if (t !== null && tracking) {
182
+ t.l |= CONFIG_VERDICT;
176
183
  t.R |= REACTIVE_PROBED;
177
184
  // Observe tier: which windows this pass entered (1 `isPending`, 2
178
185
  // `latest`) — the hold census names the affordance (attribution.ts).
179
- t.en = (t.en ?? 0) | e;
186
+ t.Ve = (t.Ve ?? 0) | e;
180
187
  }
181
188
  }
182
189
 
@@ -195,7 +202,38 @@ let probeFound = false;
195
202
  }
196
203
  }
197
204
 
198
- function isPending(e) {
205
+ /**
206
+ * Reports whether what `fn` reads is still waiting on an update. Read the
207
+ * values in question inside `fn`: the probe answers `true` while any of
208
+ * them depends on a change that is in flight — a write or an async source
209
+ * that has started producing a new value whose result has not landed yet —
210
+ * and `false` once the latest value is the one on screen. The answer is
211
+ * tracked like any read, so a memo or effect that calls `isPending` re-runs
212
+ * when it flips.
213
+ *
214
+ * A read that has no value yet is loading, not pending: inside `fn` it
215
+ * follows the surrounding `Loading` path like any other read, and the probe
216
+ * reports only updates to a value that already exists. A bare `refresh()`
217
+ * does not flip `isPending`; pair it with `affects()` to make the re-ask
218
+ * visible.
219
+ *
220
+ * @param fn expression to probe; read the values in question inside it
221
+ *
222
+ * @returns `true` while a read inside `fn` depends on a change that has not committed yet
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * const [query, setQuery] = createSignal("");
227
+ * const results = createMemo(() => searchProducts(query()));
228
+ * const stale = createMemo(() => isPending(results));
229
+ *
230
+ * setQuery("shoes");
231
+ * // stale() is true until the new results land, while results() keeps
232
+ * // serving the previous list — dim it instead of showing a spinner.
233
+ * ```
234
+ *
235
+ * @description https://v2.solidjs.com/reference/solid-js/reactivity/is-pending
236
+ */ function isPending(e) {
199
237
  markVerdictReader(1);
200
238
  const t = probing;
201
239
  const n = probeFound;
@@ -222,11 +260,16 @@ function isPending(e) {
222
260
  }
223
261
  }
224
262
 
263
+ /** The staging, or the staged error thrown (D-41). */ function staged(e) {
264
+ if (e.l & CONFIG_STAGED_ERROR) throw e.ue;
265
+ return e.ue;
266
+ }
267
+
225
268
  /** `latest()` of a held node: the flushed staging — the proposal — and, for
226
269
  * a tracked reader of a rewrite not yet flushed, the late-linker re-run in
227
270
  * the flush that carries it. */ function heldLatest(e, t) {
228
271
  const n = unflushedValue(e);
229
- if (n === NOT_PENDING) return e.Ae;
272
+ if (n === NOT_PENDING) return staged(e);
230
273
  if (t !== null) markLateLinker(t);
231
274
  return n;
232
275
  }
@@ -253,10 +296,11 @@ function isPending(e) {
253
296
  * uninitialized one throws NotReady: loading, not pending (A19 exc. 1);
254
297
  * - an errored derivation throws for every reader (#2897). */ function verdictValue(e, t) {
255
298
  const n = e;
256
- const r = t !== null && tracking && !(t.u & CONFIG_CHILDREN_FORBIDDEN);
299
+ const r = t !== null && !(t.l & CONFIG_CHILDREN_FORBIDDEN);
300
+ const i = r && tracking;
257
301
  if (t !== null && tracking) {
258
302
  link(e, t);
259
- if (n.re !== undefined) {
303
+ if (n.X !== undefined) {
260
304
  // A31: the pulled memo's pass is its own, not the probe's — it reads
261
305
  // outside the window, and the probe's read below answers for what
262
306
  // the pass produced.
@@ -269,86 +313,92 @@ function isPending(e) {
269
313
  // final by declaration — the value itself is read as below. The verdict is
270
314
  // display-ahead like any: in a transaction's flush (the declaring action's
271
315
  // body) the reader is its verdict lane's, not a staging the frame parks.
272
- if (probing && GlobalQueue.C !== undefined && GlobalQueue.C(n)) {
316
+ if (probing && GlobalQueue.p !== undefined && GlobalQueue.p(n)) {
273
317
  probeFound = true;
274
- if (r && flushTransaction !== null) verdictRead(t, flushTransaction);
318
+ if (i && flushTransaction !== null) verdictRead(t, flushTransaction);
275
319
  }
276
- if (e.u & CONFIG_OVERRIDE) {
277
- const i = txOf(e);
278
- if (r) verdictRead(t, i._parent ?? i);
279
- const u = (n.G & STATUS_PENDING) !== 0 && !quietPending(n);
280
- if (e.u & CONFIG_GUESS) {
281
- if (probing && (u || e.Ae !== NOT_PENDING && (!e.tn || !e.tn(e.m.Z, e.Ae)))) probeFound = true;
282
- return e.m.Z;
320
+ if (e.l & CONFIG_OVERRIDE && !(n.m & STATUS_ERROR && !(e.l & CONFIG_GUESS))) {
321
+ const r = txOf(e);
322
+ if (i) verdictRead(t, r._parent ?? r);
323
+ const u = (n.m & STATUS_PENDING) !== 0 && !quietPending(n);
324
+ if (e.l & CONFIG_GUESS) {
325
+ if (probing && (u || e.ue !== NOT_PENDING && (!e.Ye || !e.Ye(e.T.V, e.ue)))) probeFound = true;
326
+ return e.T.V;
283
327
  }
284
328
  if (probing && u) probeFound = true;
285
329
  return latestActive && !u ? laneValueOf(e) : display(e);
286
330
  }
287
- const i = (n.G & STATUS_UNINITIALIZED) !== 0;
288
- if (e.u & CONFIG_HELD) {
289
- if (!i) {
290
- const i = txOf(e);
331
+ const u = (n.m & STATUS_UNINITIALIZED) !== 0;
332
+ if (e.l & CONFIG_HELD) {
333
+ if (!u) {
334
+ const r = txOf(e);
291
335
  // A render effect re-run (or mounted) outside the verdict lane's flush
292
336
  // while the lane is blocked — its display held on its own derivation
293
337
  // in flight — is a stale reader of the lane (#3460, as `laneRead`): the
294
338
  // committed value now, re-derived at the reveal. Not inside a probe:
295
339
  // `isPending` is answered below either way.
296
- if (r && !probing && t.i === EFFECT_RENDER && passLane === null && i.rt !== null && i.rt.fn && flushTransaction !== i) {
297
- staleReader(t, i.rt);
298
- return e.pe;
340
+ if (i && !probing && t.i === EFFECT_RENDER && passLane === null && r.it !== null && r.it.ln && flushTransaction !== r) {
341
+ staleReader(t, r.it);
342
+ return e.Ie;
299
343
  }
300
- if (r) verdictRead(t, i);
344
+ if (i) verdictRead(t, r);
301
345
  if (probing && heldNotFinal(n)) probeFound = true;
302
- if (latestActive && e.Ae !== NOT_PENDING) {
346
+ if (latestActive && e.ue !== NOT_PENDING) {
303
347
  // An action body reading another transaction's proposal derives
304
348
  // from it: the two settle as one (A15; posture C, 2026-09-15). An
305
349
  // untracked read elsewhere has no transaction to entangle.
306
- if (!r && flushTransaction !== null && !globalQueue.ee) joinFuture(txOf(e));
307
- return heldLatest(e, r ? t : null);
350
+ if (!i && flushTransaction !== null && !globalQueue.q) joinFuture(txOf(e));
351
+ return heldLatest(e, i ? t : null);
308
352
  }
309
- return e.pe;
353
+ return e.Ie;
354
+ }
355
+ // Born into the future: its staging is its only value. It has no
356
+ // committed value, so no verdict: `isPending` reads it as its read does —
357
+ // the owner joins the hold (held with it, or caught by a boundary that
358
+ // has not shown content) and answers for the commit, where it is final.
359
+ if (e.ue !== NOT_PENDING && !(n.m & STATUS_PENDING)) {
360
+ if (probing && r) joinPass(t, e);
361
+ return staged(e);
310
362
  }
311
- // Born into the future: its staging is its only value.
312
- if (e.Ae !== NOT_PENDING && !(n.G & STATUS_PENDING)) return e.Ae;
313
- } else if (e.Ae !== NOT_PENDING) {
314
- if (n.G & STATUS_PENDING && !i) pendingVerdict(r ? t : null, n); else if (r && globalQueue.ee) provisionalVerdict(t, !latestActive && !i);
363
+ } else if (e.ue !== NOT_PENDING) {
364
+ if (n.m & STATUS_PENDING && !u) pendingVerdict(i ? t : null, n); else if (i && globalQueue.q) provisionalVerdict(t, !latestActive && !u);
315
365
  // Outside a flush, the staging is an unflushed write (A28): the probe
316
366
  // answers for the flushed world, and watches the seam of the flush that
317
367
  // carries the write — a memo of `isPending(x)` created after the write
318
368
  // flips if that flush parks (#3078). (The write's own fan-out ran before
319
369
  // the reader existed; nothing else would re-derive it.)
320
- else if (r && probing) watchVerdict(t);
370
+ else if (i && probing) watchVerdict(t);
321
371
  if (latestActive) {
322
372
  // A28: an unflushed write is not what `latest` serves; a tracked
323
373
  // reader derived from the flushed world runs again in the carrying
324
374
  // flush.
325
- if (globalQueue.ee) return e.Ae;
375
+ if (globalQueue.q) return staged(e);
326
376
  if (t !== null) markLateLinker(t);
327
377
  }
328
- return e.pe;
378
+ return e.Ie;
329
379
  }
330
- if (n.G & STATUS_PENDING) {
331
- if (!i) {
332
- pendingVerdict(r ? t : null, n);
333
- return e.pe;
380
+ if (n.m & STATUS_PENDING) {
381
+ if (!u) {
382
+ pendingVerdict(i ? t : null, n);
383
+ return e.Ie;
334
384
  }
335
385
  // As `read()`: an untracked read of a pending node still re-runs its
336
386
  // reader when the node settles (a projection's pull reads its firewall
337
387
  // untracked, store/store.ts).
338
388
  if (t !== null && !tracking && !spectating && e !== t) link(e, t);
339
- throw n.m?.v;
389
+ throw n.T?.F;
340
390
  }
341
- if (n.re !== undefined && n.G & STATUS_ERROR) throw n.m.v;
342
- return e.pe;
391
+ if (n.X !== undefined && n.m & STATUS_ERROR) throw n.T.F;
392
+ return e.Ie;
343
393
  }
344
394
 
345
395
  // Installed at module evaluation — present exactly when something imports
346
396
  // `latest` or `isPending`. (`read` dispatches to `verdictValue` through
347
397
  // `verdict`, set while a window is open.)
348
- GlobalQueue.vt = observeFlight;
398
+ installVerdictHooks(observeFlight);
349
399
 
350
400
  /** A store read inside an untracked `isPending` probe found a live mark on
351
- * a record with no node to carry it (store/affects.ts): not final. */ GlobalQueue.Bt = () => {
401
+ * a record with no node to carry it (store/affects.ts): not final. */ GlobalQueue.bt = () => {
352
402
  if (probing) probeFound = true;
353
403
  };
354
404
 
@@ -0,0 +1,184 @@
1
+ import { CONFIG_REDERIVE, STATUS_UNINITIALIZED, CONFIG_LAGS, CONFIG_SLOT_NODE, REACTIVE_RECOMPUTING_DEPS, REACTIVE_DISPOSED, REACTIVE_REASK, CONFIG_HELD, NOT_PENDING } from "./core/constants.js";
2
+
3
+ import { isThenable, handleAsync } from "./core/async.js";
4
+
5
+ import { NotReadyError } from "./core/error.js";
6
+
7
+ import "./core/verdict.js";
8
+
9
+ import { computed } from "./core/core.js";
10
+
11
+ import { cleanup, getOwner } from "./core/owner.js";
12
+
13
+ import "./core/dev.js";
14
+
15
+ import { GlobalQueue, holdNode, txOf, schedule } from "./core/scheduler.js";
16
+
17
+ import "./core/effect.js";
18
+
19
+ import { mark, repoll, unmark } from "./affects-marks.js";
20
+
21
+ import { accessor } from "./signals.js";
22
+
23
+ // `createDeferred` — an async memo that may lag, but never leads (D-40,
24
+ // docs/create-deferred.md).
25
+ // Once the node has a committed value, its own non-final work is invisible
26
+ // to the graph: its readers keep the committed value and hold nothing, its
27
+ // landings are changes of their own, and only the verdict (`isPending`) and
28
+ // the truth readers (`until`, `refresh`'s waiter) see the flight.
29
+ // Built from parts the engine already has, so core pays nothing for it:
30
+ // - **Read side: the loading window** (`_loading`, async.ts). Every pass of
31
+ // a node with a committed value opens the window before it asks, so a
32
+ // flight serves the committed value instead of throwing, and an unready
33
+ // source parks the node instead of pending it. `CONFIG_REDERIVE` makes a
34
+ // source going pending a question for the node's pass (which parks) rather
35
+ // than a status pushed through it to its readers.
36
+ // - **Verdict: a mark** (affects-marks.ts) whose lifetime is the flight —
37
+ // started when a pass asks a new question (a `refresh()` re-ask is quiet, F-9),
38
+ // dropped at the landing, the rejection, a sync answer or the node's death.
39
+ // - **Never leads:** a flight that lands while an input it read is held
40
+ // derived from that hold's world, so it lands into the hold (`holdNode`;
41
+ // `setSignal` stages it there). An input that changed after the pass
42
+ // would have superseded the flight, so a held input at the landing is one
43
+ // the pass read held.
44
+ // - **Truth readers** wait the flight out (`GlobalQueue._lagging`, signals.ts
45
+ // `watch`): `await refresh(d)` delivers the landing, `until` sees only
46
+ // settled values.
47
+ /** Deferred nodes with a flight up. */
48
+ const lagging = new Set;
49
+
50
+ /** The deferred node with a flight up that `c`'s value derives from, if
51
+ * any: `c` itself, or one reached through its current dependencies. */ function lagOf(e, r) {
52
+ if (lagging.has(e)) return e;
53
+ if (r.has(e)) return;
54
+ r.add(e);
55
+ if (e.l & CONFIG_SLOT_NODE) return lagOf(e.O?.fam?.node ?? e, r);
56
+ const t = e.R & REACTIVE_RECOMPUTING_DEPS ? e._ : undefined;
57
+ if (t !== null) for (let o = e.C ?? null; o !== null; o = o.G) {
58
+ const e = lagOf(o.S, r);
59
+ if (e) return e;
60
+ if (o === t) break;
61
+ }
62
+ }
63
+
64
+ GlobalQueue.er = e => lagging.size !== 0 ? lagOf(e, new Set) : undefined;
65
+
66
+ /**
67
+ * Creates an async memo that may lag, but never leads. Before its first
68
+ * answer it is an ordinary async memo: readers suspend to the nearest
69
+ * `<Loading>`. After it, a refetch holds nothing: readers keep the last
70
+ * committed value until the new one lands, and the rest of the screen moves
71
+ * on without it. `isPending` reports the refetch, on the node and on what
72
+ * derives from it. Use it for independent widgets (dashboards, feeds,
73
+ * search results) where the previous answer with a "refreshing" indicator is
74
+ * better than waiting with everything else.
75
+ *
76
+ * - It never shows a value ahead of a held change: a refetch that read a
77
+ * held value lands with that hold.
78
+ * - A rejected refetch errors like any memo; the stale value never masks it.
79
+ * - `refresh(d)` is a quiet re-ask, and `await refresh(d)` and `until`
80
+ * wait for the landing rather than the stale value.
81
+ * - Combine with `loadingValue` for a node that never suspends at all.
82
+ *
83
+ * @example
84
+ * ```tsx
85
+ * const rows = createDeferred(() => fetchRows(store.period));
86
+ *
87
+ * <section class={{ stale: isPending(() => rows()) }}>
88
+ * <For each={rows()}>{row => <Row row={row} />}</For>
89
+ * </section>
90
+ * ```
91
+ */ function createDeferred(e, r) {
92
+ let t;
93
+ /** The flight in force: a promise, a stream, or the error an unready
94
+ * source parked the node with. */ let o = null;
95
+ let n = false;
96
+ let l = false;
97
+ /** The flight `r` is over. An async answer (`held`) arriving while an
98
+ * input is held lands into that hold first. */ const land = (e, r) => {
99
+ if (e === null || o !== e) return;
100
+ o = null;
101
+ lagging.delete(t);
102
+ if (r) for (let e = t.C; e !== null; e = e.G) if (e.S.l & CONFIG_HELD) {
103
+ holdNode(t, txOf(e.S));
104
+ break;
105
+ }
106
+ if (n) {
107
+ n = false;
108
+ unmark(t);
109
+ schedule();
110
+ }
111
+ };
112
+ const ask = e => {
113
+ o = e;
114
+ lagging.add(t);
115
+ if (!n && !(t.R & REACTIVE_REASK)) {
116
+ n = true;
117
+ mark(t);
118
+ repoll(t);
119
+ }
120
+ };
121
+ /** Registers the flight and serves the pass's value: the committed one,
122
+ * or over a held landing the landing itself, which a lagging question
123
+ * does not replace (D-40) — restaged as the committed value, the hold's
124
+ * readers would re-derive against the old input. */ const serve = e => {
125
+ const r = handleAsync(t, e);
126
+ return t.l & CONFIG_HELD && t.ue !== NOT_PENDING ? t.ue : r;
127
+ };
128
+ const end = () => t.R & REACTIVE_DISPOSED && land(o);
129
+ return accessor(computed(r => {
130
+ t = getOwner();
131
+ t.l |= CONFIG_REDERIVE;
132
+ cleanup(end);
133
+ // Answered: the node has a real committed answer — not a first load,
134
+ // not a `loadingValue` window still open (both are loading, F-9).
135
+ // Sticky, and read before this wrapper ever opens the window itself.
136
+ const n = l ||= !(t.m & STATUS_UNINITIALIZED) && !t.ce;
137
+ if (n) t.ce = true, t.l |= CONFIG_LAGS;
138
+ let c;
139
+ try {
140
+ c = e(r);
141
+ } catch (e) {
142
+ // The wrapping form: an unready source parks the node in the window,
143
+ // and the wait is its flight until the source's settle re-runs it.
144
+ if (n && e instanceof NotReadyError) ask(e);
145
+ throw e;
146
+ }
147
+ if (n && typeof c === "object" && c !== null) {
148
+ if (isThenable(c)) {
149
+ ask(c);
150
+ const done = () => land(c, true);
151
+ c.then(done, done);
152
+ return serve(c);
153
+ }
154
+ const e = c[Symbol.asyncIterator];
155
+ if (e) {
156
+ // A stream's flight ends at its first yield (or its end).
157
+ const r = e.call(c);
158
+ let t = true;
159
+ const done = () => land(o, true);
160
+ const o = {
161
+ // Registered ahead of the engine's own reaction to the same
162
+ // promise, as for a plain flight: verdict and value in one flush.
163
+ next: e => {
164
+ const o = r.next(e);
165
+ if (t) t = false, o.then(done, done);
166
+ return o;
167
+ },
168
+ return: e => r.return?.(e) ?? Promise.resolve({
169
+ done: true,
170
+ value: e
171
+ })
172
+ };
173
+ ask(o);
174
+ return serve({
175
+ [Symbol.asyncIterator]: () => o
176
+ });
177
+ }
178
+ }
179
+ land(o);
180
+ return c;
181
+ }, r));
182
+ }
183
+
184
+ export { createDeferred };
@@ -4,12 +4,14 @@ export { isPending, latest } from "./core/verdict.js";
4
4
 
5
5
  export { action } from "./core/action.js";
6
6
 
7
- export { clearSnapshots, isEqual, markSnapshotScope, releaseSnapshotScope, runWithOwner, setSnapshotCapture, untrack } from "./core/core.js";
7
+ export { clearSnapshots, isEqual, markSnapshotScope, releaseSnapshotScope, runWithOwner, setSnapshotCapture, setWriteHook, untrack } from "./core/core.js";
8
8
 
9
9
  export { enableExternalSource } from "./core/external.js";
10
10
 
11
11
  export { createOwner, createRoot, getNextChildId, getObserver, getOwner, isDisposed, peekNextChildId } from "./core/owner.js";
12
12
 
13
+ export { setReadWatch, setSignalIdentities, signalName } from "./core/identities.js";
14
+
13
15
  export { createContext, getContext, setContext } from "./core/context.js";
14
16
 
15
17
  export { $REFRESH, SUPPORTS_PROXY } from "./core/constants.js";
@@ -24,6 +26,8 @@ import "./core/effect.js";
24
26
 
25
27
  export { configureClientErrors } from "./core/error-hooks.js";
26
28
 
29
+ export { storeName } from "./store/edges.js";
30
+
27
31
  export { createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onSettled, refresh, resolve, until } from "./signals.js";
28
32
 
29
33
  export { mapArray, repeat } from "./map.js";
@@ -32,6 +36,8 @@ export { createStore, deep, reconcile, snapshot } from "./store/index.js";
32
36
 
33
37
  export { affects } from "./affects.js";
34
38
 
39
+ export { createDeferred } from "./deferred.js";
40
+
35
41
  export { flatten } from "./flatten.js";
36
42
 
37
43
  export { createErrorBoundary, createLoadingBoundary } from "./boundaries.js";