@solidjs/signals 2.0.0-beta.9 → 2.0.0-rc.0
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.
- package/README.md +13 -9
- package/dist/dev.js +5971 -1426
- package/dist/node.cjs +7681 -3293
- package/dist/prod/affects.js +126 -0
- package/dist/prod/boundaries.js +572 -0
- package/dist/prod/core/action.js +139 -0
- package/dist/prod/core/async.js +553 -0
- package/dist/prod/core/constants.js +92 -0
- package/dist/prod/core/context.js +67 -0
- package/dist/prod/core/core.js +817 -0
- package/dist/prod/core/dev.js +3 -0
- package/dist/prod/core/effect.js +145 -0
- package/dist/prod/core/error.js +68 -0
- package/dist/prod/core/external.js +98 -0
- package/dist/prod/core/graph.js +104 -0
- package/dist/prod/core/heap.js +140 -0
- package/dist/prod/core/invariants.js +42 -0
- package/dist/prod/core/lanes.js +140 -0
- package/dist/prod/core/optimistic.js +245 -0
- package/dist/prod/core/owner.js +302 -0
- package/dist/prod/core/scheduler.js +749 -0
- package/dist/prod/core/verdict.js +369 -0
- package/dist/prod/index.js +45 -0
- package/dist/prod/map.js +317 -0
- package/dist/prod/signals.js +375 -0
- package/dist/prod/store/optimistic.js +217 -0
- package/dist/prod/store/projection.js +231 -0
- package/dist/prod/store/reconcile.js +707 -0
- package/dist/prod/store/store.js +1081 -0
- package/dist/prod/store/storePath.js +103 -0
- package/dist/prod/store/utils.js +328 -0
- package/dist/types/affects.d.ts +47 -0
- package/dist/types/boundaries.d.ts +60 -13
- package/dist/types/core/action.d.ts +35 -6
- package/dist/types/core/async.d.ts +16 -1
- package/dist/types/core/constants.d.ts +35 -6
- package/dist/types/core/context.d.ts +10 -2
- package/dist/types/core/core.d.ts +58 -63
- package/dist/types/core/dev.d.ts +17 -2
- package/dist/types/core/effect.d.ts +1 -2
- package/dist/types/core/error.d.ts +33 -0
- package/dist/types/core/external.d.ts +0 -11
- package/dist/types/core/graph.d.ts +2 -1
- package/dist/types/core/heap.d.ts +8 -0
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/core/invariants.d.ts +59 -0
- package/dist/types/core/lanes.d.ts +2 -0
- package/dist/types/core/optimistic.d.ts +6 -0
- package/dist/types/core/owner.d.ts +50 -3
- package/dist/types/core/scheduler.d.ts +82 -10
- package/dist/types/core/types.d.ts +66 -1
- package/dist/types/core/verdict.d.ts +2 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/map.d.ts +21 -5
- package/dist/types/signals.d.ts +164 -12
- package/dist/types/store/index.d.ts +1 -1
- package/dist/types/store/optimistic.d.ts +3 -3
- package/dist/types/store/projection.d.ts +10 -6
- package/dist/types/store/reconcile.d.ts +31 -8
- package/dist/types/store/store.d.ts +101 -6
- package/dist/types-cjs/affects.d.cts +47 -0
- package/dist/types-cjs/boundaries.d.cts +60 -13
- package/dist/types-cjs/core/action.d.cts +35 -6
- package/dist/types-cjs/core/async.d.cts +16 -1
- package/dist/types-cjs/core/constants.d.cts +35 -6
- package/dist/types-cjs/core/context.d.cts +10 -2
- package/dist/types-cjs/core/core.d.cts +58 -63
- package/dist/types-cjs/core/dev.d.cts +17 -2
- package/dist/types-cjs/core/effect.d.cts +1 -2
- package/dist/types-cjs/core/error.d.cts +33 -0
- package/dist/types-cjs/core/external.d.cts +0 -11
- package/dist/types-cjs/core/graph.d.cts +2 -1
- package/dist/types-cjs/core/heap.d.cts +8 -0
- package/dist/types-cjs/core/index.d.cts +3 -2
- package/dist/types-cjs/core/invariants.d.cts +59 -0
- package/dist/types-cjs/core/lanes.d.cts +2 -0
- package/dist/types-cjs/core/optimistic.d.cts +6 -0
- package/dist/types-cjs/core/owner.d.cts +50 -3
- package/dist/types-cjs/core/scheduler.d.cts +82 -10
- package/dist/types-cjs/core/types.d.cts +66 -1
- package/dist/types-cjs/core/verdict.d.cts +2 -0
- package/dist/types-cjs/index.d.cts +3 -2
- package/dist/types-cjs/map.d.cts +21 -5
- package/dist/types-cjs/signals.d.cts +164 -12
- package/dist/types-cjs/store/index.d.cts +1 -1
- package/dist/types-cjs/store/optimistic.d.cts +3 -3
- package/dist/types-cjs/store/projection.d.cts +10 -6
- package/dist/types-cjs/store/reconcile.d.cts +31 -8
- package/dist/types-cjs/store/store.d.cts +101 -6
- package/package.json +11 -9
- package/dist/prod.js +0 -3627
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { globalQueue, activeTransition, currentTransition, schedule, flush } from "./scheduler.js";
|
|
2
|
+
|
|
3
|
+
import { isThenable } from "./async.js";
|
|
4
|
+
|
|
5
|
+
import "./core.js";
|
|
6
|
+
|
|
7
|
+
function restoreTransition(e, r) {
|
|
8
|
+
globalQueue.initTransition(e);
|
|
9
|
+
const t = r();
|
|
10
|
+
flush();
|
|
11
|
+
return t;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The primitive for mutations: imperative async workflows whose *writes span
|
|
16
|
+
* an async gap* — optimistic write, server round-trip, reconciling write —
|
|
17
|
+
* where intermediate state must not leak and failure must revert cleanly
|
|
18
|
+
* (pair with `createOptimistic` / `createOptimisticStore`).
|
|
19
|
+
*
|
|
20
|
+
* Navigation-shaped updates do not need an action. A plain setter call is
|
|
21
|
+
* enough: reads pull the async, and downstream async computeds hold their
|
|
22
|
+
* previous values per-node until the new ones are ready (`isPending` /
|
|
23
|
+
* `latest` expose the in-flight state). Reach for `action` only when writes
|
|
24
|
+
* happen *after* async work, not merely upstream of it.
|
|
25
|
+
*
|
|
26
|
+
* Framework-level actions (router form actions, server actions) are
|
|
27
|
+
* specializations of this primitive: they are actions in exactly this sense —
|
|
28
|
+
* the same transactional semantics — with form binding, serialization, and
|
|
29
|
+
* submission tracking layered on top. The shared name is deliberate.
|
|
30
|
+
*
|
|
31
|
+
* Wraps a generator function so each invocation runs as a single transaction
|
|
32
|
+
* (a "transition") that batches every signal/store write between yields. The
|
|
33
|
+
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
|
34
|
+
* until the action either completes or the next `yield` resolves.
|
|
35
|
+
*
|
|
36
|
+
* `yield` is the transaction-safe suspension point: the action waits for a
|
|
37
|
+
* yielded promise and re-enters the transaction before running the code after
|
|
38
|
+
* it. A plain `await` does NOT — the runtime has no hook into an async
|
|
39
|
+
* generator's internal await continuations, so writes to fresh signals
|
|
40
|
+
* between an `await` and the next `yield` escape the transaction and commit
|
|
41
|
+
* immediately. `await` is still the ergonomic choice for typed results; just
|
|
42
|
+
* put a bare `yield` before any writes that follow it:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const saved = await api.createTodo(text); // typed result
|
|
46
|
+
* yield; // re-enter the transaction before writing
|
|
47
|
+
* setTodos(t => { ... });
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* (For the same reason, don't call `flush()` inside an action body — it
|
|
51
|
+
* drains the transaction mid-step.)
|
|
52
|
+
*
|
|
53
|
+
* Each call returns a `Promise` that resolves with the generator's return
|
|
54
|
+
* value, or rejects if it throws. Pair with `createOptimistic` /
|
|
55
|
+
* `createOptimisticStore` to apply tentative writes that auto-revert if the
|
|
56
|
+
* action fails.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const [todos, setTodos] = createOptimisticStore<Todo[]>([]);
|
|
61
|
+
*
|
|
62
|
+
* const addTodo = action(async function* (text: string) {
|
|
63
|
+
* const tempId = crypto.randomUUID();
|
|
64
|
+
* setTodos(t => { t.push({ id: tempId, text, pending: true }); }); // optimistic
|
|
65
|
+
* const saved = await api.createTodo(text); // network round-trip, typed
|
|
66
|
+
* yield; // re-enter the transaction
|
|
67
|
+
* setTodos(t => {
|
|
68
|
+
* const i = t.findIndex(x => x.id === tempId);
|
|
69
|
+
* if (i >= 0) t[i] = saved;
|
|
70
|
+
* });
|
|
71
|
+
* return saved;
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* await addTodo("buy milk");
|
|
75
|
+
* ```
|
|
76
|
+
*/ function action(e) {
|
|
77
|
+
return (...r) => new Promise((t, n) => {
|
|
78
|
+
const i = e(...r);
|
|
79
|
+
globalQueue.initTransition();
|
|
80
|
+
let o = activeTransition;
|
|
81
|
+
o.ie.push(i);
|
|
82
|
+
const done = (e, r, u = false) => {
|
|
83
|
+
o = currentTransition(o);
|
|
84
|
+
const s = o.ie.indexOf(i);
|
|
85
|
+
if (s >= 0) o.ie.splice(s, 1);
|
|
86
|
+
// Re-adopt through initTransition like every other resumption site:
|
|
87
|
+
// a bare setActiveTransition leaves globalQueue._batch as a detached
|
|
88
|
+
// ambient batch, and anything registered before the scheduled flush
|
|
89
|
+
// (held writes on a merging transition, optimistic overrides,
|
|
90
|
+
// affects() marks) lands there with nothing to ever finalize it.
|
|
91
|
+
globalQueue.initTransition(o);
|
|
92
|
+
schedule();
|
|
93
|
+
u ? n(r) : t(e);
|
|
94
|
+
};
|
|
95
|
+
const step = (e, r) => {
|
|
96
|
+
let t;
|
|
97
|
+
try {
|
|
98
|
+
t = r ? i.throw(e) : i.next(e);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return done(undefined, e, true);
|
|
101
|
+
}
|
|
102
|
+
// A rejected iterator result (async generators) means the error already
|
|
103
|
+
// escaped the generator body — it is completed, and throwing back in
|
|
104
|
+
// would just reject again forever. Settle instead.
|
|
105
|
+
if (isThenable(t)) return void t.then(run, e => done(undefined, e, true));
|
|
106
|
+
run(t);
|
|
107
|
+
};
|
|
108
|
+
const run = e => {
|
|
109
|
+
if (e.done) return done(e.value);
|
|
110
|
+
// Thenable assimilation can itself throw synchronously (a `then`
|
|
111
|
+
// getter, or a `then()` method that throws — #2918). Match `await`
|
|
112
|
+
// semantics: the failure is thrown back into the generator at the
|
|
113
|
+
// yield point (catchable there); if uncaught, step()'s guard settles
|
|
114
|
+
// the action so its iterator never leaks in the transition. The
|
|
115
|
+
// settled flag implements A+ 2.3.3.3.4.1: a throw after the thenable
|
|
116
|
+
// already called a callback is ignored.
|
|
117
|
+
let r = false;
|
|
118
|
+
try {
|
|
119
|
+
if (isThenable(e.value)) return void e.value.then(e => {
|
|
120
|
+
if (r) return;
|
|
121
|
+
r = true;
|
|
122
|
+
restoreTransition(o, () => step(e));
|
|
123
|
+
}, e => {
|
|
124
|
+
if (r) return;
|
|
125
|
+
r = true;
|
|
126
|
+
restoreTransition(o, () => step(e, true));
|
|
127
|
+
});
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (r) return;
|
|
130
|
+
r = true;
|
|
131
|
+
return void restoreTransition(o, () => step(e, true));
|
|
132
|
+
}
|
|
133
|
+
restoreTransition(o, () => step(e.value));
|
|
134
|
+
};
|
|
135
|
+
step();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { action };
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { STATUS_UNINITIALIZED, STATUS_ERROR, STATUS_PENDING, REACTIVE_DIRTY, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
import { untrack, context, setSignal } from "./core.js";
|
|
4
|
+
|
|
5
|
+
import "./invariants.js";
|
|
6
|
+
|
|
7
|
+
import { NotReadyError, StatusError } from "./error.js";
|
|
8
|
+
|
|
9
|
+
import { trimStaleDeps, unobserved } from "./graph.js";
|
|
10
|
+
|
|
11
|
+
import { enqueueSub } from "./heap.js";
|
|
12
|
+
|
|
13
|
+
import { resolveTransition, hasActiveOverride, assignOrMergeLane, resolveLane } from "./lanes.js";
|
|
14
|
+
|
|
15
|
+
import { cleanup } from "./owner.js";
|
|
16
|
+
|
|
17
|
+
import { globalQueue, GlobalQueue, schedule, flush, queuePendingNode, insertSubs, clock, currentTransition } from "./scheduler.js";
|
|
18
|
+
|
|
19
|
+
// The lazily-created Set is the ONE container for pending sources. Its
|
|
20
|
+
// predecessor — a singular slot promoted to a Set on the second source —
|
|
21
|
+
// created dual state whose migration invariant was easy to break: a third
|
|
22
|
+
// overlapping source landed beside the Set and removePendingSource refused
|
|
23
|
+
// to clear it, stranding the Set members' pending forever (#2893).
|
|
24
|
+
function addPendingSource(e, n) {
|
|
25
|
+
if (e.oe?.has(n)) return false;
|
|
26
|
+
(e.oe ??= new Set).add(n);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function removePendingSource(e, n) {
|
|
31
|
+
if (!e.oe?.delete(n)) return false;
|
|
32
|
+
if (e.oe.size === 0) e.oe = undefined;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clearPendingSources(e) {
|
|
37
|
+
e.oe?.clear();
|
|
38
|
+
e.oe = undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A loading-window node hit an unready source (sync throw in recompute, or a
|
|
43
|
+
* NotReadyError-rejected flight): register for the source's settle — the
|
|
44
|
+
* settlePendingSource walk runs off `_pendingSources` + `_blocked` alone —
|
|
45
|
+
* with NO read-visible pending status, no downstream propagation, no
|
|
46
|
+
* transition, no lane registration. Commit #0 keeps serving.
|
|
47
|
+
*/ function parkLoadingWindow(e, n) {
|
|
48
|
+
e.ue = true;
|
|
49
|
+
if (n.source) addPendingSource(e, n.source);
|
|
50
|
+
// A settled error is the node's answer ("the error stays the answer until
|
|
51
|
+
// this retry can actually run") — the park must not replace it: reads
|
|
52
|
+
// throw `_error` while STATUS_ERROR is set, and overwriting it here leaks
|
|
53
|
+
// a pending-class NotReadyError from a read-invisible park (#2989).
|
|
54
|
+
if (!(e.S & STATUS_ERROR)) setPendingError(e, n.source, n);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setPendingError(e, n, t) {
|
|
58
|
+
if (!n) {
|
|
59
|
+
e._ = null;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (t instanceof NotReadyError && t.source === n) {
|
|
63
|
+
e._ = t;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const r = e._;
|
|
67
|
+
if (!(r instanceof NotReadyError) || r.source !== n) {
|
|
68
|
+
e._ = new NotReadyError(n);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function forEachDependent(e, n) {
|
|
73
|
+
for (let t = e.o; t !== null; t = t.le) n(t.fe, t);
|
|
74
|
+
// `?? null`: affects() marks route plain signals (no `_child` slot) through here.
|
|
75
|
+
for (let t = e.u ?? null; t !== null; t = t.ae) {
|
|
76
|
+
for (let e = t.o; e !== null; e = e.le) n(e.fe, e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Queue a node to re-run on the next flush (used both when a pending source
|
|
81
|
+
// settles and when an `isPending` observer must re-evaluate after a real error):
|
|
82
|
+
// shared scheduling helper in heap.ts (tracked effects bypass the heap).
|
|
83
|
+
// Settle-time counterpart of unlinkSubs' last-one-out check. A lazy node that
|
|
84
|
+
// loses its last subscriber while STATUS_PENDING is exempt from autodispose
|
|
85
|
+
// (the in-flight work is an observer), so whatever CLEARS that pending state
|
|
86
|
+
// must run the release — otherwise the node stays linked and recomputes
|
|
87
|
+
// forever with zero subscribers (#2934). The node's own promise/iterator
|
|
88
|
+
// callbacks handle their own release (settleAutodispose in handleAsync); this
|
|
89
|
+
// covers derivatively-pending dependents, which have no callbacks of their own.
|
|
90
|
+
function releaseIfSettledUnobserved(e) {
|
|
91
|
+
e.ce && e.T & CONFIG_AUTO_DISPOSE && !e.o && !(e.se & REACTIVE_ZOMBIE) && !(e.S & STATUS_PENDING) && unobserved(e);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Error-path sweep: notifyStatus(STATUS_ERROR) clears dependents' pending
|
|
95
|
+
// sources through its own recursion (no per-node settle callback), so after
|
|
96
|
+
// the propagation completes, walk the same graph for stranded lazy nodes.
|
|
97
|
+
// Collect-then-release so unobserved() never unlinks under the walk.
|
|
98
|
+
function releaseSettledDependents(e) {
|
|
99
|
+
let n;
|
|
100
|
+
const t = new Set;
|
|
101
|
+
const visit = e => {
|
|
102
|
+
if (t.has(e)) return;
|
|
103
|
+
t.add(e);
|
|
104
|
+
if (!e.o && e.T & CONFIG_AUTO_DISPOSE) (n ??= []).push(e);
|
|
105
|
+
forEachDependent(e, visit);
|
|
106
|
+
};
|
|
107
|
+
forEachDependent(e, visit);
|
|
108
|
+
if (n) for (const e of n) releaseIfSettledUnobserved(e);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Error-dimension twin of settlePendingSource's blocked re-enqueue (#2949):
|
|
112
|
+
// a node in STATUS_ERROR that recovers by recomputing to an UNCHANGED value
|
|
113
|
+
// fires no value notification — the recovery is completely silent. But a
|
|
114
|
+
// dependent that re-ran during the error window consumed its dirty flag and
|
|
115
|
+
// committed nothing (the fresh sibling values it read were absorbed into an
|
|
116
|
+
// errored run), so its committed value is stale. The propagated error is one
|
|
117
|
+
// object identity down the whole dependent tree, and holding it is exactly
|
|
118
|
+
// the "blocked on this error" marker — re-enqueue those holders so they
|
|
119
|
+
// re-run: fresh values commit and flow, and a dependent with another
|
|
120
|
+
// still-broken source simply re-errors. The async dimension needs no twin of
|
|
121
|
+
// its own: recovery there passes through a pending window whose re-runners
|
|
122
|
+
// set _blocked and ride settlePendingSource. Walks the full dependent graph
|
|
123
|
+
// (releaseSettledDependents shape): identity holders can sit below an
|
|
124
|
+
// intermediate whose own error state has since been scrubbed or replaced
|
|
125
|
+
// (e.g. an error boundary's tree node).
|
|
126
|
+
function settleErroredDependents(e, n) {
|
|
127
|
+
let t = false;
|
|
128
|
+
const r = new Set;
|
|
129
|
+
const visit = e => {
|
|
130
|
+
if (r.has(e)) return;
|
|
131
|
+
r.add(e);
|
|
132
|
+
if (e._ === n) {
|
|
133
|
+
enqueueSub(e);
|
|
134
|
+
t = true;
|
|
135
|
+
}
|
|
136
|
+
forEachDependent(e, visit);
|
|
137
|
+
};
|
|
138
|
+
forEachDependent(e, visit);
|
|
139
|
+
if (t) schedule();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function settlePendingSource(e) {
|
|
143
|
+
let n = false;
|
|
144
|
+
let t;
|
|
145
|
+
const r = new Set;
|
|
146
|
+
// Companion updates no-op without the verdict layer (null hook).
|
|
147
|
+
const o = GlobalQueue.Se;
|
|
148
|
+
const settle = u => {
|
|
149
|
+
if (r.has(u) || !removePendingSource(u, e)) return;
|
|
150
|
+
r.add(u);
|
|
151
|
+
u.de = clock;
|
|
152
|
+
const i = u.oe?.values().next().value;
|
|
153
|
+
// STATUS_ERROR + pending sources only coexist via an errored loading
|
|
154
|
+
// window's park (notifyStatus(STATUS_ERROR) clears pending sources
|
|
155
|
+
// otherwise): the settled error stays the answer through the settle —
|
|
156
|
+
// nulling it here would have reads throw `null` until the re-enqueued
|
|
157
|
+
// retry lands, or lose it entirely if that retry parks again (#2989).
|
|
158
|
+
const l = u.S & STATUS_ERROR;
|
|
159
|
+
if (i) {
|
|
160
|
+
if (!l) setPendingError(u, i);
|
|
161
|
+
o !== null && o(u);
|
|
162
|
+
} else {
|
|
163
|
+
u.S &= ~STATUS_PENDING;
|
|
164
|
+
if (!l) setPendingError(u);
|
|
165
|
+
o !== null && o(u);
|
|
166
|
+
if (u.ue) {
|
|
167
|
+
enqueueSub(u);
|
|
168
|
+
n = true;
|
|
169
|
+
}
|
|
170
|
+
u.ue = false;
|
|
171
|
+
// Fully settled with nobody watching: release candidate (#2934). Checked
|
|
172
|
+
// again at release time — deferred so unobserved() can't unlink subs
|
|
173
|
+
// lists this walk is still iterating.
|
|
174
|
+
if (!u.o && u.T & CONFIG_AUTO_DISPOSE) (t ??= []).push(u);
|
|
175
|
+
}
|
|
176
|
+
forEachDependent(u, settle);
|
|
177
|
+
};
|
|
178
|
+
forEachDependent(e, settle);
|
|
179
|
+
// Release before the flush schedule below: unobserved() pulls the node back
|
|
180
|
+
// out of the heap, so the enqueueSub above never recomputes a released node.
|
|
181
|
+
if (t) for (const e of t) releaseIfSettledUnobserved(e);
|
|
182
|
+
if (n) schedule();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Object-thenable detection (Promises/A+ shape).
|
|
186
|
+
function isThenable(e) {
|
|
187
|
+
return e != null && typeof e === "object" && typeof e.then === "function";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleAsync(e, n, t) {
|
|
191
|
+
let r = false;
|
|
192
|
+
let o = false;
|
|
193
|
+
if (typeof n === "object" && n !== null) {
|
|
194
|
+
untrack(() => {
|
|
195
|
+
r = n[Symbol.asyncIterator];
|
|
196
|
+
o = !r && isThenable(n);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (!o && !r) {
|
|
200
|
+
e.Te = null;
|
|
201
|
+
// A sync landing is the first real answer for a loadingValue node.
|
|
202
|
+
e.Ee = false;
|
|
203
|
+
return n;
|
|
204
|
+
}
|
|
205
|
+
e.Te = n;
|
|
206
|
+
let u;
|
|
207
|
+
// Settle-time transition re-entry. The loading rail is invisible to
|
|
208
|
+
// transactions (#2933): a boundary-caught first load never registers as an
|
|
209
|
+
// async reporter, so its settle — the boundary's fallback -> content
|
|
210
|
+
// reveal — must flow ambiently. The node can still carry a `_transition`
|
|
211
|
+
// stamp (pending-node bookkeeping rides through the stamping sites), and
|
|
212
|
+
// blindly re-entering that stamped, still-incomplete transaction stashed
|
|
213
|
+
// the reveal with it — a deadlock when the transaction's completion
|
|
214
|
+
// depended on the reveal (#2937). An ESCAPED first load did register and
|
|
215
|
+
// keeps transition scheduling; initialized (value-holding) pending settles
|
|
216
|
+
// are the transaction's reveal machinery and always re-enter.
|
|
217
|
+
const settleTransition = () => {
|
|
218
|
+
const n = resolveTransition(e);
|
|
219
|
+
if (n && e.S & STATUS_UNINITIALIZED && !currentTransition(n).Ie.has(e)) {
|
|
220
|
+
// Drop the stale stamp too: the plain settle write (setSignal) and the
|
|
221
|
+
// stash-path restamp both re-enter the transaction through it.
|
|
222
|
+
e.Ne = null;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
globalQueue.initTransition(n);
|
|
226
|
+
};
|
|
227
|
+
const handleError = t => {
|
|
228
|
+
if (e.Te !== n) return;
|
|
229
|
+
// NotReadyError from rejected promises should be treated as pending, not error
|
|
230
|
+
let r = t instanceof NotReadyError;
|
|
231
|
+
if (r && e.Ee) {
|
|
232
|
+
// Loading window: the flight died waiting on an unready source. Keep
|
|
233
|
+
// serving commit #0 — same parking as recompute's catch for sync
|
|
234
|
+
// dependency throws. The dead flight is released so the clock-gated
|
|
235
|
+
// error-retry pull (updateIfNecessary) can also re-ask.
|
|
236
|
+
e.Te = null;
|
|
237
|
+
parkLoadingWindow(e, t);
|
|
238
|
+
e.de = clock;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
settleTransition();
|
|
242
|
+
notifyStatus(e, r ? STATUS_PENDING : STATUS_ERROR, t);
|
|
243
|
+
e.de = clock;
|
|
244
|
+
// A real error settles derivatively-pending dependents (notifyStatus
|
|
245
|
+
// cleared their pending sources), so stranded lazy ones release here —
|
|
246
|
+
// the error twin of settlePendingSource's release (#2934).
|
|
247
|
+
if (!r) releaseSettledDependents(e);
|
|
248
|
+
};
|
|
249
|
+
const asyncWrite = (r, o) => {
|
|
250
|
+
if (e.Te !== n) return;
|
|
251
|
+
// If the node was dirtied by a newer write (optimistic override or regular),
|
|
252
|
+
// skip this stale async result — the upcoming flush will recompute the node
|
|
253
|
+
// with the new value, creating a fresh Promise that supersedes this one.
|
|
254
|
+
if (e.se & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
|
|
255
|
+
settleTransition();
|
|
256
|
+
const u = !!(e.S & STATUS_UNINITIALIZED);
|
|
257
|
+
trimStaleDeps(e);
|
|
258
|
+
clearStatus(e);
|
|
259
|
+
const i = resolveLane(e);
|
|
260
|
+
if (i) i.Ae.delete(e);
|
|
261
|
+
if (t) {
|
|
262
|
+
t(r);
|
|
263
|
+
if (u) clearStatus(e, true);
|
|
264
|
+
} else if (e._e !== undefined) {
|
|
265
|
+
// Optimistic node — resting OR covered by an active override — holds
|
|
266
|
+
// through the shared pending-node path, exactly like a plain async memo,
|
|
267
|
+
// so the commit clears STATUS_UNINITIALIZED (#2806) and elevation to
|
|
268
|
+
// _value happens on this value's OWN transition schedule (A18 as
|
|
269
|
+
// re-ruled 2026-07-07: _value only changes at commit points). With an
|
|
270
|
+
// override active the hold and its eventual commit are unobservable
|
|
271
|
+
// (A17 — every reader sees the override); the revert reveals whatever
|
|
272
|
+
// has committed by then, so corrections reveal atomically with their
|
|
273
|
+
// transition rather than escaping it.
|
|
274
|
+
if (e.De === NOT_PENDING) queuePendingNode(e);
|
|
275
|
+
e.De = r;
|
|
276
|
+
// The hold is a companion-visible write like any other (A13/A19): the
|
|
277
|
+
// clearStatus() above computed its verdict before the hold existed, so
|
|
278
|
+
// isPending must re-derive (the value is not final until commit — V1)
|
|
279
|
+
// and latest() must see the fresh in-flight value (V2). Subscribers are
|
|
280
|
+
// only notified when the hold is visible to them: under an active
|
|
281
|
+
// override every reader sees the override (A17), so waking subs would
|
|
282
|
+
// re-show an unchanged view — the revert is the notification point.
|
|
283
|
+
GlobalQueue.Pe !== null && GlobalQueue.Pe(e, r);
|
|
284
|
+
if (!hasActiveOverride(e)) insertSubs(e);
|
|
285
|
+
e.de = clock;
|
|
286
|
+
} else if (i) {
|
|
287
|
+
// Route through lane's effect queue for independent flushing
|
|
288
|
+
const n = e.Re;
|
|
289
|
+
const t = e.Ue;
|
|
290
|
+
const o = e.be;
|
|
291
|
+
try {
|
|
292
|
+
if (!n && u || !o || !o(r, t)) {
|
|
293
|
+
e.Ue = r;
|
|
294
|
+
e.de = clock;
|
|
295
|
+
// The latest() shadow write gives latest() effects independent lanes; the
|
|
296
|
+
// _pendingSignal update is a no-op repeat of the clearStatus() call above
|
|
297
|
+
// (computePendingState doesn't read _value).
|
|
298
|
+
GlobalQueue.Pe !== null && GlobalQueue.Pe(e, r);
|
|
299
|
+
insertSubs(e, true);
|
|
300
|
+
}
|
|
301
|
+
} catch (n) {
|
|
302
|
+
// A user comparator throwing during async resolution has no caller to
|
|
303
|
+
// surface to (we're in promise machinery) — route it through the node's
|
|
304
|
+
// error status so boundaries contain it instead of an unhandled
|
|
305
|
+
// rejection (#2837).
|
|
306
|
+
notifyStatus(e, STATUS_ERROR, n);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
try {
|
|
310
|
+
setSignal(e, () => r);
|
|
311
|
+
} catch (n) {
|
|
312
|
+
// Same containment as above: setSignal's comparator throw is the only
|
|
313
|
+
// pre-commit failure here, and there is no user callsite to throw to.
|
|
314
|
+
notifyStatus(e, STATUS_ERROR, n);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// First real answer landing: the window closes when the answer becomes
|
|
318
|
+
// OBSERVABLE. A direct commit is observable now; a transition-held write
|
|
319
|
+
// (`_pendingValue` set above or inside setSignal) is not — the verdict's
|
|
320
|
+
// held-value branch is window-gated, and commitPendingNode closes the
|
|
321
|
+
// window when the hold commits, so no one-frame isPending pulse can leak
|
|
322
|
+
// to live observers between the landing and its commit (#2990).
|
|
323
|
+
if (e.De === NOT_PENDING) e.Ee = false;
|
|
324
|
+
settlePendingSource(e);
|
|
325
|
+
schedule();
|
|
326
|
+
flush();
|
|
327
|
+
o?.();
|
|
328
|
+
};
|
|
329
|
+
// A pending node's in-flight promise is an observer: `unlinkSubs` skips
|
|
330
|
+
// autodispose while STATUS_PENDING so subscriber churn can't orphan the
|
|
331
|
+
// work (a lazy async memo would otherwise tear down and re-execute — one
|
|
332
|
+
// fetch per suspended re-read). Settling is that observer's release, so
|
|
333
|
+
// it runs the same last-one-out check the other release sites run.
|
|
334
|
+
// Returns whether the node released, so the iterator branch can stop
|
|
335
|
+
// pulling values instead of pumping an unobserved stream forever (#2935).
|
|
336
|
+
const settleAutodispose = () => {
|
|
337
|
+
if (e.T & CONFIG_AUTO_DISPOSE && !e.o && !(e.S & STATUS_PENDING)) {
|
|
338
|
+
unobserved(e);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
};
|
|
343
|
+
if (o) {
|
|
344
|
+
let t = false, r = false, o, i = true;
|
|
345
|
+
n.then(e => {
|
|
346
|
+
if (i) {
|
|
347
|
+
u = e;
|
|
348
|
+
t = true;
|
|
349
|
+
} else {
|
|
350
|
+
asyncWrite(e);
|
|
351
|
+
settleAutodispose();
|
|
352
|
+
}
|
|
353
|
+
}, e => {
|
|
354
|
+
if (i) {
|
|
355
|
+
o = e;
|
|
356
|
+
r = true;
|
|
357
|
+
} else {
|
|
358
|
+
handleError(e);
|
|
359
|
+
settleAutodispose();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
i = false;
|
|
363
|
+
if (r) {
|
|
364
|
+
// Settle through the same status path an async rejection uses, then
|
|
365
|
+
// unwind the in-progress synchronous read so the errored node isn't
|
|
366
|
+
// momentarily read as `undefined`.
|
|
367
|
+
handleError(o);
|
|
368
|
+
throw o;
|
|
369
|
+
} else if (!t) {
|
|
370
|
+
// Loading window: serve commit #0 instead of suspending. No transition
|
|
371
|
+
// is opened — first-flight work on a loadingValue node is loading-class
|
|
372
|
+
// (invisible to boundaries and transitions); the flight itself is
|
|
373
|
+
// already registered in _inFlight and lands through asyncWrite.
|
|
374
|
+
if (e.Ee) return e.Ue;
|
|
375
|
+
globalQueue.initTransition(resolveTransition(e));
|
|
376
|
+
throw new NotReadyError(context);
|
|
377
|
+
} else {
|
|
378
|
+
// Synchronously-resolved promise: the first real answer landed.
|
|
379
|
+
e.Ee = false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (r) {
|
|
383
|
+
const t = n[Symbol.asyncIterator]();
|
|
384
|
+
let r = false;
|
|
385
|
+
let o = false;
|
|
386
|
+
let i = true;
|
|
387
|
+
cleanup(() => {
|
|
388
|
+
if (o) return;
|
|
389
|
+
o = true;
|
|
390
|
+
try {
|
|
391
|
+
const e = t.return?.();
|
|
392
|
+
if (isThenable(e)) e.then(undefined, () => {});
|
|
393
|
+
} catch {}
|
|
394
|
+
});
|
|
395
|
+
// Release check before each next pull: an unobserved lazy node must tear
|
|
396
|
+
// down (its cleanup above closes the iterator) instead of pumping the
|
|
397
|
+
// stream forever with zero subscribers (#2935).
|
|
398
|
+
const iterateOrRelease = () => {
|
|
399
|
+
if (!settleAutodispose()) iterate();
|
|
400
|
+
};
|
|
401
|
+
const iterate = () => {
|
|
402
|
+
let l, s, f = false, a = false, c = true;
|
|
403
|
+
// Protocol tolerance, matching `for await`: `await` unwraps whatever
|
|
404
|
+
// next() returns — a thenable OR a bare IteratorResult. Real producers
|
|
405
|
+
// use the bare form as a promise-free fast path when a value is already
|
|
406
|
+
// buffered (seroval's deserialized streams do), so a bare result is
|
|
407
|
+
// assimilated as an already-settled step instead of crashing on `.then`.
|
|
408
|
+
const S = t.next();
|
|
409
|
+
const d = isThenable(S) ? S : {
|
|
410
|
+
then: e => void e(S)
|
|
411
|
+
};
|
|
412
|
+
d.then(t => {
|
|
413
|
+
// The sync stash only serves the INITIAL drain (handleAsync's caller
|
|
414
|
+
// consumes syncValue / throws NotReady from it). A sync-settled step
|
|
415
|
+
// after an async gap — seroval buffering values between pulls, a
|
|
416
|
+
// sync-thenable producer mid-stream — has no caller reading the
|
|
417
|
+
// stash: it must write through the async path or the value is
|
|
418
|
+
// silently dropped.
|
|
419
|
+
if (c && i) {
|
|
420
|
+
l = t;
|
|
421
|
+
f = true;
|
|
422
|
+
if (t.done) o = true;
|
|
423
|
+
} else if (e.Te !== n) {
|
|
424
|
+
return;
|
|
425
|
+
} else if (!t.done) {
|
|
426
|
+
r = true;
|
|
427
|
+
asyncWrite(t.value, iterateOrRelease);
|
|
428
|
+
} else {
|
|
429
|
+
o = true;
|
|
430
|
+
if (r) {
|
|
431
|
+
schedule();
|
|
432
|
+
flush();
|
|
433
|
+
} else {
|
|
434
|
+
// Empty completion settles like the immediately-done sync path.
|
|
435
|
+
asyncWrite(undefined);
|
|
436
|
+
}
|
|
437
|
+
settleAutodispose();
|
|
438
|
+
}
|
|
439
|
+
}, t => {
|
|
440
|
+
if (c) {
|
|
441
|
+
s = t;
|
|
442
|
+
a = true;
|
|
443
|
+
} else if (e.Te === n) {
|
|
444
|
+
o = true;
|
|
445
|
+
handleError(t);
|
|
446
|
+
settleAutodispose();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
c = false;
|
|
450
|
+
if (a) {
|
|
451
|
+
// Match the promise branch, but only rethrow during the initial read.
|
|
452
|
+
o = true;
|
|
453
|
+
handleError(s);
|
|
454
|
+
if (i) throw s;
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
if (f && !l.done) {
|
|
458
|
+
u = l.value;
|
|
459
|
+
r = true;
|
|
460
|
+
return iterate();
|
|
461
|
+
}
|
|
462
|
+
return f && l.done;
|
|
463
|
+
};
|
|
464
|
+
const l = iterate();
|
|
465
|
+
// Later iterate() calls run from asyncWrite, where rethrowing would be unhandled.
|
|
466
|
+
i = false;
|
|
467
|
+
if (!r && !l) {
|
|
468
|
+
// Loading window: serve commit #0 (see the promise branch above).
|
|
469
|
+
if (e.Ee) return e.Ue;
|
|
470
|
+
globalQueue.initTransition(resolveTransition(e));
|
|
471
|
+
throw new NotReadyError(context);
|
|
472
|
+
}
|
|
473
|
+
// A sync first yield (or immediate empty completion) is the first real
|
|
474
|
+
// answer; async yields clear inside asyncWrite.
|
|
475
|
+
e.Ee = false;
|
|
476
|
+
}
|
|
477
|
+
return u;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function clearStatus(e, n = false) {
|
|
481
|
+
if (e.oe) clearPendingSources(e);
|
|
482
|
+
if (e.ue) e.ue = false;
|
|
483
|
+
// The pending window is over; its quiet classification dies with it.
|
|
484
|
+
// (Unconditional: _reask is baked into the node literals, so this is a
|
|
485
|
+
// plain store to an existing slot — no shape change.)
|
|
486
|
+
e.he = false;
|
|
487
|
+
e.S = n ? 0 : e.S & STATUS_UNINITIALIZED;
|
|
488
|
+
if (e._) setPendingError(e);
|
|
489
|
+
// Update pending signal for isPending() reactivity (companions only exist
|
|
490
|
+
// once the verdict layer created them, which installs the hooks).
|
|
491
|
+
if (e.ge || e.pe) GlobalQueue.Se(e);
|
|
492
|
+
if (e.u && GlobalQueue.Oe !== null) GlobalQueue.Oe(e);
|
|
493
|
+
if (e.i) e.i();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function notifyStatus(e, n, t, r, o) {
|
|
497
|
+
// Wrap regular errors to track source node
|
|
498
|
+
if (n === STATUS_ERROR && !(t instanceof StatusError) && !(t instanceof NotReadyError)) t = new StatusError(e, t);
|
|
499
|
+
const u = n === STATUS_PENDING && t instanceof NotReadyError ? t.source : undefined;
|
|
500
|
+
const i = u === e;
|
|
501
|
+
const l = n === STATUS_PENDING && e._e !== undefined && !i;
|
|
502
|
+
const s = l && hasActiveOverride(e);
|
|
503
|
+
if (!r) {
|
|
504
|
+
if (n === STATUS_PENDING && u) {
|
|
505
|
+
addPendingSource(e, u);
|
|
506
|
+
e.S = STATUS_PENDING | e.S & STATUS_UNINITIALIZED;
|
|
507
|
+
// Preserve the current source on this propagation so render-effect notification
|
|
508
|
+
// can register every distinct pending source with the transition.
|
|
509
|
+
setPendingError(e, u, t);
|
|
510
|
+
} else {
|
|
511
|
+
clearPendingSources(e);
|
|
512
|
+
e.S = n | (n !== STATUS_ERROR ? e.S & STATUS_UNINITIALIZED : 0);
|
|
513
|
+
e._ = t;
|
|
514
|
+
}
|
|
515
|
+
GlobalQueue.Se !== null && GlobalQueue.Se(e);
|
|
516
|
+
if (e.u && GlobalQueue.Oe !== null) GlobalQueue.Oe(e);
|
|
517
|
+
}
|
|
518
|
+
if (o && !r) {
|
|
519
|
+
assignOrMergeLane(e, o);
|
|
520
|
+
}
|
|
521
|
+
const f = r || s;
|
|
522
|
+
const a = r || l ? undefined : o;
|
|
523
|
+
if (e.i) {
|
|
524
|
+
if (r && n === STATUS_PENDING) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (f) {
|
|
528
|
+
e.i(n, t);
|
|
529
|
+
} else {
|
|
530
|
+
e.i();
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
forEachDependent(e, (e, r) => {
|
|
535
|
+
e.de = clock;
|
|
536
|
+
if (n === STATUS_PENDING && u && !e.oe?.has(u) || n !== STATUS_PENDING && (e._ !== t || e.oe)) {
|
|
537
|
+
// A pending-observer link is the subscription an `isPending` read created.
|
|
538
|
+
// It exists so the observer re-runs when the source settles, but it must
|
|
539
|
+
// not carry a real (non-NotReadyError) error — the synchronous `isPending`
|
|
540
|
+
// read swallows those, and the async path must match. Re-run the observer
|
|
541
|
+
// so `isPending` re-evaluates (to not-pending) instead of forwarding.
|
|
542
|
+
if (r.Ge && n !== STATUS_PENDING && !(t instanceof NotReadyError)) {
|
|
543
|
+
enqueueSub(e);
|
|
544
|
+
schedule();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (!f && !e.Ne) queuePendingNode(e);
|
|
548
|
+
notifyStatus(e, n, t, f, a);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export { addPendingSource, clearStatus, forEachDependent, handleAsync, isThenable, notifyStatus, parkLoadingWindow, releaseSettledDependents, setPendingError, settleErroredDependents, settlePendingSource };
|