@solidjs/signals 2.0.0-rc.17 → 2.0.0-rc.19

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 (71) hide show
  1. package/dist/dev-shared.js +426 -241
  2. package/dist/dev.attribution.js +19 -16
  3. package/dist/dev.js +768 -409
  4. package/dist/observe/boundaries.js +88 -67
  5. package/dist/observe/core/action.js +8 -0
  6. package/dist/observe/core/async.js +183 -138
  7. package/dist/observe/core/attribution.js +30 -28
  8. package/dist/observe/core/constants.js +23 -1
  9. package/dist/observe/core/core.js +185 -157
  10. package/dist/observe/core/effect.js +34 -37
  11. package/dist/observe/core/graph.js +8 -8
  12. package/dist/observe/core/heap.js +5 -5
  13. package/dist/observe/core/lanes.js +104 -84
  14. package/dist/observe/core/owner.js +17 -16
  15. package/dist/observe/core/scheduler.js +61 -47
  16. package/dist/observe/core/truth.js +6 -6
  17. package/dist/observe/core/verdict.js +15 -15
  18. package/dist/observe/deferred.js +40 -38
  19. package/dist/observe/from.js +248 -0
  20. package/dist/observe/index.js +4 -2
  21. package/dist/observe/map.js +22 -22
  22. package/dist/observe/reveal.js +5 -5
  23. package/dist/observe/signals.js +57 -15
  24. package/dist/observe/store/affects.js +1 -1
  25. package/dist/observe/store/hooks.js +3 -0
  26. package/dist/observe/store/optimistic.js +11 -11
  27. package/dist/observe/store/projection.js +34 -21
  28. package/dist/observe/store/reconcile.js +138 -122
  29. package/dist/observe/store/store.js +628 -641
  30. package/dist/observe/store/target.js +1 -5
  31. package/dist/observe/store/utils.js +7 -2
  32. package/dist/prod/boundaries.js +153 -132
  33. package/dist/prod/core/action.js +8 -0
  34. package/dist/prod/core/async.js +242 -197
  35. package/dist/prod/core/constants.js +16 -1
  36. package/dist/prod/core/core.js +166 -138
  37. package/dist/prod/core/effect.js +31 -34
  38. package/dist/prod/core/graph.js +5 -5
  39. package/dist/prod/core/heap.js +12 -12
  40. package/dist/prod/core/lanes.js +98 -78
  41. package/dist/prod/core/owner.js +15 -14
  42. package/dist/prod/core/scheduler.js +64 -50
  43. package/dist/prod/core/truth.js +6 -6
  44. package/dist/prod/core/verdict.js +17 -17
  45. package/dist/prod/deferred.js +40 -38
  46. package/dist/prod/from.js +248 -0
  47. package/dist/prod/index.js +4 -2
  48. package/dist/prod/map.js +14 -14
  49. package/dist/prod/reveal.js +8 -8
  50. package/dist/prod/signals.js +46 -4
  51. package/dist/prod/store/affects.js +1 -1
  52. package/dist/prod/store/hooks.js +3 -0
  53. package/dist/prod/store/optimistic.js +16 -16
  54. package/dist/prod/store/projection.js +22 -9
  55. package/dist/prod/store/reconcile.js +138 -122
  56. package/dist/prod/store/store.js +308 -331
  57. package/dist/prod/store/target.js +13 -17
  58. package/dist/prod/store/utils.js +2 -0
  59. package/dist/types/core/action.d.ts +8 -0
  60. package/dist/types/core/async.d.ts +12 -2
  61. package/dist/types/core/constants.d.ts +25 -0
  62. package/dist/types/core/core.d.ts +33 -4
  63. package/dist/types/core/dev.d.ts +1 -1
  64. package/dist/types/core/index.d.ts +1 -1
  65. package/dist/types/core/lanes.d.ts +5 -1
  66. package/dist/types/core/scheduler.d.ts +17 -3
  67. package/dist/types/from.d.ts +47 -0
  68. package/dist/types/index.d.ts +4 -1
  69. package/dist/types/signals.d.ts +37 -0
  70. package/dist/types/store/target.d.ts +2 -0
  71. package/package.json +1 -1
