@solidjs/signals 2.0.0-rc.4 → 2.0.0-rc.6

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 (57) hide show
  1. package/dist/dev.js +1618 -232
  2. package/dist/node.cjs +2262 -1199
  3. package/dist/prod/boundaries.js +4 -1
  4. package/dist/prod/core/async.js +152 -101
  5. package/dist/prod/core/constants.js +55 -1
  6. package/dist/prod/core/core.js +305 -234
  7. package/dist/prod/core/effect.js +28 -28
  8. package/dist/prod/core/error.js +13 -1
  9. package/dist/prod/core/external.js +2 -2
  10. package/dist/prod/core/graph.js +27 -27
  11. package/dist/prod/core/heap.js +30 -30
  12. package/dist/prod/core/lanes.js +32 -32
  13. package/dist/prod/core/optimistic.js +57 -57
  14. package/dist/prod/core/owner.js +34 -34
  15. package/dist/prod/core/scheduler.js +346 -152
  16. package/dist/prod/core/verdict.js +122 -65
  17. package/dist/prod/index.js +3 -3
  18. package/dist/prod/map.js +106 -106
  19. package/dist/prod/signals.js +321 -26
  20. package/dist/prod/store/next/optimistic.js +363 -151
  21. package/dist/prod/store/next/patch.js +6 -6
  22. package/dist/prod/store/next/projection.js +25 -21
  23. package/dist/prod/store/next/store.js +223 -113
  24. package/dist/prod/store/store.js +2 -2
  25. package/dist/types/core/async.d.ts +2 -0
  26. package/dist/types/core/attribution-hooks.d.ts +11 -0
  27. package/dist/types/core/attribution.d.ts +57 -4
  28. package/dist/types/core/constants.d.ts +54 -0
  29. package/dist/types/core/core.d.ts +19 -20
  30. package/dist/types/core/dev.d.ts +8 -2
  31. package/dist/types/core/error.d.ts +9 -0
  32. package/dist/types/core/index.d.ts +2 -2
  33. package/dist/types/core/scheduler.d.ts +39 -0
  34. package/dist/types/core/types.d.ts +12 -0
  35. package/dist/types/index.d.ts +3 -3
  36. package/dist/types/signals.d.ts +108 -11
  37. package/dist/types/store/next/optimistic.d.ts +13 -10
  38. package/dist/types/store/next/projection.d.ts +1 -1
  39. package/dist/types/store/next/store.d.ts +22 -0
  40. package/dist/types/store/next/target.d.ts +25 -0
  41. package/dist/types-cjs/core/async.d.cts +2 -0
  42. package/dist/types-cjs/core/attribution-hooks.d.cts +11 -0
  43. package/dist/types-cjs/core/attribution.d.cts +57 -4
  44. package/dist/types-cjs/core/constants.d.cts +54 -0
  45. package/dist/types-cjs/core/core.d.cts +19 -20
  46. package/dist/types-cjs/core/dev.d.cts +8 -2
  47. package/dist/types-cjs/core/error.d.cts +9 -0
  48. package/dist/types-cjs/core/index.d.cts +2 -2
  49. package/dist/types-cjs/core/scheduler.d.cts +39 -0
  50. package/dist/types-cjs/core/types.d.cts +12 -0
  51. package/dist/types-cjs/index.d.cts +3 -3
  52. package/dist/types-cjs/signals.d.cts +108 -11
  53. package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
  54. package/dist/types-cjs/store/next/projection.d.cts +1 -1
  55. package/dist/types-cjs/store/next/store.d.cts +22 -0
  56. package/dist/types-cjs/store/next/target.d.cts +25 -0
  57. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
- import { computed, optimisticComputed, setSignal, optimisticSignal, runWithOwner, setMemo, signal, read, untrack } from "./core/core.js";
1
+ import { TimeoutError } from "./core/error.js";
2
2
 
3
- import { cleanup, createRoot, getOwner, dispose } from "./core/owner.js";
3
+ import { computed, optimisticComputed, setSignal, optimisticSignal, runWithOwner, setMemo, signal, markRefresh, installAuthoritativeRead, read, untrack } from "./core/core.js";
4
4
 
5
- import { globalQueue, Queue } from "./core/scheduler.js";
5
+ import { cleanup, createRoot, getOwner, dispose, getObserver } from "./core/owner.js";
6
6
 
7
- import { CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, EFFECT_USER, $REFRESH } from "./core/constants.js";
7
+ import { globalQueue, Queue, entangleConfirmingTransitions, activeTransition } from "./core/scheduler.js";
8
+
9
+ import { CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, EFFECT_USER, $REFRESH, CONFIG_DIRECT_COMMIT, CONFIG_AUTHORITATIVE_READ, CONFIG_FRESH_READ } from "./core/constants.js";
8
10
 
