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

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.js +2494 -271
  2. package/dist/node.cjs +3691 -1595
  3. package/dist/prod/affects.js +13 -12
  4. package/dist/prod/boundaries.js +43 -35
  5. package/dist/prod/core/action.js +3 -3
  6. package/dist/prod/core/async.js +137 -106
  7. package/dist/prod/core/constants.js +55 -1
  8. package/dist/prod/core/core.js +354 -247
  9. package/dist/prod/core/effect.js +47 -50
  10. package/dist/prod/core/error.js +13 -1
  11. package/dist/prod/core/external.js +4 -4
  12. package/dist/prod/core/graph.js +88 -52
  13. package/dist/prod/core/heap.js +38 -38
  14. package/dist/prod/core/lanes.js +34 -34
  15. package/dist/prod/core/optimistic.js +61 -58
  16. package/dist/prod/core/owner.js +38 -38
  17. package/dist/prod/core/scheduler.js +401 -174
  18. package/dist/prod/core/verdict.js +132 -65
  19. package/dist/prod/index.js +7 -3
  20. package/dist/prod/map.js +106 -106
  21. package/dist/prod/signals.js +253 -25
  22. package/dist/prod/store/index.js +2 -0
  23. package/dist/prod/store/next/optimistic.js +314 -121
  24. package/dist/prod/store/next/patch-hooks.js +13 -0
  25. package/dist/prod/store/next/patch.js +614 -0
  26. package/dist/prod/store/next/projection.js +23 -19
  27. package/dist/prod/store/next/reconcile.js +307 -120
  28. package/dist/prod/store/next/store.js +440 -117
  29. package/dist/prod/store/next/target.js +13 -4
  30. package/dist/prod/store/store.js +5 -5
  31. package/dist/types/core/async.d.ts +2 -0
  32. package/dist/types/core/attribution.d.ts +9 -4
  33. package/dist/types/core/constants.d.ts +54 -0
  34. package/dist/types/core/core.d.ts +34 -21
  35. package/dist/types/core/dev.d.ts +8 -0
  36. package/dist/types/core/error.d.ts +9 -0
  37. package/dist/types/core/graph.d.ts +22 -0
  38. package/dist/types/core/index.d.ts +2 -2
  39. package/dist/types/core/scheduler.d.ts +46 -0
  40. package/dist/types/core/types.d.ts +12 -0
  41. package/dist/types/index.d.ts +3 -3
  42. package/dist/types/signals.d.ts +108 -0
  43. package/dist/types/store/index.d.ts +2 -0
  44. package/dist/types/store/next/optimistic.d.ts +13 -10
  45. package/dist/types/store/next/patch-hooks.d.ts +41 -0
  46. package/dist/types/store/next/patch.d.ts +91 -0
  47. package/dist/types/store/next/projection.d.ts +1 -1
  48. package/dist/types/store/next/reconcile.d.ts +14 -0
  49. package/dist/types/store/next/store.d.ts +52 -2
  50. package/dist/types/store/next/target.d.ts +73 -8
  51. package/dist/types-cjs/core/async.d.cts +2 -0
  52. package/dist/types-cjs/core/attribution.d.cts +9 -4
  53. package/dist/types-cjs/core/constants.d.cts +54 -0
  54. package/dist/types-cjs/core/core.d.cts +34 -21
  55. package/dist/types-cjs/core/dev.d.cts +8 -0
  56. package/dist/types-cjs/core/error.d.cts +9 -0
  57. package/dist/types-cjs/core/graph.d.cts +22 -0
  58. package/dist/types-cjs/core/index.d.cts +2 -2
  59. package/dist/types-cjs/core/scheduler.d.cts +46 -0
  60. package/dist/types-cjs/core/types.d.cts +12 -0
  61. package/dist/types-cjs/index.d.cts +3 -3
  62. package/dist/types-cjs/signals.d.cts +108 -0
  63. package/dist/types-cjs/store/index.d.cts +2 -0
  64. package/dist/types-cjs/store/next/optimistic.d.cts +13 -10
  65. package/dist/types-cjs/store/next/patch-hooks.d.cts +41 -0
  66. package/dist/types-cjs/store/next/patch.d.cts +91 -0
  67. package/dist/types-cjs/store/next/projection.d.cts +1 -1
  68. package/dist/types-cjs/store/next/reconcile.d.cts +14 -0
  69. package/dist/types-cjs/store/next/store.d.cts +52 -2
  70. package/dist/types-cjs/store/next/target.d.cts +73 -8
  71. package/package.json +2 -2
@@ -7,14 +7,23 @@ const ownedRaw = new WeakSet;
7
7
 
8
8
  /** raw → target. The only raw-keyed lookup; boundary mechanism (O8). */ const storeNextLookup = new WeakMap;
9
9
 