@@ -0,0 +1,248 @@
1
+ import { isThenable, onSupersede } from "./core/async.js";
2
+
3
+ import { CONFIG_CHILDREN_FORBIDDEN } from "./core/constants.js";
4
+
5
+ import { cleanup, getOwner } from "./core/owner.js";
6
+
7
+ // `from` — a push source (callback subscription, Observable, AsyncIterable)
8
+ // as an AsyncIterable a computation can return or `for await` over:
9
+ // const value = createMemo(() => from(source));
10
+ // A subscription keeps the latest value: values that arrive between pulls
11
+ // coalesce. An AsyncIterable streams through unchanged. Either way the source
12
+ // opens when the result is iterated and closes on return(), on supersede (the
13
+ // owning computation re-runs) and on owner disposal, whichever comes first.
14
+ function toSubscribe(e) {
15
+ if (typeof e === "function") return e;
16
+ // TC39 / RxJS interop: `Symbol.observable` (or "@@observable") returns the observable.
17
+ // Read per call, not at module scope, so the module stays side-effect free.
18
+ const n = Symbol.observable;
19
+ const r = (n && e?.[n]) ?? e?.["@@observable"];
20
+ const t = typeof r === "function" ? r.call(e) : e;
21
+ if (t && typeof t.subscribe === "function") return (e, n, r) => t.subscribe({
22
+ next: e,
23
+ error: n,
24
+ complete: r
25
+ });
26
+ return null;
27
+ }
28
+
29
+ const DONE = {
30
+ value: undefined,
31
+ done: true
32
+ };
33
+
34
+ /**
35
+ * Ties an iterator's `finish` to the computation iterating it. Solid reaches
36
+ * an iterator's return() only when the compute returns the iterable directly.
37
+ * Inside an async generator the generator's own return() is queued behind its
38
+ * pending `await`, and owner cleanups wait for the replacing pass to commit,
39
+ * so close on supersede, with owner disposal as the backstop. No owner (an
40
+ * async compute iterating it after its first `await`, or plain iteration):
41
+ * no registration and no diagnostic, so only return() closes it. Scopes that
42
+ * forbid cleanups rely on supersede and return().
43
+ */ function bindToOwner(e) {
44
+ const n = getOwner();
45
+ if (!n) return;
46
+ onSupersede(e);
47
+ if (!(n.l & CONFIG_CHILDREN_FORBIDDEN)) cleanup(e);
48
+ }
49
+
50
+ /**
51
+ * An AsyncIterable as a stream bound to the iterating computation: every
52
+ * value passes through (no coalescing). `finish` calls the inner return()
53
+ * once and settles pending pulls as done, so a `for await` over it exits as
54
+ * soon as the computation re-runs — even when the inner iterator is an async
55
+ * generator whose own return() the language queues behind its pending
56
+ * `await`.
57
+ */ function wrapIterable(e) {
58
+ return {
59
+ [Symbol.asyncIterator]() {
60
+ const n = e[Symbol.asyncIterator]();
61
+ let r = false;
62
+ const t = new Set;
63
+ const finish = () => {
64
+ if (r) return DONE;
65
+ r = true;
66
+ for (const e of t) e(DONE);
67
+ t.clear();
68
+ try {
69
+ const e = n.return?.();
70
+ if (isThenable(e)) e.then(undefined, () => {});
71
+ } catch {}
72
+ return DONE;
73
+ };
74
+ bindToOwner(finish);
75
+ return {
76
+ next(...e) {
77
+ if (r) return Promise.resolve(DONE);
78
+ const o = n.next(...e);
79
+ // A bare result (the producer's buffered fast path) passes through.
80
+ if (!isThenable(o)) return o;
81
+ return new Promise((e, n) => {
82
+ t.add(e);
83
+ o.then(n => t.delete(e) && e(n), r => t.delete(e) && n(r));
84
+ });
85
+ },
86
+ return() {
87
+ return Promise.resolve(finish());
88
+ }
89
+ };
90
+ }
91
+ };
92
+ }
93
+
94
+ const toCleanup = e => typeof e === "function" ? e : e && typeof e.unsubscribe === "function" ? () => e.unsubscribe() : () => {};
95
+
96
+ /**
97
+ * Converts a callback subscription, an Observable or an AsyncIterable into an
98
+ * AsyncIterable for a Solid computation. A subscription keeps only the latest
99
+ * value, and a value the source emits during `subscribe` is served without a
100
+ * loading frame; an AsyncIterable streams every value through.
101
+ *
102
+ * The source closes on supersede — the owning computation re-running — and
103
+ * on owner disposal. Inside an async generator memo that holds for a
104
+ * `for await` that starts before the first `await`; one that starts after it
105
+ * has no owner to bind to, so its source closes only when the loop ends.
106
+ * Prefer returning `from(...)` from its own memo, with awaited inputs read
107
+ * from upstream memos and transforms done in a downstream memo.
108
+ *
109
+ * Iterate the result with `for await` or return it from a computation: its
110
+ * `next()` returns an already-buffered value as a bare result, not a promise.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const position = createMemo(() =>
115
+ * from<Position>((next, error) => {
116
+ * const id = navigator.geolocation.watchPosition(next, error);
117
+ * return () => navigator.geolocation.clearWatch(id);
118
+ * })
119
+ * );
120
+ * const ticks = createMemo(() => from(interval$(rate())));
121
+ * ```
122
+ */ function from(e) {
123
+ const n = toSubscribe(e);
124
+ if (!n) {
125
+ if (e && typeof e[Symbol.asyncIterator] === "function") return wrapIterable(e);
126
+ throw new TypeError("from(): expected a subscribe function, an Observable or an AsyncIterable");
127
+ }
128
+ return {
129
+ [Symbol.asyncIterator]() {
130
+ let e;
131
+ let r = false;
132
+ let t = false;
133
+ let o = false;
134
+ let u;
135
+ let f = null;
136
+ let l = null;
137
+ let s = false;
138
+ const close = () => {
139
+ if (s) return;
140
+ s = true;
141
+ t = true;
142
+ const e = l;
143
+ l = null;
144
+ // A throwing unsubscribe must not escape: at supersede this runs
145
+ // inside flush(), ahead of the replacing pass (the returned-iterator
146
+ // path swallows it the same way).
147
+ try {
148
+ e?.();
149
+ } catch {}
150
+ };
151
+ const settle = () => {
152
+ if (!f) return;
153
+ const n = f;
154
+ if (r) {
155
+ f = null;
156
+ r = false;
157
+ n.resolve({
158
+ value: e,
159
+ done: false
160
+ });
161
+ } else if (o) {
162
+ f = null;
163
+ o = false;
164
+ n.reject(u);
165
+ } else if (t) {
166
+ f = null;
167
+ n.resolve({
168
+ value: undefined,
169
+ done: true
170
+ });
171
+ }
172
+ };
173
+ const i = n(n => {
174
+ if (t) return;
175
+ e = n;
176
+ r = true;
177
+ settle();
178
+ }, e => {
179
+ if (t) return;
180
+ o = true;
181
+ u = e;
182
+ t = true;
183
+ settle();
184
+ close();
185
+ }, () => {
186
+ if (t) return;
187
+ t = true;
188
+ settle();
189
+ close();
190
+ });
191
+ l = toCleanup(i);
192
+ // Completed or errored during subscribe: release what it just returned.
193
+ if (s) {
194
+ const e = l;
195
+ l = null;
196
+ try {
197
+ e();
198
+ } catch {}
199
+ }
200
+ // Idempotent: return(), supersede and disposal may all reach it.
201
+ const finish = () => {
202
+ close();
203
+ r = false;
204
+ if (f) {
205
+ const e = f;
206
+ f = null;
207
+ e.resolve(DONE);
208
+ }
209
+ return DONE;
210
+ };
211
+ bindToOwner(finish);
212
+ return {
213
+ next() {
214
+ // A buffered value is returned as a bare result, not a promise: the
215
+ // iterator consumer lands it in the same pass, so a source that
216
+ // emits during subscribe (BehaviorSubject-style) shows its value
217
+ // with no loading frame. `for await` accepts either form.
218
+ if (r) {
219
+ r = false;
220
+ return {
221
+ value: e,
222
+ done: false
223
+ };
224
+ }
225
+ if (o) {
226
+ o = false;
227
+ return Promise.reject(u);
228
+ }
229
+ if (t) return Promise.resolve({
230
+ value: undefined,
231
+ done: true
232
+ });
233
+ return new Promise((e, n) => {
234
+ f = {
235
+ resolve: e,
236
+ reject: n
237
+ };
238
+ });
239
+ },
240
+ return() {
241
+ return Promise.resolve(finish());
242
+ }
243
+ };
244
+ }
245
+ };
246
+ }
247
+
248
+ export { from };
@@ -20,7 +20,7 @@ import { OBSERVE as OBSERVE$1 } from "./core/dev.js";
20
20
 
