@solidjs/signals 2.0.0-beta.9 → 2.0.0-rc.1
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 +14 -10
- package/dist/dev.js +6740 -2011
- package/dist/node.cjs +7927 -3340
- 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 +607 -0
- package/dist/prod/core/constants.js +92 -0
- package/dist/prod/core/context.js +67 -0
- package/dist/prod/core/core.js +828 -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 +262 -0
- package/dist/prod/core/owner.js +302 -0
- package/dist/prod/core/scheduler.js +757 -0
- package/dist/prod/core/verdict.js +369 -0
- package/dist/prod/index.js +45 -0
- package/dist/prod/map.js +319 -0
- package/dist/prod/signals.js +394 -0
- package/dist/prod/store/index.js +36 -0
- package/dist/prod/store/next/optimistic.js +377 -0
- package/dist/prod/store/next/projection.js +172 -0
- package/dist/prod/store/next/reconcile.js +327 -0
- package/dist/prod/store/next/store.js +1232 -0
- package/dist/prod/store/next/target.js +20 -0
- package/dist/prod/store/store.js +325 -0
- package/dist/prod/store/storePath.js +103 -0
- package/dist/prod/store/utils.js +201 -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 +66 -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 +85 -11
- 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 +183 -12
- package/dist/types/store/index.d.ts +16 -6
- package/dist/types/store/next/optimistic.d.ts +20 -0
- package/dist/types/store/next/projection.d.ts +24 -0
- package/dist/types/store/next/reconcile.d.ts +3 -0
- package/dist/types/store/next/store.d.ts +71 -0
- package/dist/types/store/next/target.d.ts +99 -0
- 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 +91 -71
- package/dist/types/store/utils.d.ts +0 -40
- 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 +66 -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 +85 -11
- 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 +183 -12
- package/dist/types-cjs/store/index.d.cts +16 -6
- package/dist/types-cjs/store/next/optimistic.d.cts +20 -0
- package/dist/types-cjs/store/next/projection.d.cts +24 -0
- package/dist/types-cjs/store/next/reconcile.d.cts +3 -0
- package/dist/types-cjs/store/next/store.d.cts +71 -0
- package/dist/types-cjs/store/next/target.d.cts +99 -0
- 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 +91 -71
- package/dist/types-cjs/store/utils.d.cts +0 -40
- package/package.json +12 -9
- package/dist/prod.js +0 -3627
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { NOT_PENDING } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
import { currentTransition, activeTransition } from "./scheduler.js";
|
|
4
|
+
|
|
5
|
+
// Map from optimistic signal to its lane (reused for multiple writes to same signal)
|
|
6
|
+
const signalLanes = new WeakMap;
|
|
7
|
+
|
|
8
|
+
// All active lanes (for cleanup on transition completion)
|
|
9
|
+
const activeLanes = new Set;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get an existing lane for a signal or create a new one.
|
|
13
|
+
* Reuses lane for multiple writes to the same signal.
|
|
14
|
+
*/ function getOrCreateLane(n) {
|
|
15
|
+
let e = signalLanes.get(n);
|
|
16
|
+
if (e) {
|
|
17
|
+
return findLane(e);
|
|
18
|
+
}
|
|
19
|
+
// Detect parent lane: _parentSource chains from pendingSignal → pendingValueComputed → original.
|
|
20
|
+
// The child lane should not merge with the parent lane.
|
|
21
|
+
const i = n.nn;
|
|
22
|
+
const r = i?.Ke ? findLane(i.Ke) : null;
|
|
23
|
+
e = {
|
|
24
|
+
en: n,
|
|
25
|
+
Ae: new Set,
|
|
26
|
+
rn: [ [], [] ],
|
|
27
|
+
tn: null,
|
|
28
|
+
Ne: activeTransition,
|
|
29
|
+
an: r
|
|
30
|
+
};
|
|
31
|
+
signalLanes.set(n, e);
|
|
32
|
+
activeLanes.add(e);
|
|
33
|
+
// A companion may have written before the owner's first optimistic write
|
|
34
|
+
// (affects() as an action's first statement pokes the verdict companion of a
|
|
35
|
+
// still lane-less node, #2887), leaving its lane parentless. Adopt it now:
|
|
36
|
+
// parent-child is a property of the nodes, not of write order — otherwise
|
|
37
|
+
// the owner's write merges the companion's subscribers into this lane and
|
|
38
|
+
// their effects wait on its async instead of flushing immediately.
|
|
39
|
+
adoptCompanionLane(n.Oe, e);
|
|
40
|
+
adoptCompanionLane(n.ge, e);
|
|
41
|
+
return e;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function adoptCompanionLane(n, e) {
|
|
45
|
+
if (!n) return;
|
|
46
|
+
const i = signalLanes.get(n);
|
|
47
|
+
if (!i) return;
|
|
48
|
+
const r = findLane(i);
|
|
49
|
+
// Only the companion's own unmerged root is safely re-parentable: a root
|
|
50
|
+
// that absorbed other lanes carries work that is not a child of this owner.
|
|
51
|
+
if (r !== e && r.en === n && !r.an) r.an = e;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Union-find: find the root lane.
|
|
56
|
+
*/ function findLane(n) {
|
|
57
|
+
while (n.tn) n = n.tn;
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Merge two lanes when their dependency graphs overlap.
|
|
63
|
+
*/ function mergeLanes(n, e) {
|
|
64
|
+
n = findLane(n);
|
|
65
|
+
e = findLane(e);
|
|
66
|
+
if (n === e) return n;
|
|
67
|
+
e.tn = n;
|
|
68
|
+
// Move (not copy) the merged lane's work: after the merge all routing goes
|
|
69
|
+
// through findLane() to the root, so anything left behind here is dead —
|
|
70
|
+
// and anything *added* here later is a routing bug (INV-5).
|
|
71
|
+
for (const i of e.Ae) n.Ae.add(i);
|
|
72
|
+
e.Ae.clear();
|
|
73
|
+
n.rn[0].push(...e.rn[0]);
|
|
74
|
+
n.rn[1].push(...e.rn[1]);
|
|
75
|
+
e.rn[0].length = 0;
|
|
76
|
+
e.rn[1].length = 0;
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a node's lane: follow union-find chain, verify active, clear if stale.
|
|
82
|
+
*/ function resolveLane(n) {
|
|
83
|
+
const e = n.Ke;
|
|
84
|
+
if (!e) return undefined;
|
|
85
|
+
const i = findLane(e);
|
|
86
|
+
if (activeLanes.has(i)) return i;
|
|
87
|
+
n.Ke = undefined;
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveTransition(n) {
|
|
92
|
+
// An active override answers with its owner, not its lane: lanes are
|
|
93
|
+
// scheduling affinity and a shared subscriber merges them across
|
|
94
|
+
// transactions (#2912) — the merged root's _transition would hand this
|
|
95
|
+
// node's override to whichever action wrote last through the shared
|
|
96
|
+
// reader. Chase merge chains; a dead owner settled through another path.
|
|
97
|
+
if (hasActiveOverride(n) && n.sn) {
|
|
98
|
+
const e = n.sn = currentTransition(n.sn);
|
|
99
|
+
if (e.fn !== true) return e;
|
|
100
|
+
n.sn = null;
|
|
101
|
+
}
|
|
102
|
+
return resolveLane(n)?.Ne ?? n.Ne;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a node has an active optimistic override.
|
|
107
|
+
*/ function hasActiveOverride(n) {
|
|
108
|
+
return !!(n.De !== undefined && n.De !== NOT_PENDING);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Assign or merge a lane onto a node. At convergence points (node already has
|
|
113
|
+
* a different active lane), merge unless the node has an active override.
|
|
114
|
+
*/ function assignOrMergeLane(n, e) {
|
|
115
|
+
const i = findLane(e);
|
|
116
|
+
const r = n.Ke;
|
|
117
|
+
if (r) {
|
|
118
|
+
// If the subscriber's lane was merged into another lane, it's stale —
|
|
119
|
+
// replace it with the new source lane instead of following the merge chain
|
|
120
|
+
// (which would incorrectly merge the new lane into the old group)
|
|
121
|
+
if (r.tn) {
|
|
122
|
+
n.Ke = e;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const t = findLane(r);
|
|
126
|
+
if (activeLanes.has(t)) {
|
|
127
|
+
if (t !== i && !hasActiveOverride(n)) {
|
|
128
|
+
// Parent-child lanes stay independent so isPending resolves without
|
|
129
|
+
// waiting for the parent's async. The child keeps ownership.
|
|
130
|
+
if (i.an && findLane(i.an) === t) {
|
|
131
|
+
n.Ke = e;
|
|
132
|
+
} else if (t.an && findLane(t.an) === i) ; else mergeLanes(i, t);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
n.Ke = e;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { activeLanes, assignOrMergeLane, findLane, getOrCreateLane, hasActiveOverride, mergeLanes, resolveLane, resolveTransition, signalLanes };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, OVERRIDE_UNDEFINED, STATUS_PENDING, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, EFFECT_RENDER, EFFECT_USER } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
import { latestReadActive, currentOptimisticLane, stale } from "./core.js";
|
|
4
|
+
|
|
5
|
+
import { NotReadyError } from "./error.js";
|
|
6
|
+
|
|
7
|
+
import "./invariants.js";
|
|
8
|
+
|
|
9
|
+
import { resolveTransition, getOrCreateLane, hasActiveOverride, activeLanes, signalLanes, findLane, resolveLane, assignOrMergeLane } from "./lanes.js";
|
|
10
|
+
|
|
11
|
+
import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, schedule } from "./scheduler.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The optimistic write engine, moved out of core.ts/scheduler.ts. Everything
|
|
15
|
+
* here serves only optimistic overrides — createOptimistic,
|
|
16
|
+
* createOptimisticStore (and its store-node writes), and the verdict layer's
|
|
17
|
+
* companions (which are optimistic nodes). Modules that can create optimistic
|
|
18
|
+
* state call `installOptimisticEngine()` before creating it; apps that never
|
|
19
|
+
* import one of those APIs never retain any of this.
|
|
20
|
+
*
|
|
21
|
+
* Core call sites fire the hooks behind guards on state only this module can
|
|
22
|
+
* create (`_overrideValue !== undefined`, `currentOptimisticLane !== null`,
|
|
23
|
+
* `_optimisticNodes.length`, `activeLanes.size`), so `!` invocations are safe
|
|
24
|
+
* once the gate holds — the same late-binding contract as verdict.ts.
|
|
25
|
+
*/
|
|
26
|
+
/** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */ function optimisticWrite(e, n) {
|
|
27
|
+
const t = e.De !== NOT_PENDING;
|
|
28
|
+
const i = t ? unwrapOverride(e.De) : e.be;
|
|
29
|
+
if (typeof n === "function") n = n(i);
|
|
30
|
+
const u = !!(e.S & STATUS_UNINITIALIZED) || !e.Ue || !e.Ue(i, n);
|
|
31
|
+
if (!u) {
|
|
32
|
+
// Same-value write with an active override still entangles the current
|
|
33
|
+
// action's transition — the hold must outlast all overlapping actions.
|
|
34
|
+
if (t) {
|
|
35
|
+
const n = resolveTransition(e);
|
|
36
|
+
if (n && activeTransition !== n) globalQueue.initTransition(n);
|
|
37
|
+
}
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
if (t) globalQueue.initTransition(resolveTransition(e));
|
|
41
|
+
// No revert target is stashed: while the override is active every reader
|
|
42
|
+
// sees it (A17), so authoritative arrivals commit silently into _value and
|
|
43
|
+
// reverting is just dropping the override — _value is already correct.
|
|
44
|
+
else globalQueue.m.Me.push(e);
|
|
45
|
+
// Stamp ownership on the node (post-merge, so entangled writers share the
|
|
46
|
+
// joint root). resolveTransition prefers this over the lane's _transition,
|
|
47
|
+
// which a shared subscriber can merge across transactions (#2912).
|
|
48
|
+
e.sn = activeTransition;
|
|
49
|
+
const r = getOrCreateLane(e);
|
|
50
|
+
e.Ke = r;
|
|
51
|
+
// Literal undefined must not land raw: the slot doubles as the optimistic
|
|
52
|
+
// brand, and erasing it makes the write invisible and routes follow-up
|
|
53
|
+
// writes off the optimistic path into permanent commits (#2898).
|
|
54
|
+
e.De = n === undefined ? OVERRIDE_UNDEFINED : n;
|
|
55
|
+
// syncCompanions only pokes _pendingSignal/_latestValueComputed — with
|
|
56
|
+
// neither companion present the call is a guaranteed no-op.
|
|
57
|
+
(e.Oe !== undefined || e.ge !== undefined) && GlobalQueue.Pe !== null && GlobalQueue.Pe(e, n);
|
|
58
|
+
e.de = clock;
|
|
59
|
+
insertSubs(e, true);
|
|
60
|
+
schedule();
|
|
61
|
+
return n;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* transitionComplete's override blockage: a settling transition stays open
|
|
66
|
+
* while one of its optimistic nodes holds an active override that is still
|
|
67
|
+
* pending on real (non-affects-sentinel) async.
|
|
68
|
+
*/ function transitionBlocked(e) {
|
|
69
|
+
for (let n = 0; n < e.Me.length; n++) {
|
|
70
|
+
const t = e.Me[n];
|
|
71
|
+
if (hasActiveOverride(t) && "S" in t && t.S & STATUS_PENDING && t._ instanceof NotReadyError) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveOptimisticNodes(e) {
|
|
79
|
+
// Settlement writes below (snapCompanionsToState → updatePendingSignal-style
|
|
80
|
+
// notifications) may push fresh optimistic nodes; only this batch settles
|
|
81
|
+
// now, so iterate a fixed window and splice it out at the end.
|
|
82
|
+
const n = e.length;
|
|
83
|
+
for (let t = 0; t < n; t++) {
|
|
84
|
+
const n = e[t];
|
|
85
|
+
n.Ke = undefined;
|
|
86
|
+
// Revert is a pure drop: there is no revert target to commit —
|
|
87
|
+
// override-covered authoritative values hold in _pendingValue and
|
|
88
|
+
// elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07).
|
|
89
|
+
if (!(n.S & STATUS_PENDING)) n.S &= ~STATUS_UNINITIALIZED;
|
|
90
|
+
const i = n.De;
|
|
91
|
+
n.De = NOT_PENDING;
|
|
92
|
+
if (i !== NOT_PENDING && n.be !== unwrapOverride(i)) insertSubs(n, true);
|
|
93
|
+
n.Ne = null;
|
|
94
|
+
n.sn = null;
|
|
95
|
+
}
|
|
96
|
+
// Settlement checkpoint (#2838): companions caught in this batch (or owned
|
|
97
|
+
// by a node in it) re-derive from committed state, so verdicts survive the
|
|
98
|
+
// transition that produced them (A19 — pending is a property of the data).
|
|
99
|
+
for (let t = 0; t < n; t++) {
|
|
100
|
+
const n = e[t];
|
|
101
|
+
if (n.Oe || n.ge) GlobalQueue.un(n);
|
|
102
|
+
const i = n.nn;
|
|
103
|
+
if (i && (i.Oe === n || i.ge === n)) GlobalQueue.un(i);
|
|
104
|
+
}
|
|
105
|
+
e.splice(0, n);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runQueue(e, n) {
|
|
109
|
+
for (let t = 0; t < e.length; t++) e[t](n);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run effects from all lanes that are ready (no pending async).
|
|
114
|
+
*/ function runLaneEffects(e) {
|
|
115
|
+
for (const n of activeLanes) {
|
|
116
|
+
if (n.tn || n.Ae.size > 0) continue;
|
|
117
|
+
const t = n.rn[e - 1];
|
|
118
|
+
if (t.length) {
|
|
119
|
+
n.rn[e - 1] = [];
|
|
120
|
+
runQueue(t, e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function cleanupCompletedLanes(e) {
|
|
126
|
+
for (const n of activeLanes) {
|
|
127
|
+
const t = e ? n.Ne === e : !n.Ne;
|
|
128
|
+
if (!t) continue;
|
|
129
|
+
if (!n.tn) {
|
|
130
|
+
if (n.rn[0].length) runQueue(n.rn[0], EFFECT_RENDER);
|
|
131
|
+
if (n.rn[1].length) runQueue(n.rn[1], EFFECT_USER);
|
|
132
|
+
}
|
|
133
|
+
if (n.en.Ke === n) n.en.Ke = undefined;
|
|
134
|
+
n.Ae.clear();
|
|
135
|
+
n.rn[0].length = 0;
|
|
136
|
+
n.rn[1].length = 0;
|
|
137
|
+
activeLanes.delete(n);
|
|
138
|
+
signalLanes.delete(n.en);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** read()'s per-lane suspension test (pending-throw path, lane context). */ function laneSuspends(e) {
|
|
143
|
+
// Per-lane suspension: only throw if in same lane as pending async
|
|
144
|
+
// AND the node doesn't have an active override (overrides are the visible value,
|
|
145
|
+
// downstream in the lane should read the override, not throw)
|
|
146
|
+
const n = e.Ke;
|
|
147
|
+
if (!n) return false;
|
|
148
|
+
return findLane(n) === findLane(currentOptimisticLane) && !hasActiveOverride(e);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* read()'s entanglement gate: a reader recomputing under an optimistic lane
|
|
153
|
+
* that reads a pending mid-transition write sees the committed value; the sub
|
|
154
|
+
* is recorded for replay at commit.
|
|
155
|
+
*/ function gatedRead(e, n, t) {
|
|
156
|
+
if (latestReadActive || e._e === NOT_PENDING || e.ce || n !== e && !(n.se & REACTIVE_MANUAL_WRITE)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
activeTransition.ln.add(t);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* read()'s value selection under a lane: return the committed `_value` for
|
|
165
|
+
* optimistic/lane-assigned signals, stale-mode reads, and pending owners.
|
|
166
|
+
*/ function laneReadsCommitted(e, n, t) {
|
|
167
|
+
if (e.De !== undefined || !!e.Ke || !!(n.S & STATUS_PENDING)) return true;
|
|
168
|
+
if (n === e && stale && t.nn !== e) {
|
|
169
|
+
// The committed view can hide a same-tick ambient write (a lane member —
|
|
170
|
+
// even just an isPending companion flip — puts the reader "under a lane"
|
|
171
|
+
// against unrelated plain writes). With no transaction the write commits
|
|
172
|
+
// at THIS flush's end with no re-delivery, so record the reader for
|
|
173
|
+
// replay at commit — the same contract gatedRead provides when a
|
|
174
|
+
// transaction is active (#2963). If a transaction forms mid-flush,
|
|
175
|
+
// initTransition carries the recording over to it.
|
|
176
|
+
if (e._e !== NOT_PENDING && activeTransition === null) globalQueue.m.ln.add(t);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* recompute()'s lane posture: resolve the node's own lane (own=true), or adopt
|
|
184
|
+
* a dependency's optimistic lane (own=false — parent-deeper-than-owned-child
|
|
185
|
+
* can run before its OPT-dirty child propagates).
|
|
186
|
+
*/ function recomputeLane(e, n) {
|
|
187
|
+
if (n) {
|
|
188
|
+
const n = resolveLane(e);
|
|
189
|
+
if (!n) return null;
|
|
190
|
+
// Wake-only lane demotion (#3009): a plain write to a latest()-tracked
|
|
191
|
+
// source rides the optimistic channel only to wake verdict companions —
|
|
192
|
+
// its lane is sourced by the companion shadow (_parentSource set) and owns
|
|
193
|
+
// no transaction. When such a node is pulled mid-tick by a latest()/
|
|
194
|
+
// isPending() probe, lane posture would direct-commit _value, leaking the
|
|
195
|
+
// queued write into committed reads before the flush. Return `false` so
|
|
196
|
+
// recompute() runs plain: the value stages and commits with the flush.
|
|
197
|
+
// (el's own override slot excludes companions themselves; a lane merged
|
|
198
|
+
// into a real optimistic lane resolves to a non-companion source.)
|
|
199
|
+
if (!globalQueue.cn && !activeTransition && !n.Ne && n.en.nn !== undefined && e.De === undefined) {
|
|
200
|
+
e.Ke = undefined;
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return n;
|
|
204
|
+
}
|
|
205
|
+
for (let n = e.tt; n; n = n.nt) {
|
|
206
|
+
const t = n.it;
|
|
207
|
+
if (t.se & REACTIVE_OPTIMISTIC_DIRTY) {
|
|
208
|
+
const n = resolveLane(t);
|
|
209
|
+
if (n) {
|
|
210
|
+
e.se |= REACTIVE_OPTIMISTIC_DIRTY;
|
|
211
|
+
assignOrMergeLane(e, n);
|
|
212
|
+
return n;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** recompute()'s catch path: track pending async in the current lane. */ function laneAsyncPending(e) {
|
|
220
|
+
const n = findLane(currentOptimisticLane);
|
|
221
|
+
if (n.en !== e) {
|
|
222
|
+
n.Ae.add(e);
|
|
223
|
+
e.Ke = n;
|
|
224
|
+
GlobalQueue.Se !== null && GlobalQueue.Se(n.en);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** recompute()'s success path: the node's async settled, clear it from its lane. */ function laneAsyncSettled(e) {
|
|
229
|
+
const n = resolveLane(e);
|
|
230
|
+
if (n) {
|
|
231
|
+
n.Ae.delete(e);
|
|
232
|
+
GlobalQueue.Se !== null && GlobalQueue.Se(n.en);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function trackOptimisticStore(e) {
|
|
237
|
+
// After initTransition, globalQueue._batch IS activeTransition (same reference)
|
|
238
|
+
globalQueue.m.dn.add(e);
|
|
239
|
+
schedule();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Installs the engine's hooks. Idempotent; called by every module that can
|
|
244
|
+
* create optimistic state (verdict.ts at module top level, createOptimistic
|
|
245
|
+
* and createOptimisticStore at first call) BEFORE any optimistic node exists.
|
|
246
|
+
*/ function installOptimisticEngine() {
|
|
247
|
+
if (GlobalQueue.Gt !== null) return;
|
|
248
|
+
GlobalQueue.Gt = optimisticWrite;
|
|
249
|
+
GlobalQueue.En = resolveOptimisticNodes;
|
|
250
|
+
GlobalQueue.Tn = transitionBlocked;
|
|
251
|
+
GlobalQueue.In = cleanupCompletedLanes;
|
|
252
|
+
GlobalQueue.Nn = runLaneEffects;
|
|
253
|
+
GlobalQueue.Rt = gatedRead;
|
|
254
|
+
GlobalQueue.Ot = laneSuspends;
|
|
255
|
+
GlobalQueue.Dt = laneReadsCommitted;
|
|
256
|
+
GlobalQueue.je = recomputeLane;
|
|
257
|
+
GlobalQueue.ze = laneAsyncPending;
|
|
258
|
+
GlobalQueue.Be = laneAsyncSettled;
|
|
259
|
+
GlobalQueue.Qn = trackOptimisticStore;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export { installOptimisticEngine };
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { REACTIVE_DISPOSED, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_ZOMBIE, CONFIG_TRANSPARENT, defaultContext } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
import { context, runWithOwner, pendingCheckActive, latestReadActive, tracking } from "./core.js";
|
|
4
|
+
|
|
5
|
+
import { unlinkSubs } from "./graph.js";
|
|
6
|
+
|
|
7
|
+
import { deleteFromHeap, queueFor, insertIntoHeap, insertIntoHeapHeight } from "./heap.js";
|
|
8
|
+
|
|
9
|
+
import { GlobalQueue, zombieQueue, dirtyQueue, globalQueue } from "./scheduler.js";
|
|
10
|
+
|
|
11
|
+
const PENDING_OWNER = {};
|
|
12
|
+
|
|
13
|
+
// Dummy owner to trigger store's read() path
|
|
14
|
+
function markDisposal(e) {
|
|
15
|
+
let n = e.ke;
|
|
16
|
+
while (n) {
|
|
17
|
+
const e = n.se;
|
|
18
|
+
n.se = e | REACTIVE_ZOMBIE;
|
|
19
|
+
// migrate height-adjust entries too, not just recompute entries: every
|
|
20
|
+
// `deleteFromHeap` call site picks the queue from the zombie flag, so a
|
|
21
|
+
// node left physically linked in `dirtyQueue` after being zombified gets
|
|
22
|
+
// unlinked from the wrong queue on dispose, corrupting the bucket and
|
|
23
|
+
// livelocking the next `runHeap` that reaches it (#2759)
|
|
24
|
+
if (e & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT)) {
|
|
25
|
+
deleteFromHeap(n, e & REACTIVE_ZOMBIE ? zombieQueue : dirtyQueue);
|
|
26
|
+
if (e & REACTIVE_IN_HEAP) insertIntoHeap(n, zombieQueue); else insertIntoHeapHeight(n, zombieQueue);
|
|
27
|
+
}
|
|
28
|
+
markDisposal(n);
|
|
29
|
+
n = n.He;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function dispose(e) {
|
|
34
|
+
// Leave every scheduler heap on disposal (mirrors `unobserved`): a node
|
|
35
|
+
// still queued here would be recomputed by the next flush, and recompute()
|
|
36
|
+
// rewriting `_flags` would clear REACTIVE_DISPOSED — resurrecting it (#2983).
|
|
37
|
+
deleteFromHeap(e, queueFor(e));
|
|
38
|
+
let n = e.tt;
|
|
39
|
+
while (n !== null) {
|
|
40
|
+
n = unlinkSubs(n);
|
|
41
|
+
}
|
|
42
|
+
e.tt = null;
|
|
43
|
+
e.Ye = null;
|
|
44
|
+
disposeChildren(e, true);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function disposeChildren(e, n = false, t) {
|
|
48
|
+
const i = e.se;
|
|
49
|
+
if (i & REACTIVE_DISPOSED) return;
|
|
50
|
+
if (n) {
|
|
51
|
+
e.se = i | REACTIVE_DISPOSED;
|
|
52
|
+
// Companions are created detached and outlive their owner, but a verdict
|
|
53
|
+
// must not: a disposed source can never settle, so an isPending companion
|
|
54
|
+
// latched `true` here would hold a spinner forever (INV-9, the PR #2845
|
|
55
|
+
// edge). Snap runs after the DISPOSED flag is set so the oracle reads
|
|
56
|
+
// false, and notifies subscribers still watching the companion.
|
|
57
|
+
const n = e;
|
|
58
|
+
if (n.Oe || n.ge) GlobalQueue.un(n);
|
|
59
|
+
}
|
|
60
|
+
if (n && e.ce) e.Te = null;
|
|
61
|
+
let l = t ? e.We : e.ke;
|
|
62
|
+
while (l) {
|
|
63
|
+
const e = l.He;
|
|
64
|
+
const n = l;
|
|
65
|
+
// Heap removal must not be gated on `_deps`: a dependency-free
|
|
66
|
+
// computation queued by refresh() has a null dep list but still sits in
|
|
67
|
+
// the dirty heap, and left there the post-disposal flush recomputes it —
|
|
68
|
+
// recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
|
|
69
|
+
// comes back to life (post-unmount runs, leaked cleanups, #2983).
|
|
70
|
+
if (n.se & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT)) deleteFromHeap(n, queueFor(n));
|
|
71
|
+
if (n.tt) {
|
|
72
|
+
let e = n.tt;
|
|
73
|
+
do {
|
|
74
|
+
e = unlinkSubs(e);
|
|
75
|
+
} while (e !== null);
|
|
76
|
+
n.tt = null;
|
|
77
|
+
n.Ye = null;
|
|
78
|
+
}
|
|
79
|
+
disposeChildren(l, true);
|
|
80
|
+
l = e;
|
|
81
|
+
}
|
|
82
|
+
if (t) {
|
|
83
|
+
e.We = null;
|
|
84
|
+
} else {
|
|
85
|
+
e.ke = null;
|
|
86
|
+
e.qe = 0;
|
|
87
|
+
}
|
|
88
|
+
// O(1) splice out of parent's chain on individual dispose. Skipped during
|
|
89
|
+
// batch dispose (parent already disposed) and zombie disposal (node sits on
|
|
90
|
+
// parent's _pendingFirstChild). We leave node._nextSibling intact so outer
|
|
91
|
+
// walks that already advanced past us still reach later siblings.
|
|
92
|
+
if (n && !t && !(i & REACTIVE_ZOMBIE) && e.Fe !== null && !(e.Fe.se & REACTIVE_DISPOSED)) {
|
|
93
|
+
const n = e.ct;
|
|
94
|
+
const t = e.He;
|
|
95
|
+
if (n !== null) n.He = t; else e.Fe.ke = t;
|
|
96
|
+
if (t !== null) t.ct = n;
|
|
97
|
+
e.ct = null;
|
|
98
|
+
}
|
|
99
|
+
runDisposal(e, t);
|
|
100
|
+
// Final effect-returned cleanup fires at true disposal, after `_disposal`
|
|
101
|
+
// to mirror rerun ordering (compute-phase teardown first, cleanup last).
|
|
102
|
+
if (n && e.Nt) {
|
|
103
|
+
const n = e.Nt;
|
|
104
|
+
e.Nt = undefined;
|
|
105
|
+
n();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function runDisposal(e, n) {
|
|
110
|
+
let t = n ? e.Qe : e.he;
|
|
111
|
+
if (!t) return;
|
|
112
|
+
if (Array.isArray(t)) {
|
|
113
|
+
for (let e = 0; e < t.length; e++) {
|
|
114
|
+
const n = t[e];
|
|
115
|
+
n.call(n);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
t.call(t);
|
|
119
|
+
}
|
|
120
|
+
n ? e.Qe = null : e.he = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function childId(e, n) {
|
|
124
|
+
let t = e;
|
|
125
|
+
while (t.T & CONFIG_TRANSPARENT && t.Fe) t = t.Fe;
|
|
126
|
+
if (t.id != null) return formatId(t.id, n ? t.qe++ : t.qe);
|
|
127
|
+
throw new Error("");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Allocates and returns the next stable child id for `owner`. Used by
|
|
132
|
+
* hydration plumbing and `createUniqueId`. Not part of the user-facing API.
|
|
133
|
+
*
|
|
134
|
+
* @internal
|
|
135
|
+
*/ function getNextChildId(e) {
|
|
136
|
+
return childId(e, true);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The id a freshly-created node inherits: an explicit `options.id` wins;
|
|
141
|
+
* transparent nodes share their parent's id; otherwise the parent's next
|
|
142
|
+
* child id is consumed (or `undefined` outside an id-carrying tree).
|
|
143
|
+
*/ function inheritId(e, n, t) {
|
|
144
|
+
return e?.id ?? (n ? t?.id : t?.id != null ? getNextChildId(t) : undefined);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns the *next* child id for `owner` without consuming it. Used by
|
|
149
|
+
* hydration plumbing to peek at the id a future child will receive.
|
|
150
|
+
*
|
|
151
|
+
* @internal
|
|
152
|
+
*/ function peekNextChildId(e) {
|
|
153
|
+
return childId(e, false);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatId(e, n) {
|
|
157
|
+
const t = n.toString(36), i = t.length - 1;
|
|
158
|
+
return e + (i ? String.fromCharCode(64 + i) : "") + t;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns the currently-tracking observer (the computation that subscribes to
|
|
163
|
+
* reactive reads at this point), or `null` if reads here would be untracked.
|
|
164
|
+
* Used by reactive primitives that need to know whether they're inside a
|
|
165
|
+
* tracking scope. App code rarely needs this — see `getOwner()` for the
|
|
166
|
+
* lifecycle owner instead.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* // Library predicate: only register a hot-path subscription when the
|
|
171
|
+
* // caller is inside a tracking scope (memo / effect compute / JSX).
|
|
172
|
+
* function trackIfTracked(source: () => unknown) {
|
|
173
|
+
* if (getObserver()) source();
|
|
174
|
+
* }
|
|
175
|
+
* ```
|
|
176
|
+
*/ function getObserver() {
|
|
177
|
+
if (pendingCheckActive || latestReadActive) return PENDING_OWNER;
|
|
178
|
+
return tracking ? context : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns the current reactive **owner** — the lifecycle node that the next
|
|
183
|
+
* `cleanup()` / `onCleanup()` / `createSignal()` etc. will be attached to.
|
|
184
|
+
*
|
|
185
|
+
* Returns `null` if called outside any owner. Capture the owner with
|
|
186
|
+
* `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach
|
|
187
|
+
* disposables created from a callback (event handler, async resolution, etc.)
|
|
188
|
+
* back to a component's lifecycle.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```ts
|
|
192
|
+
* function defer<T>(fn: () => T) {
|
|
193
|
+
* const owner = getOwner();
|
|
194
|
+
* queueMicrotask(() => runWithOwner(owner, fn));
|
|
195
|
+
* }
|
|
196
|
+
* ```
|
|
197
|
+
*/ function getOwner() {
|
|
198
|
+
return context;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Low-level: registers `fn` as a disposal callback on the current owner.
|
|
203
|
+
* Most code should use `onCleanup()` from `solid-js`, which adds dev-mode
|
|
204
|
+
* checks. `cleanup()` is the unchecked primitive used by internals.
|
|
205
|
+
*/ function cleanup(e) {
|
|
206
|
+
if (!context) return e;
|
|
207
|
+
if (!context.he) context.he = e; else if (Array.isArray(context.he)) context.he.push(e); else context.he = [ context.he, e ];
|
|
208
|
+
return e;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Returns `true` if the owner has been disposed (or marked zombie pending
|
|
213
|
+
* disposal). Pair with a captured owner to bail out of late callbacks whose
|
|
214
|
+
* surrounding component already unmounted.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* function onSettleSafe(fn: () => void) {
|
|
219
|
+
* const owner = getOwner();
|
|
220
|
+
* queueMicrotask(() => {
|
|
221
|
+
* if (owner && isDisposed(owner)) return; // component unmounted; skip
|
|
222
|
+
* runWithOwner(owner, fn);
|
|
223
|
+
* });
|
|
224
|
+
* }
|
|
225
|
+
* ```
|
|
226
|
+
*/ function isDisposed(e) {
|
|
227
|
+
return !!(e.se & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function disposeRootSelf(e = true) {
|
|
231
|
+
disposeChildren(this, e);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Creates a fresh owner attached as a child of the current owner (or as a
|
|
236
|
+
* detached root if there is none). Used by framework internals to group
|
|
237
|
+
* cleanups; app code should use `createRoot()` (host a reactive scope outside
|
|
238
|
+
* a component) or `runWithOwner()` (re-enter a captured owner).
|
|
239
|
+
*
|
|
240
|
+
* @internal
|
|
241
|
+
*/ function createOwner(e) {
|
|
242
|
+
const n = context;
|
|
243
|
+
const t = e?.transparent ?? false;
|
|
244
|
+
const i = {
|
|
245
|
+
id: inheritId(e, t, n),
|
|
246
|
+
T: t ? CONFIG_TRANSPARENT : 0,
|
|
247
|
+
Tt: true,
|
|
248
|
+
It: n?.Tt ? n.It : n,
|
|
249
|
+
ke: null,
|
|
250
|
+
He: null,
|
|
251
|
+
ct: null,
|
|
252
|
+
he: null,
|
|
253
|
+
C: n?.C ?? globalQueue,
|
|
254
|
+
we: n?.we || defaultContext,
|
|
255
|
+
qe: 0,
|
|
256
|
+
Qe: null,
|
|
257
|
+
We: null,
|
|
258
|
+
Fe: n,
|
|
259
|
+
dispose: disposeRootSelf
|
|
260
|
+
};
|
|
261
|
+
if (n) {
|
|
262
|
+
const e = n.ke;
|
|
263
|
+
if (e === null) {
|
|
264
|
+
n.ke = i;
|
|
265
|
+
} else {
|
|
266
|
+
i.He = e;
|
|
267
|
+
e.ct = i;
|
|
268
|
+
n.ke = i;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return i;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Creates a detached reactive root. The callback receives a `dispose()`
|
|
276
|
+
* function which, when called, tears down every signal, memo, effect, and
|
|
277
|
+
* `onCleanup` registered inside the root.
|
|
278
|
+
*
|
|
279
|
+
* Use this to host long-lived reactive scopes outside of a component (custom
|
|
280
|
+
* controllers, app bootstrapping, tests). Inside a component, prefer
|
|
281
|
+
* letting Solid's component lifecycle own things.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* const dispose = createRoot(dispose => {
|
|
286
|
+
* const [n, setN] = createSignal(0);
|
|
287
|
+
* createEffect(() => n(), value => console.log(value));
|
|
288
|
+
* setInterval(() => setN(x => x + 1), 1000);
|
|
289
|
+
* return dispose;
|
|
290
|
+
* });
|
|
291
|
+
*
|
|
292
|
+
* // Later, to tear everything down:
|
|
293
|
+
* dispose();
|
|
294
|
+
* ```
|
|
295
|
+
*
|
|
296
|
+
* @description https://docs.solidjs.com/reference/reactive-utilities/create-root
|
|
297
|
+
*/ function createRoot(e, n) {
|
|
298
|
+
const t = createOwner(n);
|
|
299
|
+
return runWithOwner(t, () => e(() => t.dispose()));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { cleanup, createOwner, createRoot, dispose, disposeChildren, getNextChildId, getObserver, getOwner, inheritId, isDisposed, markDisposal, peekNextChildId };
|