10
- function devAssertNeverUserMutation(o) {
10
+ function devAssertNeverUserMutation(e) {
11
11
  return;
12
12
  }
13
13
 
14
14
  let optHooks = null;
15
15
 
16
- function setOptHooks(o) {
17
- optHooks = o;
16
+ function setOptHooks(e) {
17
+ optHooks = e;
18
18
  }
19
19
 
20
- export { devAssertNeverUserMutation, optHooks, ownedRaw, setOptHooks, storeNextLookup };
20
+ /** Sticky descendants flag walk (§6d): reconcile's keyed pruning descends
21
+ * only where subscriptions exist at/below. Nodes AND patches count. */ function markDescendants(e) {
22
+ let t = e;
23
+ while (t && !t.d) {
24
+ t.d = true;
25
+ t = t.u;
26
+ }
27
+ }
28
+
29
+ export { devAssertNeverUserMutation, markDescendants, optHooks, ownedRaw, setOptHooks, storeNextLookup };
@@ -139,7 +139,7 @@ function ownEnumerableKeys(e) {
139
139
  // A live scope exists, so affects.ts already installed the mark engine.
140
140
  for (const [r, s] of affectsScopes) {
141
141
  if (r.o?.t && s.scope.has(t) && (s.key === undefined || s.key === o)) {
142
- GlobalQueue.h(e);
142
+ GlobalQueue.M(e);
143
143
  s.inherited.push(e);
144
144
  }
145
145
  }
@@ -252,7 +252,7 @@ s) {
252
252
  // Callers guard on `pendingCheckActive`, which only flips inside
253
253
  // isPending() — the verdict layer is loaded and its hook installed.
254
254
  const o = e[STORE_NODE]?.[$AFFECTS];
255
- if (o?.o?.t) GlobalQueue.Bt(o);
255
+ if (o?.o?.t) GlobalQueue.Wt(o);
256
256
  if (affectsScopes.size) {
257
257
  // Chained backings (§7b): a wrapper's STORE_VALUE can be another store's
258
258
  // proxy — marks cover by identity of the BASE raw, so resolve the chain
@@ -263,7 +263,7 @@ s) {
263
263
  let t = r;
264
264
  for (;;) {
265
265
  if (s.scope.has(t)) {
266
- GlobalQueue.Bt(e);
266
+ GlobalQueue.Wt(e);
267
267
  break;
268
268
  }
269
269
  const o = t?.[$TARGET];
@@ -288,11 +288,11 @@ s) {
288
288
  *
289
289
  * @internal
290
290
  */ function getStoreAffectsNodes(e, t) {
291
- GlobalQueue.G ||= e => {
291
+ GlobalQueue.p ||= e => {
292
292
  const t = affectsScopes.get(e);
293
293
  if (!t) return;
294
294
  affectsScopes.delete(e);
295
- for (let e = 0; e < t.inherited.length; e++) GlobalQueue.j(t.inherited[e]);
295
+ for (let e = 0; e < t.inherited.length; e++) GlobalQueue.N(t.inherited[e]);
296
296
  };
297
297
  if (t === undefined) {
298
298
  const t = nextAffectsNodeResolver(e, $AFFECTS);
@@ -16,6 +16,8 @@ export declare function releaseSettledDependents(el: Computed<any>): void;
16
16
  export declare function settleErroredDependents(el: Computed<any>, error: any): void;
17
17
  export declare function settlePendingSource(el: Computed<any>): void;
18
18
  export declare function isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T>;
19
+ /** Fire and clear a node's iterator-flight cancellation hook (#3122). */
20
+ export declare function releaseFlightTeardown(el: Computed<any>): void;
19
21
  export declare function handleAsync<T>(el: Computed<T>, result: T | PromiseLike<T> | AsyncIterable<T>, setter?: (value: T) => void): T;
20
22
  export declare function clearStatus(el: Computed<any>, clearUninitialized?: boolean): void;
21
23
  export declare function notifyStatus(el: Computed<any>, status: number, error: any, blockStatus?: boolean, lane?: OptimisticLane): void;
@@ -60,11 +60,16 @@ export interface RerunEvent {
60
60
  /** Wall time of this run including nested recomputes (ms). */
61
61
  totalMs: number;
62
62
  /**
63
- * Whether the run committed a changed value. A PLAIN memo run with
63
+ * Whether the run produced a changed value. A PLAIN memo run with
64
64
  * `changed: false` was pure waste — the equality cutoff stopped it from
65
- * notifying anyone; an effect run with `changed: false` computed without
66
- * firing its effect phase. Summed as `wastedMs` in costs() (plain,
67
- * non-held runs only see `phase`).
65
+ * notifying anyone. Effects run with `_equals: false` in core (their
66
+ * effect phase re-fires on every recompute), so the engine derives this
67
+ * fact itself: an effect run whose compute output is identical to the
68
+ * previous run's reports `changed: false` — the phase re-fired with the
69
+ * same input, pure waste. Side-effect-only computes (`undefined` output)
70
+ * are exempt: identity of `undefined` proves nothing about their work.
71
+ * Summed as `wastedMs` in costs() (plain, non-held runs only — see
72
+ * `phase`).
68
73
  */
69
74
  changed: boolean;
70
75
  /**
@@ -51,6 +51,60 @@ export declare const CONFIG_CHILD_COMPANIONS: number;
51
51
  * moved into the cold extension (§12), and an unconditional `_x` deref per
52
52
  * marked node measurably taxed the propagation hot path (diamond -22%). */
53
53
  export declare const CONFIG_FW_CHILDREN: number;
54
+ /** Authoritative-view reader (`until()`): while this node computes, reads
55
+ * dodge active optimistic OVERRIDES only — the predicate must observe
56
+ * arriving truth, never the caller's own tentative writes (which would
57
+ * trivially satisfy it). Everything else reads normally, INCLUDING
58
+ * transition-staged `_pendingValue`: staged data is authoritative (optimism
59
+ * lives only in override slots), and a hold that refused staged reads would
60
+ * deadlock on data the open transaction itself is holding (a refresh the
61
+ * action issued lands staged and cannot commit until the hold releases).
62
+ * read() checks the bit on the reading computation (`context`) directly — no
63
+ * ambient flag — so a shared computed the predicate pulls recomputes as
64
+ * itself (no bit) under the normal view, and its cache never forks. */
65
+ export declare const CONFIG_AUTHORITATIVE_READ: number;
66
+ /** Sticky mark: an authoritative-view reader read this node PAST an active
67
+ * override. The ack shape — an authoritative arrival EQUAL to the override —
68
+ * rides paths that are deliberately silent under A17 (every ordinary reader
69
+ * sees the override, so an equal landing changes nothing for them). A marked
70
+ * node notifies those readers on such paths anyway, so the landed truth is
71
+ * seen without re-firing ordinary subscribers. Never cleared — only nodes an
72
+ * until() predicate observed mid-override pay. */
73
+ export declare const CONFIG_AUTHORITATIVE_OBSERVED: number;
74
+ /** Promise-delivery effect (resolve()/until()): commits its computed value
75
+ * directly even when recomputing under its own held transition. These
76
+ * effects deliver applies on a microtask (#2930) instead of the stashed
77
+ * effect queues, so the value must ride the same immediate schedule — a
78
+ * staged value with an immediate apply delivers stale state (resolve) or
79
+ * deadlocks the hold (until). Safe because the node is a private leaf: no
80
+ * subscriber reads an effect's value, only its own apply does. */
81
+ export declare const CONFIG_DIRECT_COMMIT: number;
82
+ /** Fresh-pull reader (awaitable `refresh()`'s waiter effect): a read of a
83
+ * dirty source recomputes it inline even when the height gate defers to the
84
+ * flush. Closes the same-flush ordering race where a waiter created
85
+ * alongside a refresh() mark read the PRE-re-ask value as settled and
86
+ * delivered stale; with the pull, the waiter either parks on the re-ask's
87
+ * pending window (async — woken by the settle walk, which runs on every
88
+ * landing including equal-value ones) or serves its sync answer. resolve()
89
+ * deliberately keeps that race — its contract is "first settled value"
90
+ * (#2930), not "next quiescent state". */
91
+ export declare const CONFIG_FRESH_READ: number;
92
+ /** HELD truth (#3164): this node's staged `_pendingValue` is confirming
93
+ * truth riding a transaction that retains optimism, revealed only at that
94
+ * transaction's settle. Two arming sites, one meaning: the store fold
95
+ * (a landing staged into the retaining transaction) and until()'s
96
+ * flip-entanglement (a foreign carrier's staged write, stolen when it
97
+ * flipped the awaited predicate truthy). Until the reveal, ordinary
98
+ * readers — lane and speculative recomputes included — keep committed:
99
+ * the staging notified subscribers as a plain write, so without the mask
100
+ * a mid-hold recompute composes live optimism with the confirming truth,
101
+ * a frame no timeline contains (GabbeV's union tear). Authoritative
102
+ * readers (until()'s predicate) and latest() tunnel through — the
103
+ * exemption that keeps holds deadlock-free. Override-covered nodes never
104
+ * arm: the override is their display and its revert their notification
105
+ * (A17). Cleared at commit (the commit IS the reveal); subscribers masked
106
+ * during the hold are woken by finalizePureQueue's post-revert pass. */
107
+ export declare const CONFIG_HELD_TRUTH: number;
54
108
  export declare const STATUS_NONE = 0;
55
109
  export declare const STATUS_PENDING: number;
56
110
  export declare const STATUS_ERROR: number;
@@ -1,4 +1,3 @@
1
- import { type Refreshable } from "./constants.js";
2
1
  import { type OptimisticLane } from "./lanes.js";
3
2
  import type { Computed, FirewallSignal, NodeExtension, NodeOptions, Owner, Signal } from "./types.js";
4
3
  export declare const PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE = "[PRIMITIVE_IN_FORBIDDEN_SCOPE] Cannot create reactive primitives inside createTrackedEffect or owner-backed onSettled";
@@ -38,7 +37,21 @@ export declare function ext(el: {
38
37
  * mode (recompute is called explicitly by `effect()`), so we hardcode the lazy bits and skip
39
38
  * the auto-dispose CONFIG bit (effect() previously cleared it post-construction).
40
39
  */
41
- export declare function createEffectNode<T>(fn: (prev?: T) => T, effectFn: (val: T, prev: T | undefined) => void | (() => void), errorFn: ((err: unknown, cleanup: () => void) => void | (() => void)) | undefined, type: number, notifyStatus: ((status?: number, error?: any) => void) | undefined, options: NodeOptions<T> | undefined): any;
40
+ export declare function createEffectNode<T>(fn: (prev?: T) => T, effectFn: (val: T, prev: T | undefined) => void | (() => void), errorFn: ((err: unknown, cleanup: () => void) => void | (() => void)) | undefined, type: number, options: NodeOptions<T> | undefined): any;
41
+ /**
42
+ * The shared status notifier for effect nodes, installed once by effect.ts
43
+ * at module evaluation (`this`-dispatched — one function serves every
44
+ * effect, so nodes never store it). Boundary computeds keep their own
45
+ * per-node channel on `_x._notifyStatus`, which takes precedence.
46
+ */
47
+ export declare let effectStatusNotify: ((this: any, status?: number, error?: any) => void) | null;
48
+ export declare function setEffectStatusNotify(fn: NonNullable<typeof effectStatusNotify>): void;
49
+ /** Resolve a node's status notifier: an own `_x` channel (boundaries) wins;
50
+ * effect nodes (`_type` — EFFECT_PURE is 0, and only effect literals carry
51
+ * the field) fall back to the shared notifier. Presence doubles as the
52
+ * "display consumer" membership test in the status walks, exactly as the
53
+ * per-node field did when every effect carried one. */
54
+ export declare function statusNotifierOf(el: any): ((this: any, status?: number, error?: any) => void) | undefined;
42
55
  export declare function signal<T>(v: T, options?: NodeOptions<T>): Signal<T>;
43
56
  export declare function signal<T>(v: T, options?: NodeOptions<T>, firewall?: Computed<any>): FirewallSignal<T>;
44
57
  export declare function optimisticSignal<T>(v: T, options?: NodeOptions<T>): Signal<T>;
@@ -93,6 +106,19 @@ export declare const READ_SLOW: unique symbol;
93
106
  * snapshot / transition / lane / dev-strictRead state all take the full
94
107
  * resolution. Anything slow returns READ_SLOW; the caller then calls read().
95
108
  */
109
+ /**
110
+ * Wake only authoritative-view readers (until() predicates) subscribed to `el`.
111
+ * The A17-silent ack paths — an authoritative arrival equal to the active
112
+ * override — use this so the predicate re-evaluates without re-firing
113
+ * ordinary subscribers whose visible (override) value did not change.
114
+ * Pay-for-use: reached through GlobalQueue._notifyAuthoritativeObservers,
115
+ * installed at first until() call — apps that never use until() shake it.
116
+ */
117
+ export declare function notifyAuthoritativeObservers(el: Signal<any> | Computed<any>): void;
118
+ /** Installs the until() machinery hook. Idempotent; called by until() before
119
+ * any authoritative-view read happens (same late-binding contract as the
120
+ * optimistic engine). */
121
+ export declare function installAuthoritativeRead(): void;
96
122
  export declare function readNodeFast<T>(el: Signal<T>): T | typeof READ_SLOW;
97
123
  export declare function read<T>(el: Signal<T> | Computed<T>): T;
98
124
  /**
@@ -141,23 +167,10 @@ export declare function setMemo<T>(el: Computed<T>, v: T | ((prev: T) => T)): T;
141
167
  export declare function runWithOwner<T>(owner: Owner | null, fn: () => T): T;
142
168
  export declare function staleValues<T>(fn: () => T, set?: boolean): T;
143
169
  /**
144
- * Invalidates one reactive source, forcing it to re-execute even if its inputs
145
- * haven't changed.
146
- *
147
- * Pass either a Solid-created accessor or a projected store created from
148
- * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
149
- * write-like invalidation operation: it does not read the target's value, and
150
- * refreshing a plain signal accessor is a no-op.
151
- *
152
- * Use it to invalidate cached async values (e.g. force a re-fetch) without
153
- * tearing the consumer down.
154
- *
155
- * @example
156
- * ```ts
157
- * const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
158
- *
159
- * // Re-fetch on demand
160
- * <button onClick={() => refresh(user)}>Reload</button>
161
- * ```
170
+ * Core marking half of `refresh()` (the public wrapper lives in signals.ts
171
+ * it validates the target, marks through here, then builds the quiescence
172
+ * promise on the resolve()/until() effect machinery). Flags the node's next
173
+ * recompute as a quiet re-ask and schedules it; no-ops for non-derived or
174
+ * disposed targets and for same-tick manual writes.
162
175
  */
163
- export declare function refresh<T>(target: Refreshable<T>): void;
176
+ export declare function markRefresh(node: Computed<any>): void;
@@ -33,6 +33,14 @@ export interface DiagnosticCapture {
33
33
  export interface Diagnostics {
34
34
  subscribe(listener: DiagnosticListener): () => void;
35
35
  capture(): DiagnosticCapture;
36
+ /**
37
+ * Registers a console footer printed after the first console report of
38
+ * each diagnostic code — a discovery pointer to deeper guidance (e.g.
39
+ * solid-js registers its shipped repair skill). Returning undefined for
40
+ * an event suppresses the footer. Passing undefined unregisters and
41
+ * resets the once-per-code memory.
42
+ */
43
+ setConsoleFooter(footer: ((event: DiagnosticEvent) => string | undefined) | undefined): void;
36
44
  }
37
45
  export interface Dev {
38
46
  hooks: DevHooks;
@@ -39,6 +39,15 @@ export declare class StatusError extends Error {
39
39
  }
40
40
  /** Return the user's error from an internal status wrapper. */
41
41
  export declare function unwrapStatusError(error: unknown): unknown;
42
+ /**
43
+ * Rejection value of `until(fn, { timeout })` when the predicate does not turn
44
+ * truthy within the window. Inside an `action()`, the rejection is thrown back
45
+ * in at the `yield` point — catchable there, or the action fails and its
46
+ * optimistic state reverts.
47
+ */
48
+ export declare class TimeoutError extends Error {
49
+ constructor(message?: string);
50
+ }
42
51
  export declare class NoOwnerError extends Error {
43
52
  constructor();
44
53
  }
@@ -3,4 +3,26 @@ export declare function unlinkSubs(link: Link): Link | null;
3
3
  export declare function trimStaleDeps(el: Computed<any>): void;
4
4
  export declare function clearDeps(el: Computed<unknown>): void;
5
5
  export declare function unobserved(el: Computed<unknown>): void;
6
+ /**
7
+ * Deferred dormancy for never-observed auto-dispose computeds (#3078).
8
+ *
9
+ * An untracked top-level read of a subscriber-less observation-lifecycle memo
10
+ * used to call unobserved() inline at the end of read(). That kept the leak
11
+ * closed (the compute links the memo into its deps' sub lists — without a
12
+ * teardown point a never-observed memo is retained by its sources forever;
13
+ * upstream alien-signals has exactly this retention), but it made reads
14
+ * destructive: each read disposed the node, the next read revived it with a
15
+ * full recompute in whatever ambient transition/lane context happened to be
16
+ * current, so consecutive reads could return different answers with no write
17
+ * in between.
18
+ *
19
+ * Instead, reads queue the node here and the scheduler sweeps at the top of
20
+ * the next flush (before runHeap, so a same-tick dirtying is reclaimed
21
+ * instead of recomputed). Reads become idempotent within a tick (the node
22
+ * stays alive and serves its cache, uniform with observed memos) while
23
+ * reclamation still happens within one microtask — the enqueue site arms
24
+ * schedule(), so a flush is guaranteed even when no other work is queued.
25
+ */
26
+ export declare const dormantNodes: Set<Computed<unknown>>;
27
+ export declare function sweepDormant(): void;
6
28
  export declare function link(dep: Signal<any> | Computed<any>, sub: Computed<any>, pendingObserver?: boolean): void;
@@ -1,5 +1,5 @@
1
- export { ContextNotFoundError, NoOwnerError, NotReadyError } from "./error.js";
2
- export { isEqual, untrack, runWithOwner, computed, signal, read, setSignal, setMemo, suppressComputedRecompute, optimisticSignal, optimisticComputed, refresh, staleValues, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots } from "./core.js";
1
+ export { ContextNotFoundError, NoOwnerError, NotReadyError, TimeoutError } from "./error.js";
2
+ export { isEqual, untrack, runWithOwner, computed, signal, read, setSignal, setMemo, suppressComputedRecompute, optimisticSignal, optimisticComputed, installAuthoritativeRead, markRefresh, staleValues, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots } from "./core.js";
3
3
  export { enableExternalSource, _resetExternalSourceConfig, type ExternalSourceFactory, type ExternalSource, type ExternalSourceConfig } from "./external.js";
4
4
  export { createOwner, createRoot, dispose, getNextChildId, getObserver, getOwner, isDisposed, cleanup, peekNextChildId } from "./owner.js";
5
5
  export { createContext, getContext, setContext, type Context, type ContextRecord } from "./context.js";
@@ -40,6 +40,24 @@ export interface Transition {
40
40
  _done: boolean | Transition;
41
41
  _gatedSubs: Set<Computed<any>>;
42
42
  }
43
+ /**
44
+ * Flip-entanglement (#3164 follow-up): `until()` is a declaration of
45
+ * relatedness — the predicate names the condition that confirms the awaiting
46
+ * transaction. When the predicate settles truthy, every live foreign
47
+ * transition whose staged write it read IS the confirming event by the
48
+ * user's own definition, so it merges into the awaiting transaction and
49
+ * reveals at the joint settle — the cross-primitive twin of the family fold
50
+ * (a landing on an optimism-carrying family joins the retaining
51
+ * transaction). Non-flipping updates never pass through here: falsy
52
+ * evaluations don't entangle, so unrelated traffic on the watched sources
53
+ * reveals freely on its own schedule.
54
+ *
55
+ * Runs inside the predicate's compute (pure phase) — the confirming
56
+ * transition's stamps are still live and its commit decision hasn't run, so
57
+ * the merge lands before any reveal. Only the tree-shaken graphs that call
58
+ * `until()` retain this.
59
+ */
60
+ export declare function entangleConfirmingTransitions(obs: Computed<any>, target: Transition): void;
43
61
  export declare function schedule(): void;
44
62
  /**
45
63
  * Permanently halts the reactive system. Called when a user error escapes
@@ -103,11 +121,19 @@ export declare class GlobalQueue extends Queue {
103
121
  static _transitionBlocked: ((transition: Transition) => boolean) | null;
104
122
  static _cleanupLanes: ((completingTransition: Transition | null) => void) | null;
105
123
  static _runLaneEffects: ((type: number) => void) | null;
124
+ /** Patch-channel optimistic drain (next/patch.ts): optimistic emissions
125
+ * apply at lane-effect timing — visible in flight, unlike the regular
126
+ * effect queues an action stashes. Injected; null when unused. */
127
+ static _drainPatchOptimistic: (() => void) | null;
106
128
  static _gatedRead: ((el: Signal<any>, owner: OptimisticNode, c: Computed<any>) => boolean) | null;
107
129
  static _laneSuspends: ((owner: OptimisticNode) => boolean) | null;
108
130
  static _laneReadsCommitted: ((el: OptimisticNode, owner: OptimisticNode, c: Computed<any>) => boolean) | null;
109
131
  static _recomputeLane: ((el: Computed<any>, own: boolean) => OptimisticLane | null | false) | null;
110
132
  static _laneAsyncPending: ((el: Computed<any>) => void) | null;
133
+ /** Authoritative-view reader wakeup (until()): installed at first until() call.
134
+ * Call sites are gated by CONFIG_AUTHORITATIVE_OBSERVED, which only until()'s
135
+ * carve-out read can set, so `!` invocations are safe once the gate holds. */
136
+ static _notifyAuthoritativeObservers: ((el: Signal<any> | Computed<any>) => void) | null;
111
137
  static _laneAsyncSettled: ((el: Computed<any>) => void) | null;
112
138
  static _trackOptimisticStore: ((store: any) => void) | null;
113
139
  flush(): void;
@@ -126,6 +152,14 @@ export declare function armReaskClear(): void;
126
152
  export declare function insertSubs(node: Signal<any> | Computed<any>, optimistic?: boolean): void;
127
153
  export declare let storeCommitHook: (() => void) | null;
128
154
  export declare function setStoreCommitHook(fn: () => void): void;
155
+ /** Patch-channel release hook (next/patch.ts): transition-stamped patch
156
+ * emissions are released when THEIR batch commits. Transitions never
157
+ * abort: failed actions still commit (only optimistic overrides revert),
158
+ * and merged-away transitions hand their stash to the survivor
159
+ * (mergeTransitionState) — every stash drains exactly once. Injected like
160
+ * storeCommitHook to stay tree-shakeable. */
161
+ export declare let patchCommitHook: ((batch: Transition) => void) | null;
162
+ export declare function setPatchCommitHook(fn: (batch: Transition) => void): void;
129
163
  export declare function finalizePureQueue(completingTransition?: Transition | null, incomplete?: boolean): void;
130
164
  /**
131
165
  * Count of live `affects()` registrations across the system (including
@@ -178,3 +212,15 @@ export declare function flush<T>(fn: () => T): T;
178
212
  export declare function currentTransition(transition: Transition): Transition;
179
213
  export declare function setActiveTransition(transition: Transition | null): void;
180
214
  export declare function runInTransition<T>(transition: Transition, fn: () => T): T;
215
+ /** Run `fn` with `transition` as BOTH the ambient transaction and the
216
+ * registration batch, restoring both after. runInTransition alone is not
217
+ * enough for code that WRITES on behalf of a transaction from inside someone
218
+ * else's window (optimistic replay re-arming a still-open action's edits
219
+ * during a landing commit, #3123): registrations route through the queue's
220
+ * batch pointer, and a bare activeTransition swap leaves them in the ambient
221
+ * batch — a plain batch "completes" at the next flush and reverts optimistic
222
+ * registrations that were supposed to live with the transaction.
223
+ * initTransition is the wrong tool here: it MERGES the currently ambient
224
+ * transaction into the target, entangling whatever the interrupted window
225
+ * belonged to. */
226
+ export declare function runAsTransitionBatch<T>(transition: Transition, fn: () => T): T;
@@ -27,6 +27,11 @@ export interface NodeOptions<T> {
27
27
  ownedWrite?: boolean;
28
28
  /** Exclude this signal from snapshot capture (internal — not part of public API) */
29
29
  _noSnapshot?: boolean;
30
+ /** Extra CONFIG_* bits OR'd into the node's config at creation (internal —
31
+ * not part of public API). Used by resolve()/until() for
32
+ * CONFIG_DIRECT_COMMIT / CONFIG_AUTHORITATIVE_READ, keeping the per-flag
33
+ * option arms out of the core creation path. */
34
+ _extraConfig?: number;
30
35
  unobserved?: () => void;
31
36
  lazy?: boolean;
32
37
  sync?: boolean;
@@ -73,6 +78,13 @@ export interface NodeExtension {
73
78
  */
74
79
  _affectsCount: number;
75
80
  _inFlight: PromiseLike<any> | AsyncIterable<any> | null;
81
+ /** Cancellation for the CURRENT iterator flight (#3122): closes the
82
+ * iterator (`it.return()`), idempotent. Fired at the sites that release
83
+ * `_inFlight` so a superseded stream stops at supersede time — its owner
84
+ * cleanup registration may ride the zombie-disposal channel, which a held
85
+ * transition defers until the SUPERSEDING flight settles. Null for plain
86
+ * promise flights (no cancellation hook exists). */
87
+ _flightTeardown: (() => void) | null;
76
88
  _error: unknown;
77
89
  _blocked: boolean | undefined;
78
90
  _pendingSources: Set<Computed<any>> | undefined;
@@ -1,9 +1,9 @@
1
- export { $REFRESH, ContextNotFoundError, NoOwnerError, NotReadyError, action, createContext, createOwner, createRoot, runWithOwner, flush, getNextChildId, peekNextChildId, getContext, setContext, getOwner, isDisposed, getObserver, isEqual, untrack, isPending, latest, refresh, SUPPORTS_PROXY, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots, enforceLoadingBoundary, enableExternalSource, resetErrorHalt } from "./core/index.js";
1
+ export { $REFRESH, ContextNotFoundError, NoOwnerError, NotReadyError, TimeoutError, action, createContext, createOwner, createRoot, runWithOwner, flush, getNextChildId, peekNextChildId, getContext, setContext, getOwner, isDisposed, getObserver, isEqual, untrack, isPending, latest, SUPPORTS_PROXY, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots, enforceLoadingBoundary, enableExternalSource, resetErrorHalt } from "./core/index.js";
2
2
  import { type Dev } from "./core/index.js";
3
3
  export declare const DEV: Dev | undefined;
4
4
  export type { Owner, Context, ContextRecord, IQueue, ExternalSourceFactory, ExternalSource, ExternalSourceConfig, Refreshable, Dev, DevHooks, DiagnosticCapture, DiagnosticCode, DiagnosticEvent, DiagnosticKind, Diagnostics, DiagnosticSeverity } from "./core/index.js";
5
- export { createSignal, createMemo, createEffect, createRenderEffect, createTrackedEffect, createReaction, createOptimistic, resolve, onSettled, onCleanup } from "./signals.js";
6
- export type { Accessor, SourceAccessor, Setter, Signal, ComputeFunction, EffectFunction, EffectBundle, EffectOptions, SignalOptions, MemoOptions, NoInfer } from "./signals.js";
5
+ export { createSignal, createMemo, createEffect, createRenderEffect, createTrackedEffect, createReaction, createOptimistic, refresh, resolve, until, onSettled, onCleanup } from "./signals.js";
6
+ export type { Truthy, UntilOptions, Accessor, SourceAccessor, Setter, Signal, ComputeFunction, EffectFunction, EffectBundle, EffectOptions, SignalOptions, MemoOptions, NoInfer } from "./signals.js";
7
7
  export { affects } from "./affects.js";
8
8
  export { mapArray, repeat, type Maybe } from "./map.js";
9
9
  export * from "./store/index.js";
@@ -491,6 +491,114 @@ export declare function createReaction(effectFn: EffectFunction<undefined> | Eff
491
491
  * @param fn a reactive expression to resolve
492
492
  */
493
493
  export declare function resolve<T>(fn: () => T): Promise<T>;
494
+ /**
495
+ * Invalidates one reactive source, forcing it to re-execute even if its inputs
496
+ * haven't changed, and returns a promise for the target's NEXT QUIESCENT
497
+ * STATE — the re-ask (and anything that supersedes it) has settled.
498
+ *
499
+ * Pass either a Solid-created accessor or a projected store created from
500
+ * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a
501
+ * write-like invalidation operation: it does not read the target's value, and
502
+ * refreshing a plain signal accessor is a no-op that resolves immediately.
503
+ *
504
+ * The returned promise is safe to ignore (fire-and-forget refresh is
505
+ * unchanged, and a failed refetch will not surface an unhandled rejection).
506
+ * Awaiting it gives imperative flows the settle point without a reactive
507
+ * read:
508
+ * - Accessor targets resolve with the settled value; store targets resolve
509
+ * with the store node passed (reads through it are fresh after the await).
510
+ * - A failed re-ask rejects with the error (inside an action's generator,
511
+ * `yield refresh(x)` throws back at the yield point and the action reverts
512
+ * like any other failure).
513
+ * - Semantics are quiescence, not flight identity: if another refresh (or
514
+ * any invalidation) supersedes this one mid-flight, the promise waits for
515
+ * — and delivers — whatever finally lands.
516
+ * - Inside an action, truth landing into the held transaction is STAGED;
517
+ * the promise still settles then (matching `resolve()`/`until()`, #2930)
518
+ * and delivers the staged value — the caller's own optimistic override is
519
+ * never the delivered value.
520
+ * - The re-ask itself stays verdict-quiet exactly as before: `isPending`
521
+ * does not flip for a bare refresh (pair with `affects()` for a visible
522
+ * pending window).
523
+ *
524
+ * @example
525
+ * ```ts
526
+ * const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json()));
527
+ *
528
+ * // Fire-and-forget re-fetch
529
+ * <button onClick={() => refresh(user)}>Reload</button>;
530
+ *
531
+ * // Imperative settle point
532
+ * const fresh = await refresh(user);
533
+ * ```
534
+ */
535
+ export declare function refresh<T>(target: Refreshable<T>): Promise<T extends (...args: any) => infer V ? V : T>;
536
+ /** Falsy values a truthy predicate result is narrowed against. */
537
+ export type Truthy<T> = Exclude<T, false | 0 | 0n | "" | null | undefined>;
538
+ export interface UntilOptions {
539
+ /** Reject with `TimeoutError` if the predicate has not turned truthy within
540
+ * this many milliseconds. Strongly recommended when the confirming truth
541
+ * arrives over a transport that can drop (sockets, subscriptions). */
542
+ timeout?: number;
543
+ /** Reject with `signal.reason` on abort. */
544
+ signal?: AbortSignal;
545
+ }
546
+ /**
547
+ * Awaits a reactive predicate and resolves the first time it settles *truthy*,
548
+ * with that (narrowed) value. Falsy results and pending async reads both mean
549
+ * "not yet": the subscription stays live and re-evaluates as sources change.
550
+ * If the predicate settles with an error — a throw, or an async source that
551
+ * rejects — the promise rejects with it, as do timeout and abort.
552
+ *
553
+ * Where {@link resolve} answers "what is this value" (first settled value,
554
+ * whatever it is), `until` answers "when does the world confirm this
555
+ * condition". The difference matters inside an `action()`: `yield until(...)`
556
+ * holds the action's transaction — and any optimistic state riding it — open
557
+ * until the condition is independently true.
558
+ *
559
+ * To make that sound, `until`'s predicate reads the AUTHORITATIVE view — and
560
+ * this is the one read-semantics difference from `resolve`, which reads the
561
+ * normal (transaction's own) view where overrides are visible:
562
+ *
563
+ * - **Optimistic overrides are invisible** to the predicate. Your own
564
+ * tentative write can never satisfy your own ack, even on the
565
+ * single-primitive shape where the optimistic store IS the live-fed store.
566
+ * (Derived computeds serve their normal cached values — express the
567
+ * condition over sources of truth, not derived views of the overlay.)
568
+ * - **Everything else reads normally, including uncommitted transition-staged
569
+ * data.** Real data is real wherever it currently lives. This is
570
+ * load-bearing, not a loophole: truth that arrives *into* the open
571
+ * transaction (a `refresh()` this action issued, an entangled landing)
572
+ * stages and cannot commit until the hold releases — a predicate that
573
+ * refused staged reads would deadlock on the very data it is waiting for.
574
+ *
575
+ * This is the acknowledgment mechanism for mutations confirmed on a live data
576
+ * channel (sockets, subscriptions, live queries) rather than by the mutation's
577
+ * own response: correlate by a client-generated id or version in the predicate,
578
+ * and let truth arrive however it arrives — push, refetch, or another tab.
579
+ *
580
+ * Failure composes with action semantics: a rejection is thrown back into the
581
+ * generator at the `yield` point — catchable there, or the action fails and
582
+ * its optimistic state reverts.
583
+ *
584
+ * Must be called *outside* a tracking scope.
585
+ *
586
+ * @example
587
+ * ```ts
588
+ * const send = action(async function* (text: string) {
589
+ * const clientId = crypto.randomUUID();
590
+ * setMessages(m => { m.push({ clientId, text, pending: true }); }); // optimistic
591
+ * await socket.send({ clientId, text }); // fire-and-forget transport
592
+ * // Hold until the live source echoes the write (authoritative view —
593
+ * // the optimistic row above cannot satisfy this):
594
+ * yield until(() => messages.some(m => m.clientId === clientId), { timeout: 10_000 });
595
+ * });
596
+ * ```
597
+ *
598
+ * @param fn a reactive predicate over authoritative state
599
+ * @param options optional `timeout` (ms) and abort `signal`
600
+ */
601
+ export declare function until<T>(fn: () => T, options?: UntilOptions): Promise<Truthy<T>>;
494
602
  /**
495
603
  * Creates an optimistic signal that can be used to optimistically update a value
496
604
  * and then revert it back to the previous value at end of transition.
@@ -4,6 +4,8 @@ export { isWrappable, $TRACK, $PROXY, $TARGET } from "./store.js";
4
4
  import type { NoFn, ProjectionOptions, Store, StoreOptions, StoreSetter } from "./store.js";
5
5
  import type { Refreshable } from "../core/index.js";
6
6
  export { createProjectionNext as createProjection } from "./next/projection.js";
7
+ export { registerPatch, registerRowOps, registerSlotPatchNext as registerSlotPatch, patchableRaw } from "./next/patch.js";
8
+ export { storeIsShallow, storeHasFamily, storeHasOptimisticFamily } from "./next/store.js";
7
9
  export { createOptimisticStoreNext as createOptimisticStore } from "./next/optimistic.js";
8
10
  /** Public createStore: plain form `(init, options?)` and derived writable
9
11
  * form `(fn, seed, options?)`. */
@@ -1,20 +1,23 @@
1
+ import { type Transition } from "../../core/scheduler.js";
1
2
  import { type NoFn, type ProjectionOptions, type Store, type StoreSetter } from "../store.js";
2
- import type { StoreNextFamily, StoreNextTarget } from "./target.js";
3
+ import type { StoreNextTarget } from "./target.js";
4
+ /** #3164 fold: a stamped truth is HELD (masked from ordinary readers until
5
+ * the reveal) only while its transition is live AND retaining optimism —
6
+ * overrides are what make partial-coverage composition a tear. A plain
7
+ * async transition carries no overrides, so downstream computes must see
8
+ * staged values to converge (normal speculation). Resolves merges first:
9
+ * merge unions optimistic nodes/stores into the target. */
10
+ export declare function transitionHoldsOptimism(transition: Transition): boolean;
3
11
  export declare function createOptimisticStoreNext<T extends object = {}>(first: T | ((store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>), second?: NoFn<T> | Store<NoFn<T>>, options?: ProjectionOptions): [get: Store<T>, set: StoreSetter<T>];
4
12
  /** Diff the draft against the current OPTIMISTIC VIEW (committed + active
5
13
  * overrides — the same view the draft was seeded from) and emit engine writes
6
14
  * for exactly the changed keys. Visible-view diffing keeps no-op writes from
7
15
  * entangling lanes (RUL-10 / opt R38). */
8
16
  export declare function notifyOptimisticWrites(t: StoreNextTarget, pb: Record<PropertyKey, any>): void;
9
- /**
10
- * Landing consumption (RUL-2): fresh authoritative data supersedes every
11
- * tentative override in the family. Mirrors legacy clearProjectionOverride —
12
- * drop the override, clear lane/ownership, notify subscribers whose visible
13
- * value changes (reversion effects go to regular queues via the projection
14
- * write posture the caller holds).
15
- */
16
- export declare function consumeOverridesNext(fam: StoreNextFamily): void;
17
17
  /** Optimistic-view composition for snapshot/deep (O1: snapshot is the CURRENT
18
18
  * view, lane values included; a fresh copy per call during pending windows —
19
- * RUL-12). Returns `src` untouched when no override is active on `t`. */
19
+ * RUL-12). Returns `src` untouched when no override is active on `t`.
20
+ * Authoritative-view reads (until()'s predicate) skip composition entirely:
21
+ * the predicate observes authoritative truth, never the caller's tentative
22
+ * overlay. (Write-side emission callers never run under such a compute.) */
20
23
  export declare function optimisticView(t: StoreNextTarget, src: Record<PropertyKey, any>): Record<PropertyKey, any>;
@@ -0,0 +1,41 @@
1
+ import type { StoreNextTarget } from "./target.js";
2
+ import type { RowOps } from "./patch.js";
3
+ /**
4
+ * Patch-channel emission seams (pay-for-use). The store/reconcile/optimistic
5
+ * write paths emit through these installed hook objects instead of importing
6
+ * `patch.js` statically, so the channel tree-shakes out of apps that never
7
+ * register a patch consumer.
8
+ *
9
+ * TWO TIERS, armed at registration (patch.js installs them; it is retained
10
+ * only through its registration exports, which only compiled patch-mode
11
+ * output — via the web runtime's driver module — imports):
12
+ * - VALUE hooks (`patchHooks`): record patches. Armed by `registerPatch` —
13
+ * present in any bundle with one eligible template under patch mode.
14
+ * - ROW hooks (`rowHooks`): list structure (row ops, slot ticks, the
15
+ * identity/keyed diff builders in reconcile.js they drag in). Armed by
16
+ * `registerRowOps`/`registerSlotPatchNext` — the LIST driver's
17
+ * registrations, so value-only bundles never retain the row machinery.
18
+ *
19
+ * Soundness: every emission site is guarded by the matching `pc` channel
20
+ * (`pc.p` for value, `pc.ro`/`pc.sp` for rows), and a target can only
21
+ * acquire that channel through the corresponding registration — so each
22
+ * hook object is installed by the time any guard passes. Type-only imports
23
+ * from `patch.js` are erased.
24
+ */
25
+ export interface PatchValueHooks {
26
+ emitPatch(t: StoreNextTarget, next: any, prev: any): void;
27
+ emitPatchLocal(t: StoreNextTarget, next: any, prev: any): void;
28
+ emitPatchOptimistic(t: StoreNextTarget, next: any, prev: any): void;
29
+ hasPatches(): boolean;
30
+ demoteToEffects(t: StoreNextTarget): void;
31
+ }
32
+ export interface PatchRowHooks {
33
+ emitRowOps(t: StoreNextTarget, next: any[], ops: RowOps): void;
34
+ emitSlotPatch(t: StoreNextTarget, index: number, next: any, prev: any): void;
35
+ emitSetterRowOps(t: StoreNextTarget, prevRows: any[], nextRows: any[]): void;
36
+ emitRowOpsOptimistic(t: StoreNextTarget, next: any[] | null, ops: RowOps | null): void;
37
+ }
38
+ export declare let patchHooks: PatchValueHooks | null;
39
+ export declare let rowHooks: PatchRowHooks | null;
40
+ export declare function installPatchHooks(hooks: PatchValueHooks): void;
41
+ export declare function installRowHooks(hooks: PatchRowHooks): void;