21
21
  export { setConsoleFooter } from "./core/dev.js";
22
22
 
23
- export { ROOT_ERROR_HOOK, enforceLoadingBoundary, flush, resetErrorHalt } from "./core/scheduler.js";
23
+ export { ROOT_ERROR_HOOK, clock, enforceLoadingBoundary, flush, resetErrorHalt } from "./core/scheduler.js";
24
24
 
25
25
  import "./core/effect.js";
26
26
 
@@ -28,7 +28,7 @@ export { configureClientErrors } from "./core/error-hooks.js";
28
28
 
29
29
  export { storeName } from "./store/edges.js";
30
30
 
31
- export { createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onSettled, refresh, resolve, until } from "./signals.js";
31
+ export { createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onFlush, onSettled, refresh, resolve, until } from "./signals.js";
32
32
 
33
33
  export { mapArray, repeat } from "./map.js";
34
34
 
@@ -38,6 +38,8 @@ export { affects } from "./affects.js";
38
38
 
39
39
  export { createDeferred } from "./deferred.js";
40
40
 
41
+ export { from } from "./from.js";
42
+
41
43
  export { flatten } from "./flatten.js";
42
44
 
43
45
  export { createErrorBoundary, createLoadingBoundary } from "./boundaries.js";
@@ -29,7 +29,7 @@ function mapArray(t, e, i) {
29
29
  Mt: [],
30
30
  Wt: r,
31
31
  vt: [],
32
- je: [],
32
+ Ye: [],
33
33
  Vt: s,
34
34
  Kt: s || i?.keyed === false ? [] : undefined,
35
35
  xt: n && i?.keyed !== false ? [] : undefined,
@@ -157,7 +157,7 @@ function trySmallMove(t, e, i, s) {
157
157
  }
158
158
  }