9
11
  import "./core/invariants.js";
10
12
 
@@ -76,7 +78,74 @@ function createMemo(e, t) {
76
78
  return accessor(computed(e, t));
77
79
  }
78
80
 
79
- function createEffect(e, t, n) {
81
+ /**
82
+ * Creates a reactive effect with **separate compute and effect phases**.
83
+ *
84
+ * - `compute(prev)` runs reactively — *put all reactive reads here*. The
85
+ * returned value is passed to `effect` and is also the new "previous" value
86
+ * for the next run.
87
+ * - `effect(next, prev?)` runs imperatively (untracked) after the queue
88
+ * flushes. *Put DOM writes / fetch / logging / subscriptions here.* It may
89
+ * return a cleanup function which runs before the next effect or on
90
+ * disposal.
91
+ *
92
+ * Reactive reads inside `effect` will *not* re-trigger this effect — that's
93
+ * intentional. If you need a single-phase tracked effect, use
94
+ * `createTrackedEffect` (with the tradeoffs noted there).
95
+ *
96
+ * Pass an `EffectBundle` (`{ effect, error }`) instead of a plain function to
97
+ * intercept **compute-phase** errors — errors thrown by `compute` or arriving
98
+ * from upstream reactive sources (including async rejections), which your own
99
+ * code has no frame to `try/catch`. The `error` handler is the error arm of
100
+ * the effect phase: it runs on the same schedule and in the same imperative,
101
+ * writable scope as `effect` (setting error state via signals is fine), and
102
+ * only for *settled* errors — a transient error that recovers before the
103
+ * effect phase runs `effect` with the recovered value instead, and a held
104
+ * transition defers it exactly as it defers `effect`. Without an `error`
105
+ * handler a compute-phase error is logged and the effect simply skips that
106
+ * run — a non-render effect's reactivity failing does not crash the app.
107
+ * Rethrowing from `error` escalates it to the nearest error boundary
108
+ * (halting the system if none exists).
109
+ *
110
+ * The **effect phase is different**: it is your own imperative code, so handle
111
+ * failures with `try/catch` where they occur. An uncaught effect-phase throw
112
+ * is treated as an unhandled application error — caught by the nearest
113
+ * `createErrorBoundary`/`<Errored>`, and permanently halting the reactive
114
+ * system if there is none. It is *not* routed to the bundle's `error` handler.
115
+ *
116
+ * ```typescript
117
+ * createEffect<T>(compute, effectFn | { effect, error }, options?: EffectOptions);
118
+ * ```
119
+ * @param compute a function that receives its previous value and returns a new value used to react on a computation
120
+ * @param effectFn a function that receives the new value and is used to perform side effects (return a cleanup function), or an `EffectBundle` with `effect` and `error` handlers
121
+ * @param options `EffectOptions` -- name, defer, schedule, transparent
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * const [count, setCount] = createSignal(0);
126
+ *
127
+ * createEffect(
128
+ * () => count(), // compute: tracks `count`
129
+ * value => console.log(value) // effect: side effect
130
+ * );
131
+ *
132
+ * setCount(1); // logs 1 after the next flush
133
+ * ```
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * createEffect(
138
+ * () => userId(),
139
+ * id => {
140
+ * const ctrl = new AbortController();
141
+ * fetch(`/users/${id}`, { signal: ctrl.signal });
142
+ * return () => ctrl.abort(); // cleanup before next run / disposal
143
+ * }
144
+ * );
145
+ * ```
146
+ *
147
+ * @description https://docs.solidjs.com/reference/basic-reactivity/create-effect
148
+ */ function createEffect(e, t, n) {
80
149
  effect(e, t.effect || t, t.error, {
81
150
  user: true,
82
151
  ...n
@@ -181,25 +250,25 @@ function createEffect(e, t, n) {
181
250
  */ function createReaction(e, t) {
182
251
  let n = undefined;
183
252
  cleanup(() => n?.());
184
- const c = getOwner();
253
+ const r = getOwner();
185
254
  // The currently armed effect node. `track()` replaces the previous
186
255
  // subscription (1.x semantics): without disposing the superseded arm, its
187
256
  // sources stayed live (firing the callback for replaced dependencies), each
188
257
  // accumulated arm delivered its own fire, and un-fired arms leaked as live
189
258
  // effect nodes until the owner disposed (#2861).
190
- let r;
191
- return o => {
192
- if (r) {
193
- dispose(r);
194
- r = undefined;
259
+ let o;
260
+ return i => {
261
+ if (o) {
262
+ dispose(o);
263
+ o = undefined;
195
264
  }
196
- runWithOwner(c, () => {
197
- effect(() => (o(), r = getOwner()), t => {
198
- r = undefined;
265
+ runWithOwner(r, () => {
266
+ effect(() => (i(), o = getOwner()), t => {
267
+ o = undefined;
199
268
  n?.();
200
- const c = (e.effect || e)?.();
201
- if (false && c !== undefined && typeof c !== "function") ;
202
- n = c;
269
+ const r = (e.effect || e)?.();
270
+ if (false && r !== undefined && typeof r !== "function") ;
271
+ n = r;
203
272
  dispose(t);
204
273
  }, e.error, {
205
274
  ...false ? {
@@ -241,7 +310,7 @@ function createEffect(e, t, n) {
241
310
  * @param fn a reactive expression to resolve
242
311
  */ function resolve(e) {
243
312
  return new Promise((t, n) => {
244
- createRoot(c => {
313
+ createRoot(r => {
245
314
  // Deliver effect applies on a microtask instead of the owner queue: an
246
315
  // incomplete transition stashes its effect queues until it settles, but
247
316
  // an action yielding this promise is itself what keeps the transition
@@ -249,26 +318,252 @@ function createEffect(e, t, n) {
249
318
  // still runs in place (under the transaction's view when created inside
250
319
  // an action step), and status/boundary notifications keep their normal
251
320
  // route through the inherited queue.
252
- const r = getOwner();
253
- const o = new MicrotaskQueue;
254
- o.ke = r.C;
321
+ const o = getOwner();
322
+ const i = new MicrotaskQueue;
323
+ i.ke = o.C;
255
324
  // notify() forwards up the normal chain
256
- r.C = o;
325
+ o.C = i;
257
326
  // A user effect rather than a bare computed: computeds are pull-based and
258
327
  // are only re-enqueued when a pending source *resolves* — a rejection just
259
328
  // marks them errored, so nothing would re-run and the promise would never
260
329
  // settle (#2842). The effect's error channel is notified on rejection.
261
330
  effect(e, e => {
262
331
  t(e);
263
- c();
332
+ r();
264
333
  }, e => {
265
334
  // The error arm already unwraps StatusError (#2840) — `err` is the
266
335
  // user's original error, matching what error boundaries expose.
267
336
  n(e);
268
- c();
337
+ r();
338
+ },
339
+ // DIRECT_COMMIT: a source settling INTO the held transaction (e.g. a
340
+ // refresh this action issued) stages its landing; the effect's own
341
+ // recompute must not stage too, or the microtask apply reads the
342
+ // stale mainline value and resolves with old data.
343
+ {
344
+ user: true,
345
+ dt: CONFIG_DIRECT_COMMIT
346
+ });
347
+ });
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Invalidates one reactive source, forcing it to re-execute even if its inputs
353
+ * haven't changed, and returns a promise for the target's NEXT QUIESCENT
354
+ * STATE — the re-ask (and anything that supersedes it) has settled.
355
+ *
356
+ * Pass either a Solid-created accessor or a projected store created from
357
+ * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
358
+ * write-like invalidation operation: it does not read the target's value, and
359
+ * refreshing a plain signal accessor is a no-op that resolves immediately.
360
+ *
361
+ * The returned promise is safe to ignore (fire-and-forget refresh is
362
+ * unchanged, and a failed refetch will not surface an unhandled rejection).
363
+ * Awaiting it gives imperative flows the settle point without a reactive
364
+ * read:
365
+ * - Accessor targets resolve with the settled value; store targets resolve
366
+ * with the store node passed (reads through it are fresh after the await).
367
+ * - A failed re-ask rejects with the error (inside an action's generator,
368
+ * `yield refresh(x)` throws back at the yield point and the action reverts
369
+ * like any other failure).
370
+ * - Semantics are quiescence, not flight identity: if another refresh (or
371
+ * any invalidation) supersedes this one mid-flight, the promise waits for
372
+ * — and delivers — whatever finally lands.
373
+ * - Inside an action, truth landing into the held transaction is STAGED;
374
+ * the promise still settles then (matching `resolve()`/`until()`, #2930)
375
+ * and delivers the staged value — the caller's own optimistic override is
376
+ * never the delivered value.
377
+ * - The re-ask itself stays verdict-quiet exactly as before: `isPending`
378
+ * does not flip for a bare refresh (pair with `affects()` for a visible
379
+ * pending window).
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
384
+ *
385
+ * // Fire-and-forget re-fetch
386
+ * <button onClick={() => refresh(user)}>Reload</button>;
387
+ *
388
+ * // Imperative settle point
389
+ * const fresh = await refresh(user);
390
+ * ```
391
+ */ function refresh(e) {
392
+ const t = e?.[$REFRESH];
393
+ if (!t) {
394
+ return Promise.resolve(undefined);
395
+ }
396
+ // Mark now, watch on a microtask. The waiter is resolve()'s machinery with
397
+ // two extra reader bits, but it must NOT compute at call time (effects
398
+ // recompute eagerly on creation): same-tick refreshes coalesce into ONE
399
+ // re-ask only because every mark lands before anything pulls, and eager
400
+ // per-call pulls turned three refreshes into three fetches. Deferred, the
401
+ // waiter's first read sees the coalesced state: FRESH_READ pulls the node
402
+ // through recompute if it is still dirty (self-deduping — a clean node
403
+ // no-ops, so N waiters cost one pull; this also closes the race where a
404
+ // waiter reads the PRE-re-ask value as settled and delivers stale), after
405
+ // which the read either parks on the re-ask's pending window (async — the
406
+ // settle walk re-runs it on every landing, equal-value and
407
+ // staged-under-hold included, and a rejection arrives through the effect's
408
+ // error channel) or serves the sync answer. AUTHORITATIVE_READ keeps an
409
+ // action's own optimistic override out of the delivered value. resolve()'s
410
+ // own eager compute is untouched: created after a refresh it still settles
411
+ // stale-while-revalidate (#2930) — its contract is "first settled value",
412
+ // not "next quiescent state".
413
+ markRefresh(t);
414
+ const n = new Promise((n, r) => {
415
+ queueMicrotask(() => {
416
+ // No createRoot: the microtask has no ambient owner, so the effect is
417
+ // naturally detached, and settle disposes the node directly — the root
418
+ // added ~560B of otherwise-shakeable machinery for nothing but the
419
+ // dev-mode NO_OWNER_EFFECT warning, so dev keeps a root husk purely to
420
+ // stay quiet. The waiter swaps in its microtask queue during its own
421
+ // first compute (before the initial apply enqueue), replacing the
422
+ // root-owner plumbing.
423
+ // Typed as the effect node, not Owner: the capture runs inside the
424
+ // effect's own compute, where the ambient owner IS the effect —
425
+ // exactly what dispose() takes.
426
+ let o = null;
427
+ const make = () => effect(() => {
428
+ if (o === null) {
429
+ o = getOwner();
430
+ const e = new MicrotaskQueue;
431
+ e.ke = o.C;
432
+ o.C = e;
433
+ }
434
+ return read(t);
435
+ }, t => {
436
+ n(typeof e === "function" ? t : e);
437
+ dispose(o);
438
+ }, e => {
439
+ r(e);
440
+ dispose(o);
269
441
  }, {
270
- user: true
442
+ user: true,
443
+ dt: CONFIG_DIRECT_COMMIT | CONFIG_AUTHORITATIVE_READ | CONFIG_FRESH_READ
444
+ });
445
+ make();
446
+ });
447
+ });
448
+ // Fire-and-forget refresh must not turn a failed refetch into an unhandled
449
+ // rejection; awaiting callers attach their own handlers to `promise`.
450
+ n.catch(() => {});
451
+ return n;
452
+ }
453
+
454
+ /**
455
+ * Awaits a reactive predicate and resolves the first time it settles *truthy*,
456
+ * with that (narrowed) value. Falsy results and pending async reads both mean
457
+ * "not yet": the subscription stays live and re-evaluates as sources change.
458
+ * If the predicate settles with an error — a throw, or an async source that
459
+ * rejects — the promise rejects with it, as do timeout and abort.
460
+ *
461
+ * Where {@link resolve} answers "what is this value" (first settled value,
462
+ * whatever it is), `until` answers "when does the world confirm this
463
+ * condition". The difference matters inside an `action()`: `yield until(...)`
464
+ * holds the action's transaction — and any optimistic state riding it — open
465
+ * until the condition is independently true.
466
+ *
467
+ * To make that sound, `until`'s predicate reads the AUTHORITATIVE view — and
468
+ * this is the one read-semantics difference from `resolve`, which reads the
469
+ * normal (transaction's own) view where overrides are visible:
470
+ *
471
+ * - **Optimistic overrides are invisible** to the predicate. Your own
472
+ * tentative write can never satisfy your own ack, even on the
473
+ * single-primitive shape where the optimistic store IS the live-fed store.
474
+ * (Derived computeds serve their normal cached values — express the
475
+ * condition over sources of truth, not derived views of the overlay.)
476
+ * - **Everything else reads normally, including uncommitted transition-staged
477
+ * data.** Real data is real wherever it currently lives. This is
478
+ * load-bearing, not a loophole: truth that arrives *into* the open
479
+ * transaction (a `refresh()` this action issued, an entangled landing)
480
+ * stages and cannot commit until the hold releases — a predicate that
481
+ * refused staged reads would deadlock on the very data it is waiting for.
482
+ *
483
+ * This is the acknowledgment mechanism for mutations confirmed on a live data
484
+ * channel (sockets, subscriptions, live queries) rather than by the mutation's
485
+ * own response: correlate by a client-generated id or version in the predicate,
486
+ * and let truth arrive however it arrives — push, refetch, or another tab.
487
+ *
488
+ * Failure composes with action semantics: a rejection is thrown back into the
489
+ * generator at the `yield` point — catchable there, or the action fails and
490
+ * its optimistic state reverts.
491
+ *
492
+ * Must be called *outside* a tracking scope.
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * const send = action(async function* (text: string) {
497
+ * const clientId = crypto.randomUUID();
498
+ * setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
499
+ * await socket.send({ clientId, text }); // fire-and-forget transport
500
+ * // Hold until the live source echoes the write (authoritative view —
501
+ * // the optimistic row above cannot satisfy this):
502
+ * yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
503
+ * });
504
+ * ```
505
+ *
506
+ * @param fn a reactive predicate over authoritative state
507
+ * @param options optional `timeout` (ms) and abort `signal`
508
+ */ function until(e, t) {
509
+ // Late-bind the wakeup hook for the A17-silent ack paths (pay-for-use:
510
+ // apps that never call until() never retain it).
511
+ installAuthoritativeRead();
512
+ // Flip-entanglement (#3164 follow-up): the transaction this until() holds
513
+ // open (the action's, when yielded from one). The predicate is the user's
514
+ // declaration of what confirms it — when a foreign transition's staged
515
+ // write flips it truthy, that transition merges here and reveals at the
516
+ // joint settle instead of painting the confirmation under live optimism.
517
+ const n = activeTransition;
518
+ return new Promise((r, o) => {
519
+ const i = t?.signal;
520
+ if (i?.aborted) return o(i.reason);
521
+ createRoot(c => {
522
+ // Same delivery contract as resolve() (#2930): effect applies ride a
523
+ // microtask so the promise can settle while the transaction the caller
524
+ // yielded it into is still open — that transaction being open is the
525
+ // entire point of the hold.
526
+ const s = getOwner();
527
+ const u = new MicrotaskQueue;
528
+ u.ke = s.C;
529
+ s.C = u;
530
+ let f;
531
+ let a;
532
+ const settle = e => {
533
+ if (f !== undefined) clearTimeout(f);
534
+ if (a !== undefined) i.removeEventListener("abort", a);
535
+ e();
536
+ c();
537
+ };
538
+ effect(n === null ? e : () => {
539
+ const t = e();
540
+ // Runs inside the compute (pure phase): the confirming
541
+ // transition's stamps are live and its commit hasn't run, so
542
+ // the merge lands before any reveal. Falsy evaluations skip —
543
+ // non-flipping updates were never named as the confirmation.
544
+ if (t) entangleConfirmingTransitions(getObserver(), n);
545
+ return t;
546
+ }, e => {
547
+ // Falsy is "not yet": keep the subscription live and wait for the
548
+ // next evaluation. Only a truthy settled value resolves.
549
+ if (e) settle(() => r(e));
550
+ }, e => settle(() => o(e)),
551
+ // AUTHORITATIVE_READ: overrides invisible to the predicate.
552
+ // DIRECT_COMMIT: truth that stages into the held transaction (a
553
+ // refresh the action issued) must flow through to the microtask
554
+ // apply — a staged effect value would deadlock the hold on data
555
+ // the hold itself is keeping uncommitted.
556
+ {
557
+ user: true,
558
+ dt: CONFIG_AUTHORITATIVE_READ | CONFIG_DIRECT_COMMIT
271
559
  });
560
+ if (t?.timeout !== undefined) f = setTimeout(() => settle(() => o(new TimeoutError)), t.timeout);
561
+ if (i !== undefined) {
562
+ a = () => settle(() => o(i.reason));
563
+ i.addEventListener("abort", a, {
564
+ once: true
565
+ });
566
+ }
272
567
  });
273
568
  });
274
569
  }
@@ -391,4 +686,4 @@ function createOptimistic(e, t) {
391
686
  });
392
687
  }
393
688
 
394
- export { accessor, createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onSettled, resolve };
689
+ export { accessor, createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onSettled, refresh, resolve, until };