159
159
  const a = t.vt;
160
- const l = t.je;
160
+ const l = t.Ye;
161
161
  const u = a.slice(0, i);
162
162
  const d = l.slice(0, i);
163
163
  for (let t = 0; t < r.length; t += 3) {
@@ -177,7 +177,7 @@ function trySmallMove(t, e, i, s) {
177
177
  d[t] = l[e];
178
178
  }
179
179
  t.vt = u;
180
- t.je = d;
180
+ t.Ye = d;
181
181
  t.wt = i;
182
182
  t.Mt = e.slice(0);
183
183
  // Dispose unmatched sources LAST (general-path ordering).
@@ -197,7 +197,7 @@ function trySmallMove(t, e, i, s) {
197
197
  * needs the reset). The stage arrays are scratch — nothing live is touched. */ function stageRemoval(t, e, i, s, n, r, o, f, h) {
198
198
  const c = t.Vt;
199
199
  const a = t.Mt;
200
- const l = t.je;
200
+ const l = t.Ye;
201
201
  const u = [];
202
202
  let d = i;
203
203
  for (let p = i; p <= n; d++, p++) {
@@ -222,7 +222,7 @@ function trySmallMove(t, e, i, s) {
222
222
  * index signals. Its own function so the general path's body stays under
223
223
  * the JIT's optimization budget — the inlined commit measured ~10% slower on
224
224
  * full-window reorders (the small-move note has the same finding). */ function commitWindow(t, e, i, s, n, r, o, f, h) {
225
- const c = t.je;
225
+ const c = t.Ye;
226
226
  const a = t.Kt;
227
227
  const l = t.xt;
228
228
  const u = t.wt;
@@ -267,7 +267,7 @@ function updateKeyedMap() {
267
267
  // the flight's reader (pending tracking + post-settle retry) instead of
268
268
  // vanishing. Set per pass: the first pass runs inside `computed()`, before
269
269
  // the node exists to the caller.
270
- this.H.Ht = getOwner();
270
+ this.H.ht = getOwner();
271
271
  const t = this.yt() || [], e = t.length;
272
272
  t[$TRACK];
273
273
  // top level tracking (a store array's container node)
@@ -298,7 +298,7 @@ function updateKeyedMap() {
298
298
  if (e === 0) {
299
299
  if (this.wt !== 0) {
300
300
  this.H.dispose(false);
301
- this.je = [];
301
+ this.Ye = [];
302
302
  this.Mt = [];
303
303
  this.vt = [];
304
304
  this.wt = 0;
@@ -308,8 +308,8 @@ function updateKeyedMap() {
308
308
  if (this.L && !this.vt.length) {
309
309
  // an aborted fallback attempt leaves an owner without a mapping;
310
310
  // dispose it before re-creating
311
- this.je[0]?.dispose();
312
- this.vt[0] = runWithOwner(this.je[0] = createOwner(), this.L);
311
+ this.Ye[0]?.dispose();
312
+ this.vt[0] = runWithOwner(this.Ye[0] = createOwner(), this.L);
313
313
  }
314
314
  }
315
315
  // fast path for new create
@@ -328,10 +328,10 @@ function updateKeyedMap() {
328
328
  throw t;
329
329
  }
330
330
  // commit
331
- if (this.je[0]) this.je[0].dispose();
331
+ if (this.Ye[0]) this.Ye[0].dispose();
332
332
  // previous fallback
333
333
  this.vt = h;
334
- this.je = c;
334
+ this.Ye = c;
335
335
  r && (this.Kt = r);
336
336
  o && (this.xt = o);
337
337
  this.Mt = t.slice(0);
@@ -447,13 +447,13 @@ function updateKeyedMap() {
447
447
  s = d.get(u);
448
448
  if (s !== undefined && s !== -1) {
449
449
  S[s] = this.vt[i];
450
- M[s - h] = this.je[i];
450
+ M[s - h] = this.Ye[i];
451
451
  r && (r[s - h] = this.Kt[i]);
452
452
  o && (o[s - h] = this.xt[i]);
453
453
  s = p[s - h];
454
454
  d.set(u, s);
455
455
  } else {
456
- (w ??= []).push(this.je[i]);
456
+ (w ??= []).push(this.Ye[i]);
457
457
  if (true && attrHooks !== null) (O ??= []).push(l);
458
458
  }
459
459
  }
@@ -483,7 +483,7 @@ function updateKeyedMap() {
483
483
  // save a copy of the mapped items for the next update
484
484
  this.Mt = t.slice(0);
485
485
  if (w) for (i = 0; i < w.length; i++) w[i].dispose();
486
- if (true && attrHooks !== null && O !== undefined && _ !== undefined) attrHooks.listChurn(this.H.Ht, O, _, e, this.Vt !== undefined);
486
+ if (true && attrHooks !== null && O !== undefined && _ !== undefined) attrHooks.listChurn(this.H.ht, O, _, e, this.Vt !== undefined);
487
487
  }
488
488
  });
489
489
  return this.vt;
@@ -511,7 +511,7 @@ function updateKeyedMap() {
511
511
  $t: 0,
512
512
  qt: t,
513
513
  Wt: s,
514
- je: [],
514
+ Ye: [],
515
515
  vt: [],
516
516
  zt: i?.from,
517
517
  L: i?.fallback
@@ -530,14 +530,14 @@ function updateKeyedMap() {
530
530
  // disjoint-window/front-clear/end-clear/shift special cases.
531
531
  function updateRepeat() {
532
532
  // As in updateKeyedMap: the node is the row callbacks' reader, per pass.
533
- this.H.Ht = getOwner();
533
+ this.H.ht = getOwner();
534
534
  const t = this.qt();
535
535
  const e = this.zt?.() || 0;
536
536
  runWithOwner(this.H, () => {
537
537
  if (t === 0) {
538
538
  if (this.wt !== 0) {
539
539
  this.H.dispose(false);
540
- this.je = [];
540
+ this.Ye = [];
541
541
  this.vt = [];
542
542
  this.wt = 0;
543
543
  // Reset offset to match the cleared data (#2767, repro 2).
@@ -546,8 +546,8 @@ function updateRepeat() {
546
546
  if (this.L && !this.vt.length) {
547
547
  // an aborted fallback attempt leaves an owner without a mapping;
548
548
  // dispose it before re-creating
549
- this.je[0]?.dispose();
550
- this.vt[0] = runWithOwner(this.je[0] = createOwner(), this.L);
549
+ this.Ye[0]?.dispose();
550
+ this.vt[0] = runWithOwner(this.Ye[0] = createOwner(), this.L);
551
551
  }
552
552
  return;
553
553
  }
@@ -560,7 +560,7 @@ function updateRepeat() {
560
560
  const o = new Array(t);
561
561
  const f = new Array(t);
562
562
  for (let t = n; t < r; t++) {
563
- f[t - e] = this.je[t - this.$t];
563
+ f[t - e] = this.Ye[t - this.$t];
564
564
  o[t - e] = this.vt[t - this.$t];
565
565
  }
566
566
  try {
@@ -573,9 +573,9 @@ function updateRepeat() {
573
573
  throw t;
574
574
  }
575
575
  // commit: dispose the previous fallback or the rows leaving the window
576
- if (this.wt === 0) this.je[0]?.dispose(); else for (let t = this.$t; t < s; t++) if (t < e || t >= i) this.je[t - this.$t].dispose();
576
+ if (this.wt === 0) this.Ye[0]?.dispose(); else for (let t = this.$t; t < s; t++) if (t < e || t >= i) this.Ye[t - this.$t].dispose();
577
577
  this.vt = o;
578
- this.je = f;
578
+ this.Ye = f;
579
579
  this.$t = e;
580
580
  this.wt = t;
581
581
  });
@@ -106,7 +106,7 @@ class RevealController {
106
106
  }
107
107
  return t !== "natural";
108
108
  }
109
- oe(t) {
109
+ te(t) {
110
110
  if (this.Zt.includes(t)) return;
111
111
  this.Zt.push(t);
112
112
  // Gated from the start (collapsed in a collapsed sequential tail); the
@@ -124,7 +124,7 @@ class RevealController {
124
124
  }
125
125
  untrack(() => this.Bt());
126
126
  }
127
- te(t) {
127
+ oe(t) {
128
128
  const e = this.Zt.indexOf(t);
129
129
  if (e >= 0) this.Zt.splice(e, 1);
130
130
  untrack(() => this.Bt());
@@ -194,8 +194,8 @@ class RevealController {
194
194
  });
195
195
  if (true) s._name = "reveal order";
196
196
  if (r) {
197
- r.oe(i);
198
- cleanup(() => r.te(i));
197
+ r.te(i);
198
+ cleanup(() => r.oe(i));
199
199
  }
200
200
  return e;
201
201
  });
@@ -204,8 +204,8 @@ class RevealController {
204
204
  // Installed at module evaluation — present exactly when something imports
205
205
  // `createRevealOrder`; a Loading boundary without one pays a null check.
206
206
  setRevealHooks({
207
- oe: (t, e) => t.oe(e),
208
207
  te: (t, e) => t.te(e),
208
+ oe: (t, e) => t.oe(e),
209
209
  P: t => t.Bt()
210
210
  });
211
211
 
@@ -8,7 +8,7 @@ import { computed, signal, runWithOwner, setMemo, setSignal, markRefresh, read,
8
8
 
9
9
  import { registerGraph } from "./core/dev.js";
10
10
 
11
- import { globalQueue, flushTransaction, dirtyQueue, GlobalQueue, resolveTx, passTx, staleReader, joinFuture } from "./core/scheduler.js";
11
+ import { schedule, globalQueue, flushTransaction, dirtyQueue, GlobalQueue, resolveTx, passTx, staleReader, joinFuture } from "./core/scheduler.js";
12
12
 
13
13
  import { cleanup, createRoot, getOwner, dispose } from "./core/owner.js";
14
14
 
@@ -343,14 +343,14 @@ function createMemo(e, t) {
343
343
  // or an already-displayed guess would satisfy it in a later slice. (REDERIVE
344
344
  // after: the first pass's own error is the promise's rejection.)
345
345
  computed(() => {
346
- let i, u = false;
346
+ let u, i = false;
347
347
  try {
348
- i = e();
348
+ u = e();
349
349
  } catch (e) {
350
350
  if (e instanceof NotReadyError) throw e;
351
351
  // An error is an answer (D-41): delivered by the same frame rule.
352
- i = unwrapStatusError(e);
353
- u = true;
352
+ u = unwrapStatusError(e);
353
+ i = true;
354
354
  }
355
355
  const s = getOwner();
356
356
  // A truth reader waits out a `createDeferred` flight its value derives
@@ -363,9 +363,9 @@ function createMemo(e, t) {
363
363
  staleReader(s, resolveTx(a));
364
364
  return;
365
365
  }
366
- if (u) return void queueMicrotask(() => r(i));
367
- o?.(i);
368
- queueMicrotask(() => t(i));
366
+ if (i) return void queueMicrotask(() => r(u));
367
+ o?.(u);
368
+ queueMicrotask(() => t(u));
369
369
  }, {
370
370
  St: n
371
371
  }).l |= CONFIG_REDERIVE;
@@ -525,14 +525,14 @@ function createMemo(e, t) {
525
525
  // (`saving=true` beside the confirmed `version=1` is a tear no timeline
526
526
  // contains). Updates that do not flip the predicate reveal freely.
527
527
  const c = flushTransaction;
528
- createRoot(i => {
529
- let u;
528
+ createRoot(u => {
529
+ let i;
530
530
  let s;
531
531
  const settle = e => {
532
- if (u !== undefined) clearTimeout(u);
532
+ if (i !== undefined) clearTimeout(i);
533
533
  if (s !== undefined) o.removeEventListener("abort", s);
534
534
  e();
535
- i();
535
+ u();
536
536
  };
537
537
  // Authoritative: the predicate sees the truth, never the action's own
538
538
  // optimistic guess — a guess cannot satisfy the wait for its own
@@ -547,7 +547,7 @@ function createMemo(e, t) {
547
547
  // revealed with the action.
548
548
  if (e && c !== null && globalQueue.q && resolveTx(c).tt !== 0) joinFuture(c);
549
549
  });
550
- if (t?.timeout !== undefined) u = setTimeout(() => settle(() => n(new TimeoutError)), t.timeout);
550
+ if (t?.timeout !== undefined) i = setTimeout(() => settle(() => n(new TimeoutError)), t.timeout);
551
551
  if (o !== undefined) {
552
552
  s = () => settle(() => n(o.reason));
553
553
  o.addEventListener("abort", s, {
@@ -663,7 +663,7 @@ function createMemo(e, t) {
663
663
  // still holding the optimistic value — reads do not pull (#3411). Fall
664
664
  // to the next pass while the heap has work; `run` swapped the queue,
665
665
  // so this lands there, and `enqueue` keeps the drain alive.
666
- if (dirtyQueue.jt >= dirtyQueue.st) return globalQueue.enqueue(EFFECT_USER, fire);
666
+ if (dirtyQueue.jt >= dirtyQueue.lt) return globalQueue.enqueue(EFFECT_USER, fire);
667
667
  // Unowned, out-of-band fire (no owner, or a children-forbidden one this
668
668
  // one-shot must not bind to): a returned cleanup has no lifecycle to
669
669
  // attach to. Reject it in dev; in production the return is simply
@@ -672,4 +672,46 @@ function createMemo(e, t) {
672
672
  });
673
673
  }
674
674
 
675
- export { accessor, createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onSettled, refresh, resolve, until };
675
+ /**
676
+ * Schedules `callback` to run **once**, right after the pending flush
677
+ * finishes, in the same microtask checkpoint (before the browser paints).
678
+ * Unlike `onSettled`, it never waits on async work: if that flush is held by
679
+ * a pending transition, the callback still runs.
680
+ *
681
+ * Its main use is keeping a write out of another update's hold. Writes made
682
+ * in the same synchronous frame are applied together, so when one of them
683
+ * starts async work (a route change, a fetch), the others are held with it
684
+ * until it resolves. A write made in `onFlush` happens after that flush, so
685
+ * it is its own update: it commits on its own unless something it changes
686
+ * also derives from the held work (a memo or `createEffect` reading both),
687
+ * in which case it joins that hold as any later write would.
688
+ *
689
+ * Call it from event handlers and effect callbacks (including
690
+ * `createTrackedEffect` and `onSettled` bodies). In an owned scope such as a
691
+ * component body or a computation it warns in dev: use `onSettled` there.
692
+ *
693
+ * - A write in the callback lands after any write made inline in the same
694
+ * frame. Don't write the same signal both inside and outside it; use a
695
+ * functional setter if the order matters.
696
+ * - Several `onFlush` callbacks run in call order and batch into one flush.
697
+ * - `queueMicrotask` is not a substitute: called before the frame's first
698
+ * write, it runs before that flush and its write joins it.
699
+ *
700
+ * @example
701
+ * ```tsx
702
+ * // A menu closes now even when the consumer's onSelect starts async work.
703
+ * const select = (id: string) => {
704
+ * onFlush(() => setOpen(false));
705
+ * props.onSelect?.(id);
706
+ * };
707
+ * ```
708
+ *
709
+ * @param callback Function to run once after the pending flush
710
+ */ function onFlush(e) {
711
+ // The frame's flush is queued (or already draining) ahead of this
712
+ // microtask, so the callback's writes start a flush of their own.
713
+ schedule();
714
+ queueMicrotask(e);
715
+ }
716
+
717
+ export { accessor, createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onFlush, onSettled, refresh, resolve, until };
@@ -72,7 +72,7 @@ function scopeOf(e, o) {
72
72
  if (f !== undefined) {
73
73
  if (f.pb !== null) o.oo.add(f.v);
74
74
  // The writer's view: the tick's own unflushed guesses are in motion too.
75
- if (f.fam?.opt === true) r = optHooks.view(f, r, true);
75
+ if (optHooks && f.fam?.opt === true) r = optHooks.view(f, r, true);
76
76
  const e = f.n;
77
77
  if (e !== null) for (const o of Reflect.ownKeys(e))
78
78
  // Another mark's carrier is its own channel: counting it here would
@@ -1,3 +1,6 @@
1
+ // Set before any family is optimistic (`fam.opt`): a call site checks the
2
+ // hooks first, so a build that does not import optimistic.ts folds the arm
3
+ // away.
1
4
  let optHooks = null;
2
5
 
3
6
  function installOptHooks(o) {