bireactive 0.2.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/animation/anim.d.ts +57 -0
- package/dist/animation/anim.js +318 -0
- package/dist/animation/combinators.d.ts +39 -0
- package/dist/animation/combinators.js +113 -0
- package/dist/animation/easings.d.ts +5 -0
- package/dist/animation/easings.js +5 -0
- package/dist/animation/index.d.ts +3 -0
- package/dist/animation/index.js +3 -0
- package/dist/assert/algebra.d.ts +20 -0
- package/dist/assert/algebra.js +79 -0
- package/dist/assert/claim.d.ts +40 -0
- package/dist/assert/claim.js +129 -0
- package/dist/assert/index.d.ts +7 -0
- package/dist/assert/index.js +19 -0
- package/dist/assert/predicates.d.ts +18 -0
- package/dist/assert/predicates.js +43 -0
- package/dist/assert/record.d.ts +20 -0
- package/dist/assert/record.js +78 -0
- package/dist/assert/scope.d.ts +42 -0
- package/dist/assert/scope.js +233 -0
- package/dist/assert/span.d.ts +37 -0
- package/dist/assert/span.js +68 -0
- package/dist/assert/tree.d.ts +22 -0
- package/dist/assert/tree.js +65 -0
- package/dist/code/code.d.ts +70 -0
- package/dist/code/code.js +361 -0
- package/dist/code/index.d.ts +2 -0
- package/dist/code/index.js +9 -0
- package/dist/code/morph.d.ts +5 -0
- package/dist/code/morph.js +194 -0
- package/dist/code/tokenize.d.ts +8 -0
- package/dist/code/tokenize.js +51 -0
- package/dist/constraints/cluster.d.ts +83 -0
- package/dist/constraints/cluster.js +213 -0
- package/dist/constraints/drivers.d.ts +15 -0
- package/dist/constraints/drivers.js +40 -0
- package/dist/constraints/factories.d.ts +73 -0
- package/dist/constraints/factories.js +248 -0
- package/dist/constraints/index.d.ts +11 -0
- package/dist/constraints/index.js +39 -0
- package/dist/constraints/interaction.d.ts +21 -0
- package/dist/constraints/interaction.js +148 -0
- package/dist/constraints/linalg.d.ts +18 -0
- package/dist/constraints/linalg.js +141 -0
- package/dist/constraints/phases.d.ts +21 -0
- package/dist/constraints/phases.js +60 -0
- package/dist/constraints/physics.d.ts +34 -0
- package/dist/constraints/physics.js +128 -0
- package/dist/constraints/rigid.d.ts +210 -0
- package/dist/constraints/rigid.js +835 -0
- package/dist/constraints/solver.d.ts +107 -0
- package/dist/constraints/solver.js +510 -0
- package/dist/constraints/term.d.ts +50 -0
- package/dist/constraints/term.js +80 -0
- package/dist/constraints/terms.d.ts +80 -0
- package/dist/constraints/terms.js +302 -0
- package/dist/constraints/world.d.ts +31 -0
- package/dist/constraints/world.js +245 -0
- package/dist/core/aggregates.d.ts +64 -0
- package/dist/core/aggregates.js +198 -0
- package/dist/core/anim.d.ts +84 -0
- package/dist/core/anim.js +301 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +38 -0
- package/dist/core/introspect.d.ts +5 -0
- package/dist/core/introspect.js +31 -0
- package/dist/core/lenses/closed-form-policies.d.ts +64 -0
- package/dist/core/lenses/closed-form-policies.js +452 -0
- package/dist/core/lenses/domain-aggregates.d.ts +54 -0
- package/dist/core/lenses/domain-aggregates.js +259 -0
- package/dist/core/lenses/factor-lens.d.ts +42 -0
- package/dist/core/lenses/factor-lens.js +419 -0
- package/dist/core/lenses/index.d.ts +5 -0
- package/dist/core/lenses/index.js +16 -0
- package/dist/core/lenses/memory.d.ts +47 -0
- package/dist/core/lenses/memory.js +102 -0
- package/dist/core/lenses/typed-factor.d.ts +45 -0
- package/dist/core/lenses/typed-factor.js +376 -0
- package/dist/core/network-utils.d.ts +14 -0
- package/dist/core/network-utils.js +62 -0
- package/dist/core/new-primitives.d.ts +33 -0
- package/dist/core/new-primitives.js +113 -0
- package/dist/core/signal.d.ts +254 -0
- package/dist/core/signal.js +1349 -0
- package/dist/core/traits.d.ts +61 -0
- package/dist/core/traits.js +56 -0
- package/dist/core/tree.d.ts +23 -0
- package/dist/core/tree.js +62 -0
- package/dist/core/values/anchor.d.ts +23 -0
- package/dist/core/values/anchor.js +23 -0
- package/dist/core/values/audio.d.ts +33 -0
- package/dist/core/values/audio.js +107 -0
- package/dist/core/values/bool.d.ts +37 -0
- package/dist/core/values/bool.js +75 -0
- package/dist/core/values/box.d.ts +77 -0
- package/dist/core/values/box.js +211 -0
- package/dist/core/values/canvas.d.ts +71 -0
- package/dist/core/values/canvas.js +495 -0
- package/dist/core/values/color.d.ts +49 -0
- package/dist/core/values/color.js +106 -0
- package/dist/core/values/flags.d.ts +18 -0
- package/dist/core/values/flags.js +50 -0
- package/dist/core/values/gpu.d.ts +74 -0
- package/dist/core/values/gpu.js +426 -0
- package/dist/core/values/matrix.d.ts +53 -0
- package/dist/core/values/matrix.js +140 -0
- package/dist/core/values/num.d.ts +62 -0
- package/dist/core/values/num.js +166 -0
- package/dist/core/values/pose.d.ts +31 -0
- package/dist/core/values/pose.js +83 -0
- package/dist/core/values/range.d.ts +83 -0
- package/dist/core/values/range.js +167 -0
- package/dist/core/values/str.d.ts +76 -0
- package/dist/core/values/str.js +346 -0
- package/dist/core/values/template.d.ts +49 -0
- package/dist/core/values/template.js +148 -0
- package/dist/core/values/transform.d.ts +49 -0
- package/dist/core/values/transform.js +115 -0
- package/dist/core/values/tri.d.ts +31 -0
- package/dist/core/values/tri.js +95 -0
- package/dist/core/values/vec.d.ts +72 -0
- package/dist/core/values/vec.js +219 -0
- package/dist/core/writable.d.ts +15 -0
- package/dist/core/writable.js +29 -0
- package/dist/ext/events.d.ts +10 -0
- package/dist/ext/events.js +31 -0
- package/dist/ext/index.d.ts +4 -0
- package/dist/ext/index.js +4 -0
- package/dist/ext/snapshot.d.ts +8 -0
- package/dist/ext/snapshot.js +29 -0
- package/dist/ext/timeline.d.ts +56 -0
- package/dist/ext/timeline.js +94 -0
- package/dist/ext/waapi.d.ts +25 -0
- package/dist/ext/waapi.js +198 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/propagators/index.d.ts +6 -0
- package/dist/propagators/index.js +6 -0
- package/dist/propagators/layout.d.ts +68 -0
- package/dist/propagators/layout.js +336 -0
- package/dist/propagators/network.d.ts +52 -0
- package/dist/propagators/network.js +185 -0
- package/dist/propagators/propagator.d.ts +12 -0
- package/dist/propagators/propagator.js +16 -0
- package/dist/propagators/range.d.ts +45 -0
- package/dist/propagators/range.js +147 -0
- package/dist/propagators/relations.d.ts +60 -0
- package/dist/propagators/relations.js +343 -0
- package/dist/shapes/annular-sector.d.ts +15 -0
- package/dist/shapes/annular-sector.js +64 -0
- package/dist/shapes/button.d.ts +14 -0
- package/dist/shapes/button.js +31 -0
- package/dist/shapes/choreographers.d.ts +22 -0
- package/dist/shapes/choreographers.js +69 -0
- package/dist/shapes/circle.d.ts +17 -0
- package/dist/shapes/circle.js +57 -0
- package/dist/shapes/clip.d.ts +5 -0
- package/dist/shapes/clip.js +31 -0
- package/dist/shapes/connect.d.ts +16 -0
- package/dist/shapes/connect.js +70 -0
- package/dist/shapes/curve.d.ts +60 -0
- package/dist/shapes/curve.js +285 -0
- package/dist/shapes/dashed.d.ts +16 -0
- package/dist/shapes/dashed.js +142 -0
- package/dist/shapes/debug.d.ts +43 -0
- package/dist/shapes/debug.js +97 -0
- package/dist/shapes/group.d.ts +5 -0
- package/dist/shapes/group.js +10 -0
- package/dist/shapes/handle.d.ts +32 -0
- package/dist/shapes/handle.js +88 -0
- package/dist/shapes/index.d.ts +23 -0
- package/dist/shapes/index.js +23 -0
- package/dist/shapes/interaction.d.ts +32 -0
- package/dist/shapes/interaction.js +187 -0
- package/dist/shapes/label.d.ts +20 -0
- package/dist/shapes/label.js +42 -0
- package/dist/shapes/layout.d.ts +29 -0
- package/dist/shapes/layout.js +74 -0
- package/dist/shapes/line.d.ts +21 -0
- package/dist/shapes/line.js +79 -0
- package/dist/shapes/list.d.ts +18 -0
- package/dist/shapes/list.js +51 -0
- package/dist/shapes/mount.d.ts +7 -0
- package/dist/shapes/mount.js +10 -0
- package/dist/shapes/path.d.ts +77 -0
- package/dist/shapes/path.js +227 -0
- package/dist/shapes/rect.d.ts +30 -0
- package/dist/shapes/rect.js +131 -0
- package/dist/shapes/shape.d.ts +132 -0
- package/dist/shapes/shape.js +306 -0
- package/dist/shapes/text.d.ts +24 -0
- package/dist/shapes/text.js +53 -0
- package/dist/shapes/tokens.d.ts +28 -0
- package/dist/shapes/tokens.js +27 -0
- package/dist/shapes/transitions.d.ts +23 -0
- package/dist/shapes/transitions.js +62 -0
- package/dist/tex/decorations.d.ts +26 -0
- package/dist/tex/decorations.js +116 -0
- package/dist/tex/index.d.ts +5 -0
- package/dist/tex/index.js +5 -0
- package/dist/tex/marker.d.ts +17 -0
- package/dist/tex/marker.js +63 -0
- package/dist/tex/motion.d.ts +43 -0
- package/dist/tex/motion.js +290 -0
- package/dist/tex/parts.d.ts +65 -0
- package/dist/tex/parts.js +149 -0
- package/dist/tex/tex.d.ts +45 -0
- package/dist/tex/tex.js +244 -0
- package/dist/web/attr.d.ts +16 -0
- package/dist/web/attr.js +98 -0
- package/dist/web/diagram.d.ts +49 -0
- package/dist/web/diagram.js +260 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +6 -0
- package/dist/web/md-marker.d.ts +6 -0
- package/dist/web/md-marker.js +39 -0
- package/dist/web/md-tex.d.ts +6 -0
- package/dist/web/md-tex.js +61 -0
- package/dist/web/raf.d.ts +6 -0
- package/dist/web/raf.js +24 -0
- package/dist/web/viewport.d.ts +7 -0
- package/dist/web/viewport.js +13 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
// signal.ts — symmetric bidirectional reactive engine.
|
|
2
|
+
//
|
|
3
|
+
// Forward propagation is alien-signals verbatim (link/propagate/
|
|
4
|
+
// checkDirty/shallowPropagate, Dirty/Pending/Recursed flags, lazy pull).
|
|
5
|
+
// Backward is not a second engine: a write "compiles" a view-edit into
|
|
6
|
+
// source-edits by walking up `_bwdParent`, applying each lens's `put` to
|
|
7
|
+
// compute what the source(s) must become, committing via the SAME
|
|
8
|
+
// forward write path. So views are never sticky (a view is always
|
|
9
|
+
// `get(source)`; lossy lenses snap), no-op deltas short-circuit for free
|
|
10
|
+
// via equality, and backward cost ≤ forward cost.
|
|
11
|
+
//
|
|
12
|
+
// Duals:
|
|
13
|
+
// * merge — N→1 backward aggregation (dual of computed's N→1 forward).
|
|
14
|
+
// Contributions land in a slot map keyed by contributor identity,
|
|
15
|
+
// fold via a user policy, reset per settle.
|
|
16
|
+
// * multi-parent lens — a write that SPLITS across N parents
|
|
17
|
+
// (`_put(target)` → per-parent update array, `propagateSplit`); the
|
|
18
|
+
// dual of a getter reading N parents. Covers coupled writables
|
|
19
|
+
// (N→M, e.g. mean/diff). Info the source can't hold lives in a
|
|
20
|
+
// stateful-lens complement, not a bespoke engine kind.
|
|
21
|
+
//
|
|
22
|
+
// Core asymmetry: forward deps are IMPLICIT (auto-tracked reads of
|
|
23
|
+
// `.value` under `activeSub`); backward targets are EXPLICIT (declared
|
|
24
|
+
// at construction in `_bwdParent`). Hence no `activeBwdWrite` global.
|
|
25
|
+
//
|
|
26
|
+
// Mode table — a cell's role is fully determined by which fields are set:
|
|
27
|
+
// source getter undefined (truth in currentValue)
|
|
28
|
+
// computed getter, no _bwd (read-only derived)
|
|
29
|
+
// lens 1→1 getter + _bwd{ put, parent: Cell }
|
|
30
|
+
// multi-out getter + _bwd{ put, parent: Cell[] } (1→N / N→M bwd)
|
|
31
|
+
// merge getter + _bwd{ merge } (N→1 backward fold)
|
|
32
|
+
// stateful getter + _bwd{ put, parent, stateful } (complement-carrying)
|
|
33
|
+
// A cell is writable iff `_bwd !== undefined` (the backward sidecar; see
|
|
34
|
+
// `BwdSpec`). `pendingValue` is dual-keyed: a staged forward write for a
|
|
35
|
+
// source, a deferred backward target for a getter cell (never both).
|
|
36
|
+
//
|
|
37
|
+
// Batching: outside a batch a write propagates backward eagerly and
|
|
38
|
+
// flushes (alien's synchronous per-write semantics). Inside batch/flush,
|
|
39
|
+
// lens writes deposit their latest value and queue (last-write-wins via
|
|
40
|
+
// `_queueIdx`) and merge folds defer until all contributors land; the
|
|
41
|
+
// flush loop alternates bwd-drain / effect-drain to a fixpoint.
|
|
42
|
+
// Flag bits (alien-signals v2).
|
|
43
|
+
const F = {
|
|
44
|
+
None: 0,
|
|
45
|
+
Mutable: 1,
|
|
46
|
+
Watching: 2,
|
|
47
|
+
RecursedCheck: 4,
|
|
48
|
+
Recursed: 8,
|
|
49
|
+
Dirty: 16,
|
|
50
|
+
Pending: 32,
|
|
51
|
+
/** Backward-only: cell has a pending backward contribution queued. */
|
|
52
|
+
BwdQueued: 64,
|
|
53
|
+
};
|
|
54
|
+
let cycle = 0;
|
|
55
|
+
let runDepth = 0;
|
|
56
|
+
let batchDepth = 0;
|
|
57
|
+
let notifyIndex = 0;
|
|
58
|
+
let queuedLength = 0;
|
|
59
|
+
let activeSub;
|
|
60
|
+
let flushing = false;
|
|
61
|
+
/** Network running its body, if any. Source writes self-exclude it so a
|
|
62
|
+
* network reading+writing a cell doesn't re-trigger itself. */
|
|
63
|
+
let activeNetwork;
|
|
64
|
+
const queued = [];
|
|
65
|
+
const EMPTY_DIRTY = new Set();
|
|
66
|
+
/** Backward worklist: lens cells with deferred writes, merge cells
|
|
67
|
+
* awaiting fold. Drained to a fixpoint with effects by flush. */
|
|
68
|
+
const bwdQueue = [];
|
|
69
|
+
// Fires on every SOURCE value-change (the one place truth mutates).
|
|
70
|
+
// Backward writes reach it via `_writeSource`, attributing lens edits to
|
|
71
|
+
// the source they resolve to.
|
|
72
|
+
let writeHook;
|
|
73
|
+
/** Install a hook fired on every source value-change; returns a restore fn. */
|
|
74
|
+
export function setCellWriteHook(fn) {
|
|
75
|
+
const prev = writeHook;
|
|
76
|
+
writeHook = fn;
|
|
77
|
+
return () => {
|
|
78
|
+
writeHook = prev;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// alien-signals algorithm (verbatim): link / unlink / propagate / checkDirty.
|
|
82
|
+
function link(dep, sub, version) {
|
|
83
|
+
const prevDep = sub.depsTail;
|
|
84
|
+
if (prevDep !== undefined && prevDep.dep === dep)
|
|
85
|
+
return;
|
|
86
|
+
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps;
|
|
87
|
+
if (nextDep !== undefined && nextDep.dep === dep) {
|
|
88
|
+
nextDep.version = version;
|
|
89
|
+
sub.depsTail = nextDep;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const prevSub = dep.subsTail;
|
|
93
|
+
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub)
|
|
94
|
+
return;
|
|
95
|
+
const isFirstSub = dep.subs === undefined;
|
|
96
|
+
const newLink = (sub.depsTail =
|
|
97
|
+
dep.subsTail =
|
|
98
|
+
{
|
|
99
|
+
version,
|
|
100
|
+
dep,
|
|
101
|
+
sub,
|
|
102
|
+
prevDep,
|
|
103
|
+
nextDep,
|
|
104
|
+
prevSub,
|
|
105
|
+
nextSub: undefined,
|
|
106
|
+
});
|
|
107
|
+
if (nextDep !== undefined)
|
|
108
|
+
nextDep.prevDep = newLink;
|
|
109
|
+
if (prevDep !== undefined)
|
|
110
|
+
prevDep.nextDep = newLink;
|
|
111
|
+
else
|
|
112
|
+
sub.deps = newLink;
|
|
113
|
+
if (prevSub !== undefined)
|
|
114
|
+
prevSub.nextSub = newLink;
|
|
115
|
+
else
|
|
116
|
+
dep.subs = newLink;
|
|
117
|
+
// First-subscriber lifecycle hook (dual: last-sub in `_unwatched`).
|
|
118
|
+
if (isFirstSub && dep instanceof Cell) {
|
|
119
|
+
const hook = dep._watched;
|
|
120
|
+
if (hook !== undefined)
|
|
121
|
+
hook.call(dep);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function unlink(l, sub = l.sub) {
|
|
125
|
+
const { dep, prevDep, nextDep, nextSub, prevSub } = l;
|
|
126
|
+
if (nextDep !== undefined)
|
|
127
|
+
nextDep.prevDep = prevDep;
|
|
128
|
+
else
|
|
129
|
+
sub.depsTail = prevDep;
|
|
130
|
+
if (prevDep !== undefined)
|
|
131
|
+
prevDep.nextDep = nextDep;
|
|
132
|
+
else
|
|
133
|
+
sub.deps = nextDep;
|
|
134
|
+
if (nextSub !== undefined)
|
|
135
|
+
nextSub.prevSub = prevSub;
|
|
136
|
+
else
|
|
137
|
+
dep.subsTail = prevSub;
|
|
138
|
+
if (prevSub !== undefined)
|
|
139
|
+
prevSub.nextSub = nextSub;
|
|
140
|
+
else if ((dep.subs = nextSub) === undefined)
|
|
141
|
+
dep._unwatched();
|
|
142
|
+
return nextDep;
|
|
143
|
+
}
|
|
144
|
+
function propagate(start, innerWrite, excluding) {
|
|
145
|
+
let l = start;
|
|
146
|
+
let next = start.nextSub;
|
|
147
|
+
let stack;
|
|
148
|
+
top: do {
|
|
149
|
+
const sub = l.sub;
|
|
150
|
+
// `excluding` skips one subscriber (used by `network()` so a body
|
|
151
|
+
// writing a cell it subscribes to doesn't re-trigger itself).
|
|
152
|
+
if (sub !== excluding) {
|
|
153
|
+
let flags = sub.flags;
|
|
154
|
+
if (!(flags & (F.RecursedCheck | F.Recursed | F.Dirty | F.Pending))) {
|
|
155
|
+
sub.flags = flags | F.Pending;
|
|
156
|
+
if (innerWrite)
|
|
157
|
+
sub.flags |= F.Recursed;
|
|
158
|
+
}
|
|
159
|
+
else if (!(flags & (F.RecursedCheck | F.Recursed))) {
|
|
160
|
+
flags = F.None;
|
|
161
|
+
}
|
|
162
|
+
else if (!(flags & F.RecursedCheck)) {
|
|
163
|
+
sub.flags = (flags & ~F.Recursed) | F.Pending;
|
|
164
|
+
}
|
|
165
|
+
else if (!(flags & (F.Dirty | F.Pending)) && isValidLink(l, sub)) {
|
|
166
|
+
sub.flags = flags | (F.Recursed | F.Pending);
|
|
167
|
+
flags &= F.Mutable;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
flags = F.None;
|
|
171
|
+
}
|
|
172
|
+
if (flags & F.Watching)
|
|
173
|
+
sub._notify();
|
|
174
|
+
if (flags & F.Mutable) {
|
|
175
|
+
const subSubs = sub.subs;
|
|
176
|
+
if (subSubs !== undefined) {
|
|
177
|
+
const nextSub = (l = subSubs).nextSub;
|
|
178
|
+
if (nextSub !== undefined) {
|
|
179
|
+
stack = { value: next, prev: stack };
|
|
180
|
+
next = nextSub;
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if ((l = next) !== undefined) {
|
|
187
|
+
next = l.nextSub;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
while (stack !== undefined) {
|
|
191
|
+
l = stack.value;
|
|
192
|
+
stack = stack.prev;
|
|
193
|
+
if (l !== undefined) {
|
|
194
|
+
next = l.nextSub;
|
|
195
|
+
continue top;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
} while (true);
|
|
200
|
+
}
|
|
201
|
+
function checkDirty(startLink, startSub) {
|
|
202
|
+
let l = startLink, sub = startSub;
|
|
203
|
+
let stack;
|
|
204
|
+
let checkDepth = 0, dirty = false;
|
|
205
|
+
top: do {
|
|
206
|
+
const dep = l.dep;
|
|
207
|
+
const flags = dep.flags;
|
|
208
|
+
if (sub.flags & F.Dirty)
|
|
209
|
+
dirty = true;
|
|
210
|
+
else if ((flags & (F.Mutable | F.Dirty)) === (F.Mutable | F.Dirty)) {
|
|
211
|
+
const subs = dep.subs;
|
|
212
|
+
if (dep._update()) {
|
|
213
|
+
if (subs.nextSub !== undefined)
|
|
214
|
+
shallowPropagate(subs);
|
|
215
|
+
dirty = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if ((flags & (F.Mutable | F.Pending)) === (F.Mutable | F.Pending)) {
|
|
219
|
+
stack = { value: l, prev: stack };
|
|
220
|
+
l = dep.deps;
|
|
221
|
+
sub = dep;
|
|
222
|
+
++checkDepth;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!dirty) {
|
|
226
|
+
const nextDep = l.nextDep;
|
|
227
|
+
if (nextDep !== undefined) {
|
|
228
|
+
l = nextDep;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
while (checkDepth--) {
|
|
233
|
+
l = stack.value;
|
|
234
|
+
stack = stack.prev;
|
|
235
|
+
if (dirty) {
|
|
236
|
+
const subs = sub.subs;
|
|
237
|
+
if (sub._update()) {
|
|
238
|
+
if (subs.nextSub !== undefined)
|
|
239
|
+
shallowPropagate(subs);
|
|
240
|
+
sub = l.sub;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
dirty = false;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
sub.flags &= ~F.Pending;
|
|
247
|
+
}
|
|
248
|
+
sub = l.sub;
|
|
249
|
+
const nextDep = l.nextDep;
|
|
250
|
+
if (nextDep !== undefined) {
|
|
251
|
+
l = nextDep;
|
|
252
|
+
continue top;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return dirty && !!sub.flags;
|
|
256
|
+
} while (true);
|
|
257
|
+
}
|
|
258
|
+
function shallowPropagate(l) {
|
|
259
|
+
do {
|
|
260
|
+
const sub = l.sub;
|
|
261
|
+
const flags = sub.flags;
|
|
262
|
+
if ((flags & (F.Pending | F.Dirty)) === F.Pending) {
|
|
263
|
+
sub.flags = flags | F.Dirty;
|
|
264
|
+
if ((flags & (F.Watching | F.RecursedCheck)) === F.Watching)
|
|
265
|
+
sub._notify();
|
|
266
|
+
}
|
|
267
|
+
} while ((l = l.nextSub) !== undefined);
|
|
268
|
+
}
|
|
269
|
+
function isValidLink(checkLink, sub) {
|
|
270
|
+
let l = sub.depsTail;
|
|
271
|
+
while (l !== undefined) {
|
|
272
|
+
if (l === checkLink)
|
|
273
|
+
return true;
|
|
274
|
+
l = l.prevDep;
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
function purgeDeps(sub) {
|
|
279
|
+
const depsTail = sub.depsTail;
|
|
280
|
+
let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps;
|
|
281
|
+
while (dep !== undefined)
|
|
282
|
+
dep = unlink(dep, sub);
|
|
283
|
+
}
|
|
284
|
+
function disposeAllDepsInReverse(sub) {
|
|
285
|
+
let l = sub.depsTail;
|
|
286
|
+
while (l !== undefined) {
|
|
287
|
+
const prev = l.prevDep;
|
|
288
|
+
unlink(l, sub);
|
|
289
|
+
l = prev;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export const DIRECT_SLOT = Symbol("merge:direct-slot");
|
|
293
|
+
class MergeNode {
|
|
294
|
+
parent;
|
|
295
|
+
policy;
|
|
296
|
+
slots = new Map();
|
|
297
|
+
hasIncrementalAcc;
|
|
298
|
+
acc;
|
|
299
|
+
constructor(parent, policy) {
|
|
300
|
+
this.parent = parent;
|
|
301
|
+
this.policy = policy;
|
|
302
|
+
this.hasIncrementalAcc = policy.remove !== undefined;
|
|
303
|
+
this.acc = policy.identity;
|
|
304
|
+
}
|
|
305
|
+
receive(slot, next) {
|
|
306
|
+
if (this.hasIncrementalAcc) {
|
|
307
|
+
const remove = this.policy.remove;
|
|
308
|
+
const prior = this.slots.get(slot);
|
|
309
|
+
if (prior === undefined)
|
|
310
|
+
this.acc = this.policy.combine(this.acc, next);
|
|
311
|
+
else
|
|
312
|
+
this.acc = this.policy.combine(remove(this.acc, prior), next);
|
|
313
|
+
}
|
|
314
|
+
this.slots.set(slot, next);
|
|
315
|
+
}
|
|
316
|
+
fold() {
|
|
317
|
+
if (this.hasIncrementalAcc)
|
|
318
|
+
return this.acc;
|
|
319
|
+
let acc = this.policy.identity;
|
|
320
|
+
for (const v of this.slots.values())
|
|
321
|
+
acc = this.policy.combine(acc, v);
|
|
322
|
+
return acc;
|
|
323
|
+
}
|
|
324
|
+
reset() {
|
|
325
|
+
this.slots.clear();
|
|
326
|
+
this.acc = this.policy.identity;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// BwdSpec — the backward sidecar.
|
|
330
|
+
//
|
|
331
|
+
// Every cell carries the forward fields (links, getter, value cache).
|
|
332
|
+
// Only a WRITABLE derived cell — 1→1 lens, multi-output lens, merge,
|
|
333
|
+
// stateful lens, or `pin` — needs a backward target and the closures to
|
|
334
|
+
// drive it. Those fields live here, off a single `_bwd` pointer, rather
|
|
335
|
+
// than inline on `Cell`, so a source/computed stays lean: the forward hot
|
|
336
|
+
// path never touches them, and a plain node drops ~64 B. A cell is
|
|
337
|
+
// writable iff `_bwd !== undefined`.
|
|
338
|
+
//
|
|
339
|
+
// Two mode payloads hang off named fields rather than one union, so each
|
|
340
|
+
// stays distinctly typed: `merge` (the N→1 fold node) and `stateful` (the
|
|
341
|
+
// complement machinery of a complement-carrying lens). Both are rare, so
|
|
342
|
+
// a plain 1→1 / multi-out lens leaves them `undefined` and pays only
|
|
343
|
+
// `parent` + `put` + `queueIdx`.
|
|
344
|
+
class BwdSpec {
|
|
345
|
+
/** Backward target(s): one `Cell` (1→1 / merge) or `Cell[]` (multi-out). */
|
|
346
|
+
parent = undefined;
|
|
347
|
+
/** Lens `put` — backward derivation (dual of `getter`). Always called by
|
|
348
|
+
* the engine in 1-arg form `put(target)`; a source-reading lens bakes
|
|
349
|
+
* `settled(parent)` into this closure at build time. Multi-output:
|
|
350
|
+
* returns a per-parent update array. Stateful: the spec's `bwd`. */
|
|
351
|
+
// biome-ignore lint/suspicious/noExplicitAny: put fn is opaque shape
|
|
352
|
+
put = undefined;
|
|
353
|
+
/** Backward aggregation node; presence IS the merge-mode discriminant. */
|
|
354
|
+
merge = undefined;
|
|
355
|
+
/** Complement machinery; presence IS the stateful-mode discriminant. */
|
|
356
|
+
stateful = undefined;
|
|
357
|
+
/** Index in `bwdQueue` of this cell's latest push; the drain skips stale
|
|
358
|
+
* entries so each cell propagates backward once per flush, last-write. */
|
|
359
|
+
queueIdx = -1;
|
|
360
|
+
}
|
|
361
|
+
/** Runtime state of a stateful (complement-carrying) lens — the rare
|
|
362
|
+
* backward mode, kept off `BwdSpec` so plain lenses don't carry its slots.
|
|
363
|
+
* `put` (the spec's `bwd`) and `parent` stay on `BwdSpec`; this holds the
|
|
364
|
+
* complement and the closures that project from / advance it. */
|
|
365
|
+
class StatefulCore {
|
|
366
|
+
/** Engine-owned memory the view discards. */
|
|
367
|
+
complement;
|
|
368
|
+
/** Forward projection `fwd(sources, complement) → view`. */
|
|
369
|
+
// biome-ignore lint/suspicious/noExplicitAny: opaque fwd shape
|
|
370
|
+
fwd;
|
|
371
|
+
/** Advance the complement: `step(sources, complement, external)`. */
|
|
372
|
+
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
373
|
+
step;
|
|
374
|
+
/** Source values last written back (own-vs-external test); `undefined`
|
|
375
|
+
* until the first back-write. */
|
|
376
|
+
lastBwd = undefined;
|
|
377
|
+
constructor(complement,
|
|
378
|
+
// biome-ignore lint/suspicious/noExplicitAny: opaque fwd shape
|
|
379
|
+
fwd,
|
|
380
|
+
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
381
|
+
step) {
|
|
382
|
+
this.complement = complement;
|
|
383
|
+
this.fwd = fwd;
|
|
384
|
+
this.step = step;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Snapshot a `Val<T>` to plain `T` (one-shot, no tracking). */
|
|
388
|
+
export function readNow(v) {
|
|
389
|
+
if (v instanceof Cell)
|
|
390
|
+
return v.value;
|
|
391
|
+
return v;
|
|
392
|
+
}
|
|
393
|
+
/** Resolve a `Val<T>` to a `() => T` closure that unwraps on each call. */
|
|
394
|
+
export function reader(v) {
|
|
395
|
+
if (v instanceof Cell)
|
|
396
|
+
return () => v.value;
|
|
397
|
+
return () => v;
|
|
398
|
+
}
|
|
399
|
+
/** Lazy getter: computes once, installs a non-enumerable own prop under
|
|
400
|
+
* `key` that shadows this getter on later reads. */
|
|
401
|
+
export function lazy(self, key, make) {
|
|
402
|
+
const v = make();
|
|
403
|
+
Object.defineProperty(self, key, {
|
|
404
|
+
value: v,
|
|
405
|
+
writable: false,
|
|
406
|
+
configurable: false,
|
|
407
|
+
enumerable: false,
|
|
408
|
+
});
|
|
409
|
+
return v;
|
|
410
|
+
}
|
|
411
|
+
export const isCell = (v) => v instanceof Cell;
|
|
412
|
+
/** Lens mode: a derived cell that can be written back (has a backward sidecar). */
|
|
413
|
+
export const isLens = (v) => v instanceof Cell && v.getter !== undefined && v._bwd !== undefined;
|
|
414
|
+
/** Computed mode: derived + read-only (no backward path). */
|
|
415
|
+
export const isComputed = (v) => v instanceof Cell && v.getter !== undefined && v._bwd === undefined;
|
|
416
|
+
export class Cell {
|
|
417
|
+
flags = F.Mutable;
|
|
418
|
+
subs;
|
|
419
|
+
subsTail;
|
|
420
|
+
deps;
|
|
421
|
+
depsTail;
|
|
422
|
+
/** Forward derivation (computed/lens/merge). `undefined` ⇒ source. */
|
|
423
|
+
getter;
|
|
424
|
+
/** Per-instance equality, always defined (defaults to `Object.is` at
|
|
425
|
+
* construction) so hot paths call it without an `undefined` branch. */
|
|
426
|
+
_equals;
|
|
427
|
+
/** First-subscriber / last-subscriber lifecycle hooks. */
|
|
428
|
+
_watched;
|
|
429
|
+
_unwatchedHook;
|
|
430
|
+
/** Source: `currentValue` = committed, `pendingValue` = staged write.
|
|
431
|
+
* Getter cell: `currentValue` = last derived cache, `pendingValue`
|
|
432
|
+
* reused as the deferred backward target (see `set value`). The two
|
|
433
|
+
* roles never coexist, so two fields suffice for four. */
|
|
434
|
+
currentValue;
|
|
435
|
+
pendingValue;
|
|
436
|
+
/** Backward sidecar: target(s) + lens closures + queue slot, or
|
|
437
|
+
* `undefined` for a read-only cell (source or computed). Allocated only
|
|
438
|
+
* for writable derived cells, keeping the common node lean. Writability
|
|
439
|
+
* is exactly `_bwd !== undefined`. See `BwdSpec`. */
|
|
440
|
+
_bwd;
|
|
441
|
+
constructor(initial, opts) {
|
|
442
|
+
this.currentValue = initial;
|
|
443
|
+
this.pendingValue = initial;
|
|
444
|
+
// Pre-init every optional slot for a stable V8 hidden class across variants.
|
|
445
|
+
this.subs = undefined;
|
|
446
|
+
this.subsTail = undefined;
|
|
447
|
+
this.deps = undefined;
|
|
448
|
+
this.depsTail = undefined;
|
|
449
|
+
this.getter = undefined;
|
|
450
|
+
this._equals = Object.is;
|
|
451
|
+
this._watched = undefined;
|
|
452
|
+
this._unwatchedHook = undefined;
|
|
453
|
+
this._bwd = undefined;
|
|
454
|
+
if (opts !== undefined) {
|
|
455
|
+
if (opts.equals !== undefined)
|
|
456
|
+
this._equals = opts.equals;
|
|
457
|
+
if (opts.watched !== undefined)
|
|
458
|
+
this._watched = opts.watched;
|
|
459
|
+
if (opts.unwatched !== undefined)
|
|
460
|
+
this._unwatchedHook = opts.unwatched;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
_enqueueBwd() {
|
|
464
|
+
this.flags |= F.BwdQueued;
|
|
465
|
+
this._bwd.queueIdx = bwdQueue.length;
|
|
466
|
+
bwdQueue.push(this);
|
|
467
|
+
}
|
|
468
|
+
/** Source write (alien's signal setter). Self-excludes the active
|
|
469
|
+
* network so a body writing its own dep doesn't re-trigger itself. */
|
|
470
|
+
_writeSource(next) {
|
|
471
|
+
const prev = this.pendingValue;
|
|
472
|
+
this.pendingValue = next;
|
|
473
|
+
if (!this._equals(prev, next)) {
|
|
474
|
+
this.flags = F.Mutable | F.Dirty;
|
|
475
|
+
if (writeHook !== undefined)
|
|
476
|
+
writeHook(this);
|
|
477
|
+
const subs = this.subs;
|
|
478
|
+
if (subs !== undefined)
|
|
479
|
+
propagate(subs, runDepth > 0, activeNetwork);
|
|
480
|
+
if (batchDepth === 0 && !flushing && subs !== undefined)
|
|
481
|
+
flush();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
_update() {
|
|
485
|
+
if (this.getter !== undefined) {
|
|
486
|
+
// Computed/lens/merge: re-run the forward derivation.
|
|
487
|
+
this.depsTail = undefined;
|
|
488
|
+
this.flags = F.Mutable | F.RecursedCheck;
|
|
489
|
+
const prev = activeSub;
|
|
490
|
+
activeSub = this;
|
|
491
|
+
let threw = true;
|
|
492
|
+
try {
|
|
493
|
+
++cycle;
|
|
494
|
+
const old = this.currentValue;
|
|
495
|
+
const next = (this.currentValue = this.getter());
|
|
496
|
+
threw = false;
|
|
497
|
+
return !this._equals(old, next);
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
activeSub = prev;
|
|
501
|
+
this.flags = threw ? F.Mutable | F.Dirty : this.flags & ~F.RecursedCheck;
|
|
502
|
+
purgeDeps(this);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
this.flags = F.Mutable;
|
|
506
|
+
const prevV = this.currentValue;
|
|
507
|
+
this.currentValue = this.pendingValue;
|
|
508
|
+
return !this._equals(prevV, this.currentValue);
|
|
509
|
+
}
|
|
510
|
+
_notify() { }
|
|
511
|
+
_unwatched() {
|
|
512
|
+
if (this.getter !== undefined && this.depsTail !== undefined) {
|
|
513
|
+
this.flags = F.Mutable | F.Dirty;
|
|
514
|
+
disposeAllDepsInReverse(this);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (this._unwatchedHook !== undefined)
|
|
518
|
+
this._unwatchedHook();
|
|
519
|
+
}
|
|
520
|
+
peek() {
|
|
521
|
+
const prev = activeSub;
|
|
522
|
+
activeSub = undefined;
|
|
523
|
+
try {
|
|
524
|
+
return this.value;
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
activeSub = prev;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** Guard: silent coercion to string/number is almost always a bug. */
|
|
531
|
+
[Symbol.toPrimitive](hint) {
|
|
532
|
+
throw new TypeError(`Cell cannot be coerced to ${hint} — use \`.value\``);
|
|
533
|
+
}
|
|
534
|
+
// Construction helpers build via `new this()` so a subclass static
|
|
535
|
+
// (`Vec.lens(...)`) yields a `Vec` with its constructor-set equality.
|
|
536
|
+
// Every lens has a structural backward target (`_bwd.parent`), which is
|
|
537
|
+
// what makes the backward pass well-defined.
|
|
538
|
+
/** Endomorphic lens. A 2-arg `bwd(view, current)` consults the current
|
|
539
|
+
* source; a 1-arg `bwd(view)` reconstructs it from the view alone. */
|
|
540
|
+
lens(fwd, bwd) {
|
|
541
|
+
return buildLens1(this.constructor, this, fwd, bwd, bwd.length >= 2);
|
|
542
|
+
}
|
|
543
|
+
/** Backward-aggregating node — bwd dual of computed. Forward, the
|
|
544
|
+
* identity view of its parent; backward, folds contributions from
|
|
545
|
+
* upstream lenses (slot-keyed) and direct writes (DIRECT_SLOT). */
|
|
546
|
+
merge(policy) {
|
|
547
|
+
if (this.getter !== undefined && this._bwd === undefined) {
|
|
548
|
+
throw new TypeError("merge: receiver is read-only");
|
|
549
|
+
}
|
|
550
|
+
const parent = this;
|
|
551
|
+
const cell = new this.constructor();
|
|
552
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
553
|
+
cell.getter = () => parent.value;
|
|
554
|
+
const b = (cell._bwd = new BwdSpec());
|
|
555
|
+
b.parent = parent;
|
|
556
|
+
b.merge = new MergeNode(parent, policy);
|
|
557
|
+
return cell;
|
|
558
|
+
}
|
|
559
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
560
|
+
static derive(...args) {
|
|
561
|
+
if (args.length === 1)
|
|
562
|
+
return buildComputed(this, args[0]);
|
|
563
|
+
const [parent, fn] = args;
|
|
564
|
+
if (Array.isArray(parent))
|
|
565
|
+
return buildLensN(this, parent, fn, undefined, false);
|
|
566
|
+
return buildComputed(this, () => fn(parent.value));
|
|
567
|
+
}
|
|
568
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
569
|
+
static lens(...args) {
|
|
570
|
+
const [parent, a, b] = args;
|
|
571
|
+
if (args.length === 2)
|
|
572
|
+
return buildStateful(this, Array.isArray(parent) ? parent : [parent], a);
|
|
573
|
+
const readsSource = b.length >= 2;
|
|
574
|
+
if (Array.isArray(parent))
|
|
575
|
+
return buildLensN(this, parent, a, b, readsSource);
|
|
576
|
+
return buildLens1(this, parent, a, b, readsSource);
|
|
577
|
+
}
|
|
578
|
+
/** Type predicate against this class: `Vec.is(x)` narrows `x` to `Vec`.
|
|
579
|
+
* Inherited static; works for any subclass via polymorphic `this`. */
|
|
580
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
581
|
+
static is(v) {
|
|
582
|
+
return v instanceof this;
|
|
583
|
+
}
|
|
584
|
+
/** Lift `Val<Inner<Cls>>` → `Cls`: instance → identity, RO cell →
|
|
585
|
+
* tracked `derive`, literal → fresh seed. */
|
|
586
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
587
|
+
static from(v) {
|
|
588
|
+
if (v instanceof this)
|
|
589
|
+
return v;
|
|
590
|
+
if (v instanceof Cell) {
|
|
591
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
592
|
+
return this.derive(() => readNow(v));
|
|
593
|
+
}
|
|
594
|
+
return new this(v);
|
|
595
|
+
}
|
|
596
|
+
/** Writable-shaped constant: always reads `v`, absorbs writes
|
|
597
|
+
* (parentless sink lens), for APIs demanding bidirectionality. */
|
|
598
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
599
|
+
static pin(v) {
|
|
600
|
+
const cell = new this();
|
|
601
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
602
|
+
cell.getter = () => v;
|
|
603
|
+
const b = (cell._bwd = new BwdSpec());
|
|
604
|
+
b.put = () => undefined; // absorb (no parent → sink)
|
|
605
|
+
return cell;
|
|
606
|
+
}
|
|
607
|
+
/** Typed field lens onto `parent.value[key]`. A read-only computed
|
|
608
|
+
* parent yields a RO derive view; any writable parent yields a
|
|
609
|
+
* bidirectional field lens with spread-replace `put`. */
|
|
610
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
611
|
+
static fieldOf(
|
|
612
|
+
// biome-ignore lint/suspicious/noExplicitAny: parent is contravariant on put
|
|
613
|
+
parent, key, Cls) {
|
|
614
|
+
const ctor = Cls;
|
|
615
|
+
const get = (s) => s[key];
|
|
616
|
+
// Read-only ⇔ computed: a getter with no backward sidecar.
|
|
617
|
+
const ro = parent.getter !== undefined && parent._bwd === undefined;
|
|
618
|
+
if (ro) {
|
|
619
|
+
return buildComputed(ctor, () => get(parent.value));
|
|
620
|
+
}
|
|
621
|
+
// Spread-replace reads the current source ⇒ source-reading (lens) form.
|
|
622
|
+
return buildLens1(ctor, parent, get, (v, s) => ({ ...s, [key]: v }), true);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
626
|
+
function buildComputed(Cls, getter) {
|
|
627
|
+
const cell = new Cls();
|
|
628
|
+
cell.getter = getter;
|
|
629
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
630
|
+
return cell;
|
|
631
|
+
}
|
|
632
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
633
|
+
function buildLens1(Cls, parent, fwd, bwd, readsSource) {
|
|
634
|
+
const cell = new Cls();
|
|
635
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
636
|
+
cell.getter = (() => fwd(parent.value));
|
|
637
|
+
const b = (cell._bwd = new BwdSpec());
|
|
638
|
+
// Source-reading lenses bake the (non-committing) current source into the
|
|
639
|
+
// closure so the engine always calls the 1-arg form (no arity branch).
|
|
640
|
+
b.put = readsSource ? (t) => bwd(t, settled(parent)) : bwd;
|
|
641
|
+
b.parent = parent;
|
|
642
|
+
return cell;
|
|
643
|
+
}
|
|
644
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
645
|
+
function buildLensN(Cls, parents, fwd, bwd, readsSource) {
|
|
646
|
+
const n = parents.length;
|
|
647
|
+
const vals = new Array(n);
|
|
648
|
+
const cell = new Cls();
|
|
649
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
650
|
+
cell.getter = (() => {
|
|
651
|
+
for (let i = 0; i < n; i++)
|
|
652
|
+
vals[i] = parents[i].value;
|
|
653
|
+
return fwd(vals);
|
|
654
|
+
});
|
|
655
|
+
if (bwd === undefined)
|
|
656
|
+
return cell; // read-only derive-N
|
|
657
|
+
const b = (cell._bwd = new BwdSpec());
|
|
658
|
+
b.parent = parents;
|
|
659
|
+
b.put = readsSource
|
|
660
|
+
? (target) => {
|
|
661
|
+
for (let i = 0; i < n; i++)
|
|
662
|
+
vals[i] = parents[i].peek();
|
|
663
|
+
return bwd(target, vals);
|
|
664
|
+
}
|
|
665
|
+
: (target) => bwd(target);
|
|
666
|
+
return cell;
|
|
667
|
+
}
|
|
668
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
669
|
+
function buildStateful(Cls, parents,
|
|
670
|
+
// biome-ignore lint/suspicious/noExplicitAny: opaque spec
|
|
671
|
+
spec) {
|
|
672
|
+
const n = parents.length;
|
|
673
|
+
const vals = new Array(n);
|
|
674
|
+
const cell = new Cls();
|
|
675
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
676
|
+
const b = (cell._bwd = new BwdSpec());
|
|
677
|
+
const seed = new Array(n);
|
|
678
|
+
for (let i = 0; i < n; i++)
|
|
679
|
+
seed[i] = parents[i].peek();
|
|
680
|
+
const sc = (b.stateful = new StatefulCore(spec.init(seed), spec.fwd, spec.step));
|
|
681
|
+
b.put = spec.bwd;
|
|
682
|
+
b.parent = parents;
|
|
683
|
+
cell.getter = (() => {
|
|
684
|
+
for (let i = 0; i < n; i++)
|
|
685
|
+
vals[i] = parents[i].value;
|
|
686
|
+
// External unless the live sources still equal this lens's own last
|
|
687
|
+
// back-write.
|
|
688
|
+
let external = true;
|
|
689
|
+
const lb = sc.lastBwd;
|
|
690
|
+
if (lb !== undefined) {
|
|
691
|
+
external = false;
|
|
692
|
+
for (let i = 0; i < n; i++) {
|
|
693
|
+
if (vals[i] !== lb[i]) {
|
|
694
|
+
external = true;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
sc.complement = sc.step(vals, sc.complement, external);
|
|
700
|
+
return sc.fwd(vals, sc.complement);
|
|
701
|
+
});
|
|
702
|
+
return cell;
|
|
703
|
+
}
|
|
704
|
+
// Install `value` on the prototype (V8 JITs it better than a class get/set).
|
|
705
|
+
Object.defineProperty(Cell.prototype, "value", {
|
|
706
|
+
get() {
|
|
707
|
+
const flags = this.flags;
|
|
708
|
+
if (this.getter !== undefined) {
|
|
709
|
+
if (flags & F.RecursedCheck) {
|
|
710
|
+
throw new RangeError(`Cyclic computed: ${this.constructor.name ?? "?"} read its own value`);
|
|
711
|
+
}
|
|
712
|
+
if (flags & F.Dirty ||
|
|
713
|
+
(flags & F.Pending &&
|
|
714
|
+
(checkDirty(this.deps, this) || ((this.flags = flags & ~F.Pending), false)))) {
|
|
715
|
+
if (this._update()) {
|
|
716
|
+
const subs = this.subs;
|
|
717
|
+
if (subs !== undefined)
|
|
718
|
+
shallowPropagate(subs);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
else if (!flags) {
|
|
722
|
+
// First read: lazy init.
|
|
723
|
+
this.flags = F.Mutable | F.RecursedCheck;
|
|
724
|
+
const prev = activeSub;
|
|
725
|
+
activeSub = this;
|
|
726
|
+
let threw = true;
|
|
727
|
+
try {
|
|
728
|
+
this.currentValue = this.getter();
|
|
729
|
+
threw = false;
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
activeSub = prev;
|
|
733
|
+
this.flags = threw ? F.Mutable | F.Dirty : this.flags & ~F.RecursedCheck;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (activeSub !== undefined)
|
|
737
|
+
link(this, activeSub, cycle);
|
|
738
|
+
return this.currentValue;
|
|
739
|
+
}
|
|
740
|
+
// Cell path.
|
|
741
|
+
if (flags & F.Dirty) {
|
|
742
|
+
this.flags = F.Mutable;
|
|
743
|
+
const prevV = this.currentValue;
|
|
744
|
+
this.currentValue = this.pendingValue;
|
|
745
|
+
if (!this._equals(prevV, this.currentValue)) {
|
|
746
|
+
const subs = this.subs;
|
|
747
|
+
if (subs !== undefined)
|
|
748
|
+
shallowPropagate(subs);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (activeSub !== undefined)
|
|
752
|
+
link(this, activeSub, cycle);
|
|
753
|
+
return this.currentValue;
|
|
754
|
+
},
|
|
755
|
+
set(next) {
|
|
756
|
+
if (this.getter === undefined) {
|
|
757
|
+
this._writeSource(next);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// Backward write. Deferred while batching/flushing so repeated writes
|
|
761
|
+
// coalesce (last-write-wins) and merge folds wait for all
|
|
762
|
+
// contributors; eager + synchronous otherwise. Exception: inside a
|
|
763
|
+
// network body writes are eager so the body's fixpoint loop observes
|
|
764
|
+
// its own edits via `peek()` between steps.
|
|
765
|
+
const b = this._bwd;
|
|
766
|
+
if (b === undefined) {
|
|
767
|
+
throw new TypeError("Cannot write to a computed");
|
|
768
|
+
}
|
|
769
|
+
const deferred = (batchDepth > 0 || flushing) && activeNetwork === undefined;
|
|
770
|
+
if (b.merge !== undefined) {
|
|
771
|
+
b.merge.receive(DIRECT_SLOT, next);
|
|
772
|
+
if (deferred)
|
|
773
|
+
this._enqueueBwd();
|
|
774
|
+
else
|
|
775
|
+
bwdUntracked(this, undefined, false);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (deferred && (Array.isArray(b.parent) || b.stateful !== undefined)) {
|
|
779
|
+
// Multi-parent / stateful: defer to flush so a split coalesces, a
|
|
780
|
+
// merge folds after all contributors land, and a complement steps
|
|
781
|
+
// once. Reuse `pendingValue` (unused by a getter's forward path) as
|
|
782
|
+
// the deferred backward target; drained by flush. Entry no-op vs the
|
|
783
|
+
// current view (GetPut) skips the walk.
|
|
784
|
+
if (this._equals(next, this.peek()))
|
|
785
|
+
return;
|
|
786
|
+
this.pendingValue = next;
|
|
787
|
+
this._enqueueBwd();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
// Single-parent lens: run the walk now. Inside a batch `_writeSource`
|
|
791
|
+
// stages the source (Dirty + pending) and defers only the flush, so
|
|
792
|
+
// the view reads back consistently and a later write supersedes via
|
|
793
|
+
// the source's pending value — last-write-wins, no queue, no lost
|
|
794
|
+
// revert. We must NOT peek the view here when batching: that would
|
|
795
|
+
// commit the source's pending value and break net-zero revert
|
|
796
|
+
// coalescing — `propagateBwd`'s `settled` no-op stop prunes a true
|
|
797
|
+
// no-op without committing. Outside a batch, the O(1) GetPut check is
|
|
798
|
+
// safe (no source is staged) and worth keeping.
|
|
799
|
+
if (!deferred && this._equals(next, this.peek()))
|
|
800
|
+
return;
|
|
801
|
+
bwdUntracked(this, next, deferred);
|
|
802
|
+
},
|
|
803
|
+
enumerable: false,
|
|
804
|
+
configurable: false,
|
|
805
|
+
});
|
|
806
|
+
// Backward pass (propagateBwd).
|
|
807
|
+
//
|
|
808
|
+
// Walk up `_bwdParent`, applying `put` at each lens / folding at each
|
|
809
|
+
// merge, until a source is committed (via the forward write path) or a
|
|
810
|
+
// parent merge is reached. `deferred` (inside batch/flush) stops at a
|
|
811
|
+
// parent merge after depositing so it folds once all contributors land;
|
|
812
|
+
// eager folds merges inline. Not a second engine: every path terminates
|
|
813
|
+
// in `_writeSource`.
|
|
814
|
+
// Backward evaluation runs UNTRACKED so `bwd`/`step`/`fwd` reads don't
|
|
815
|
+
// establish forward deps on whatever `activeSub` is writing (e.g. an
|
|
816
|
+
// effect that writes a lens). All backward entry points route through here.
|
|
817
|
+
function bwdUntracked(cell, target, deferred) {
|
|
818
|
+
const prev = activeSub;
|
|
819
|
+
activeSub = undefined;
|
|
820
|
+
try {
|
|
821
|
+
propagateBwd(cell, target, deferred);
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
activeSub = prev;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/** A cell's current value for the backward pass's internal no-op checks,
|
|
828
|
+
* WITHOUT side effects. A source staged earlier in this batch reads its
|
|
829
|
+
* pending value directly; reading it via `peek` would COMMIT the pending
|
|
830
|
+
* value (`_update`: currentValue = pendingValue), so a later net-zero
|
|
831
|
+
* revert would look like a real change and over-fire downstream. A
|
|
832
|
+
* non-source (lens/computed) has no such hazard and recomputes via peek. */
|
|
833
|
+
function settled(cell) {
|
|
834
|
+
return cell.getter === undefined && (cell.flags & F.Dirty) !== 0
|
|
835
|
+
? cell.pendingValue
|
|
836
|
+
: cell.peek();
|
|
837
|
+
}
|
|
838
|
+
function propagateBwd(start, target, deferred) {
|
|
839
|
+
let cell = start;
|
|
840
|
+
let v = target;
|
|
841
|
+
while (true) {
|
|
842
|
+
// Multi-parent lens: SPLIT the write into each parent (dual of a
|
|
843
|
+
// getter reading N parents — the `put` yields N upstream values).
|
|
844
|
+
const cb = cell._bwd;
|
|
845
|
+
const parent = cb.parent;
|
|
846
|
+
if (Array.isArray(parent)) {
|
|
847
|
+
propagateSplit(cell, v, deferred);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
let push;
|
|
851
|
+
if (cb.merge !== undefined) {
|
|
852
|
+
const node = cb.merge;
|
|
853
|
+
push = node.fold();
|
|
854
|
+
node.reset();
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
// Single-arg always: a source-reading lens baked `settled(parent)`
|
|
858
|
+
// into `put` at build time (see `buildLens1`); `pin` ignores `v`.
|
|
859
|
+
push = cb.put(v);
|
|
860
|
+
}
|
|
861
|
+
// Parentless lens (e.g. `pin`): no upstream, write absorbed. Sink.
|
|
862
|
+
if (parent === undefined)
|
|
863
|
+
return;
|
|
864
|
+
// Concrete no-op stop: if the parent already holds `push`, committing
|
|
865
|
+
// changes nothing upstream, so the walk stops. Sound for ANY topology
|
|
866
|
+
// (no speculation). A lossy lens hides an off-grid edit by returning
|
|
867
|
+
// the current source from `put`. Merge parents fold instead. `settled`
|
|
868
|
+
// reads a batched source's pending value WITHOUT committing it, so a
|
|
869
|
+
// net-zero revert leaves the source unchanged and downstream un-fired.
|
|
870
|
+
const pb = parent._bwd;
|
|
871
|
+
const parentMerge = pb !== undefined ? pb.merge : undefined;
|
|
872
|
+
if (parentMerge === undefined && parent._equals(push, settled(parent)))
|
|
873
|
+
return;
|
|
874
|
+
if (parentMerge !== undefined) {
|
|
875
|
+
parentMerge.receive(cell, push);
|
|
876
|
+
if (deferred) {
|
|
877
|
+
if (!(parent.flags & F.BwdQueued))
|
|
878
|
+
parent._enqueueBwd();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
cell = parent;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (parent.getter === undefined) {
|
|
885
|
+
// Source: commit + forward-propagate (the forward write).
|
|
886
|
+
parent._writeSource(push);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
// Parent is a lens: keep walking, carrying its new view value.
|
|
890
|
+
cell = parent;
|
|
891
|
+
v = push;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/** Split a multi-parent cell's write across its N parents. `_put(target)`
|
|
895
|
+
* returns the per-parent update array (`undefined` ⇒ leave parent);
|
|
896
|
+
* each defined update recurses via `propagateBwd`. Eager splits coalesce
|
|
897
|
+
* under one flush (same guarantee as `batch()`); the coalescing lives
|
|
898
|
+
* here since such a cell may be reached as a write start or mid-chain. */
|
|
899
|
+
function propagateSplit(cell, target, deferred) {
|
|
900
|
+
const b = cell._bwd;
|
|
901
|
+
const parents = b.parent;
|
|
902
|
+
const n = parents.length;
|
|
903
|
+
// STATEFUL lens: `bwd` reads the complement and returns per-parent
|
|
904
|
+
// updates plus the post-write complement. We commit the stepped
|
|
905
|
+
// complement and fork the source updates; absorption is the lens's job
|
|
906
|
+
// (its `bwd` returns `undefined` updates, forked as no-ops).
|
|
907
|
+
const sc = b.stateful;
|
|
908
|
+
if (sc !== undefined) {
|
|
909
|
+
// Bring the complement current with the sources before the back-write
|
|
910
|
+
// (a source may have changed without the view being read, leaving
|
|
911
|
+
// `step` un-run). Untracked, so reading `.value` adds no dependency.
|
|
912
|
+
void cell.value;
|
|
913
|
+
const vals = new Array(n);
|
|
914
|
+
for (let i = 0; i < n; i++)
|
|
915
|
+
vals[i] = parents[i].peek();
|
|
916
|
+
const res = b.put(target, vals, sc.complement);
|
|
917
|
+
const updates = res.updates;
|
|
918
|
+
const cand = new Array(n);
|
|
919
|
+
let anyWrite = false;
|
|
920
|
+
for (let i = 0; i < n; i++) {
|
|
921
|
+
const u = updates[i];
|
|
922
|
+
if (u === undefined) {
|
|
923
|
+
cand[i] = vals[i];
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
cand[i] = u;
|
|
927
|
+
anyWrite = true;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
sc.complement = sc.step(cand, res.complement, false);
|
|
931
|
+
if (!anyWrite) {
|
|
932
|
+
// Complement-only change (no source moves): mark dirty for a correct next read.
|
|
933
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
sc.lastBwd = cand;
|
|
937
|
+
if (deferred) {
|
|
938
|
+
forkInto(parents, updates, n);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
++batchDepth;
|
|
942
|
+
try {
|
|
943
|
+
forkInto(parents, updates, n);
|
|
944
|
+
}
|
|
945
|
+
finally {
|
|
946
|
+
if (!--batchDepth)
|
|
947
|
+
flush();
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const updates = b.put(target);
|
|
952
|
+
// No speculation: each defined update forks to its parent, where
|
|
953
|
+
// `_writeSource`'s equality check prunes no-op sources and the forward
|
|
954
|
+
// pass prunes unchanged views. Absorption ⇒ `undefined` updates.
|
|
955
|
+
if (deferred) {
|
|
956
|
+
forkInto(parents, updates, n);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
++batchDepth;
|
|
960
|
+
try {
|
|
961
|
+
forkInto(parents, updates, n);
|
|
962
|
+
}
|
|
963
|
+
finally {
|
|
964
|
+
if (!--batchDepth)
|
|
965
|
+
flush();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/** Route each defined update to its parent: a source commits directly, a
|
|
969
|
+
* lens/multi-parent/merge re-enters the backward pass. Always called
|
|
970
|
+
* under a bumped `batchDepth` so commits coalesce into one flush. */
|
|
971
|
+
function forkInto(parents, updates, n) {
|
|
972
|
+
for (let i = 0; i < n; i++) {
|
|
973
|
+
const u = updates[i];
|
|
974
|
+
if (u === undefined)
|
|
975
|
+
continue;
|
|
976
|
+
const parent = parents[i];
|
|
977
|
+
if (parent.getter === undefined)
|
|
978
|
+
parent._writeSource(u);
|
|
979
|
+
else
|
|
980
|
+
propagateBwd(parent, u, true);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/** Writable source; passes an existing `Writable` through (idempotent). */
|
|
984
|
+
export function cell(initial, opts) {
|
|
985
|
+
if (initial instanceof Cell)
|
|
986
|
+
return initial;
|
|
987
|
+
return new Cell(initial, opts);
|
|
988
|
+
}
|
|
989
|
+
export function computed(fn) {
|
|
990
|
+
const cell = new Cell(undefined);
|
|
991
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
992
|
+
cell.getter = fn;
|
|
993
|
+
return cell;
|
|
994
|
+
}
|
|
995
|
+
// Bare (untyped) factories. Construct a plain `Cell`, inferring `R`
|
|
996
|
+
// from the closures (the polymorphic-`this` statics are for typed
|
|
997
|
+
// subclasses like `Vec.lens`).
|
|
998
|
+
const CELL_CTOR = Cell;
|
|
999
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
1000
|
+
export function derive(...args) {
|
|
1001
|
+
if (args.length === 1)
|
|
1002
|
+
return buildComputed(CELL_CTOR, args[0]);
|
|
1003
|
+
const [parent, fn] = args;
|
|
1004
|
+
if (Array.isArray(parent))
|
|
1005
|
+
return buildLensN(CELL_CTOR, parent, fn, undefined, false);
|
|
1006
|
+
return buildComputed(CELL_CTOR, () => fn(parent.value));
|
|
1007
|
+
}
|
|
1008
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
1009
|
+
export function lens(...args) {
|
|
1010
|
+
const [parent, a, b] = args;
|
|
1011
|
+
if (args.length === 2) {
|
|
1012
|
+
return buildStateful(CELL_CTOR, Array.isArray(parent) ? parent : [parent], a);
|
|
1013
|
+
}
|
|
1014
|
+
const readsSource = b.length >= 2;
|
|
1015
|
+
if (Array.isArray(parent))
|
|
1016
|
+
return buildLensN(CELL_CTOR, parent, a, b, readsSource);
|
|
1017
|
+
return buildLens1(CELL_CTOR, parent, a, b, readsSource);
|
|
1018
|
+
}
|
|
1019
|
+
// Effect — alien-signals verbatim.
|
|
1020
|
+
class Effect {
|
|
1021
|
+
flags = F.Watching | F.RecursedCheck;
|
|
1022
|
+
subs = undefined;
|
|
1023
|
+
subsTail = undefined;
|
|
1024
|
+
deps = undefined;
|
|
1025
|
+
depsTail = undefined;
|
|
1026
|
+
fn;
|
|
1027
|
+
cleanup = undefined;
|
|
1028
|
+
constructor(fn) {
|
|
1029
|
+
this.fn = fn;
|
|
1030
|
+
const prev = activeSub;
|
|
1031
|
+
activeSub = this;
|
|
1032
|
+
try {
|
|
1033
|
+
++runDepth;
|
|
1034
|
+
const ret = fn();
|
|
1035
|
+
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1036
|
+
}
|
|
1037
|
+
finally {
|
|
1038
|
+
--runDepth;
|
|
1039
|
+
activeSub = prev;
|
|
1040
|
+
this.flags &= ~F.RecursedCheck;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
_update() {
|
|
1044
|
+
this.flags = F.Mutable;
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
_notify() {
|
|
1048
|
+
let e = this;
|
|
1049
|
+
let insertIndex = queuedLength;
|
|
1050
|
+
const firstInsertedIndex = insertIndex;
|
|
1051
|
+
do {
|
|
1052
|
+
queued[insertIndex++] = e;
|
|
1053
|
+
e.flags &= ~F.Watching;
|
|
1054
|
+
const next = e.subs?.sub;
|
|
1055
|
+
if (next === undefined || !(next.flags & F.Watching))
|
|
1056
|
+
break;
|
|
1057
|
+
e = next;
|
|
1058
|
+
} while (true);
|
|
1059
|
+
queuedLength = insertIndex;
|
|
1060
|
+
let idx = insertIndex, firstIdx = firstInsertedIndex;
|
|
1061
|
+
while (firstIdx < --idx) {
|
|
1062
|
+
const left = queued[firstIdx];
|
|
1063
|
+
queued[firstIdx++] = queued[idx];
|
|
1064
|
+
queued[idx] = left;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
_unwatched() {
|
|
1068
|
+
this.flags = F.None;
|
|
1069
|
+
disposeAllDepsInReverse(this);
|
|
1070
|
+
const sub = this.subs;
|
|
1071
|
+
if (sub !== undefined)
|
|
1072
|
+
unlink(sub);
|
|
1073
|
+
if (this.cleanup)
|
|
1074
|
+
this._runCleanup();
|
|
1075
|
+
}
|
|
1076
|
+
_run() {
|
|
1077
|
+
const flags = this.flags;
|
|
1078
|
+
if (flags & F.Dirty || (flags & F.Pending && checkDirty(this.deps, this))) {
|
|
1079
|
+
if (this.cleanup) {
|
|
1080
|
+
this._runCleanup();
|
|
1081
|
+
if (!this.flags)
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
this.depsTail = undefined;
|
|
1085
|
+
this.flags = F.Watching | F.RecursedCheck;
|
|
1086
|
+
const prev = activeSub;
|
|
1087
|
+
activeSub = this;
|
|
1088
|
+
try {
|
|
1089
|
+
++cycle;
|
|
1090
|
+
++runDepth;
|
|
1091
|
+
const ret = this.fn();
|
|
1092
|
+
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1093
|
+
}
|
|
1094
|
+
finally {
|
|
1095
|
+
--runDepth;
|
|
1096
|
+
activeSub = prev;
|
|
1097
|
+
this.flags &= ~F.RecursedCheck;
|
|
1098
|
+
purgeDeps(this);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
else if (this.deps !== undefined) {
|
|
1102
|
+
this.flags = F.Watching;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
_runCleanup() {
|
|
1106
|
+
const c = this.cleanup;
|
|
1107
|
+
this.cleanup = undefined;
|
|
1108
|
+
const prev = activeSub;
|
|
1109
|
+
activeSub = undefined;
|
|
1110
|
+
try {
|
|
1111
|
+
c();
|
|
1112
|
+
}
|
|
1113
|
+
finally {
|
|
1114
|
+
activeSub = prev;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
export function effect(fn) {
|
|
1119
|
+
const e = new Effect(fn);
|
|
1120
|
+
return () => e._unwatched();
|
|
1121
|
+
}
|
|
1122
|
+
// Alternates backward-drain and effect-drain to a fixpoint: backward
|
|
1123
|
+
// commits sources (queuing effects); effects may write (more effects /
|
|
1124
|
+
// more bwd entries). Loops until both queues are exhausted.
|
|
1125
|
+
function flush() {
|
|
1126
|
+
if (flushing)
|
|
1127
|
+
return;
|
|
1128
|
+
flushing = true;
|
|
1129
|
+
let bwdIndex = 0;
|
|
1130
|
+
try {
|
|
1131
|
+
// Head-checked so the common already-drained case skips the body.
|
|
1132
|
+
while (bwdIndex < bwdQueue.length || notifyIndex < queuedLength) {
|
|
1133
|
+
while (bwdIndex < bwdQueue.length) {
|
|
1134
|
+
const cell = bwdQueue[bwdIndex];
|
|
1135
|
+
const cb = cell._bwd;
|
|
1136
|
+
if (cb.queueIdx !== bwdIndex || !(cell.flags & F.BwdQueued)) {
|
|
1137
|
+
bwdIndex++;
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
bwdIndex++;
|
|
1141
|
+
cell.flags &= ~F.BwdQueued;
|
|
1142
|
+
if (cb.merge !== undefined) {
|
|
1143
|
+
bwdUntracked(cell, undefined, true);
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
bwdUntracked(cell, cell.pendingValue, true);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
while (notifyIndex < queuedLength) {
|
|
1150
|
+
const e = queued[notifyIndex];
|
|
1151
|
+
queued[notifyIndex++] = undefined;
|
|
1152
|
+
e._run();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
finally {
|
|
1157
|
+
bwdQueue.length = 0;
|
|
1158
|
+
notifyIndex = 0;
|
|
1159
|
+
queuedLength = 0;
|
|
1160
|
+
flushing = false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
export function batch(fn) {
|
|
1164
|
+
++batchDepth;
|
|
1165
|
+
try {
|
|
1166
|
+
return fn();
|
|
1167
|
+
}
|
|
1168
|
+
finally {
|
|
1169
|
+
if (!--batchDepth)
|
|
1170
|
+
flush();
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
export function untracked(fn) {
|
|
1174
|
+
const prev = activeSub;
|
|
1175
|
+
activeSub = undefined;
|
|
1176
|
+
try {
|
|
1177
|
+
return fn();
|
|
1178
|
+
}
|
|
1179
|
+
finally {
|
|
1180
|
+
activeSub = prev;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
class _NetworkNode {
|
|
1184
|
+
subs = undefined;
|
|
1185
|
+
subsTail = undefined;
|
|
1186
|
+
deps = undefined;
|
|
1187
|
+
depsTail = undefined;
|
|
1188
|
+
flags = F.Watching | F.RecursedCheck;
|
|
1189
|
+
body;
|
|
1190
|
+
manual;
|
|
1191
|
+
/** Per-instance last-seen dep values; used to compute `dirty`. */
|
|
1192
|
+
lastValues = new Map();
|
|
1193
|
+
pending = false;
|
|
1194
|
+
disposed = false;
|
|
1195
|
+
_ownCycle = 0;
|
|
1196
|
+
_depsSet = new Set();
|
|
1197
|
+
_handle;
|
|
1198
|
+
constructor(body, manual) {
|
|
1199
|
+
this.body = body;
|
|
1200
|
+
this.manual = manual;
|
|
1201
|
+
}
|
|
1202
|
+
/** Two-phase init so the body sees its own handle on the first fire. */
|
|
1203
|
+
_initWithHandle(handle, initialDeps) {
|
|
1204
|
+
this._handle = handle;
|
|
1205
|
+
this._linkBatch(initialDeps);
|
|
1206
|
+
this._runBody(EMPTY_DIRTY);
|
|
1207
|
+
}
|
|
1208
|
+
_update() {
|
|
1209
|
+
this.flags = F.Mutable;
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
_notify() {
|
|
1213
|
+
if (this.manual) {
|
|
1214
|
+
this.pending = true;
|
|
1215
|
+
this.flags |= F.Watching;
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
queued[queuedLength++] = this;
|
|
1219
|
+
this.flags &= ~F.Watching;
|
|
1220
|
+
}
|
|
1221
|
+
_unwatched() {
|
|
1222
|
+
this.disposed = true;
|
|
1223
|
+
this.flags = F.None;
|
|
1224
|
+
disposeAllDepsInReverse(this);
|
|
1225
|
+
const sub = this.subs;
|
|
1226
|
+
if (sub !== undefined)
|
|
1227
|
+
unlink(sub);
|
|
1228
|
+
this.lastValues.clear();
|
|
1229
|
+
}
|
|
1230
|
+
_run() {
|
|
1231
|
+
if (this.disposed)
|
|
1232
|
+
return;
|
|
1233
|
+
const flags = this.flags;
|
|
1234
|
+
if (flags & F.Dirty || (flags & F.Pending && checkDirty(this.deps, this))) {
|
|
1235
|
+
this._runBody(this._computeDirty());
|
|
1236
|
+
}
|
|
1237
|
+
else if (this.deps !== undefined) {
|
|
1238
|
+
this.flags = F.Watching;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
_computeDirty() {
|
|
1242
|
+
let dirty;
|
|
1243
|
+
for (const [sig, lastVal] of this.lastValues) {
|
|
1244
|
+
if (sig.peek() !== lastVal) {
|
|
1245
|
+
if (dirty === undefined)
|
|
1246
|
+
dirty = new Set();
|
|
1247
|
+
dirty.add(sig);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return dirty ?? EMPTY_DIRTY;
|
|
1251
|
+
}
|
|
1252
|
+
_runBody(dirty) {
|
|
1253
|
+
// RecursedCheck doubles as the "body running" guard (see flush()).
|
|
1254
|
+
this.flags = F.Watching | F.RecursedCheck;
|
|
1255
|
+
const prevSettler = activeNetwork;
|
|
1256
|
+
activeNetwork = this;
|
|
1257
|
+
try {
|
|
1258
|
+
++cycle;
|
|
1259
|
+
++runDepth;
|
|
1260
|
+
++batchDepth;
|
|
1261
|
+
try {
|
|
1262
|
+
this.body(dirty, this._handle);
|
|
1263
|
+
}
|
|
1264
|
+
finally {
|
|
1265
|
+
if (!--batchDepth)
|
|
1266
|
+
flush();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
finally {
|
|
1270
|
+
--runDepth;
|
|
1271
|
+
activeNetwork = prevSettler;
|
|
1272
|
+
this.flags &= ~F.RecursedCheck;
|
|
1273
|
+
this.lastValues.clear();
|
|
1274
|
+
let l = this.deps;
|
|
1275
|
+
while (l !== undefined) {
|
|
1276
|
+
const sig = l.dep;
|
|
1277
|
+
this.lastValues.set(sig, sig.peek());
|
|
1278
|
+
l = l.nextDep;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
this.pending = false;
|
|
1282
|
+
}
|
|
1283
|
+
flush() {
|
|
1284
|
+
if (this.disposed)
|
|
1285
|
+
return;
|
|
1286
|
+
if (this.flags & F.RecursedCheck) {
|
|
1287
|
+
throw new Error("network: flush() called from inside body — would recurse infinitely. " +
|
|
1288
|
+
"Return from the body and let the next dep change drive the next fire.");
|
|
1289
|
+
}
|
|
1290
|
+
this._runBody(this._computeDirty());
|
|
1291
|
+
}
|
|
1292
|
+
subscribe(sigs) {
|
|
1293
|
+
if (this.disposed)
|
|
1294
|
+
return;
|
|
1295
|
+
this._linkBatch(sigs);
|
|
1296
|
+
}
|
|
1297
|
+
unsubscribe(sigs) {
|
|
1298
|
+
if (this.disposed)
|
|
1299
|
+
return;
|
|
1300
|
+
const set = this._depsSet;
|
|
1301
|
+
for (const s of sigs) {
|
|
1302
|
+
if (!set.has(s))
|
|
1303
|
+
continue;
|
|
1304
|
+
set.delete(s);
|
|
1305
|
+
let l = this.deps;
|
|
1306
|
+
while (l !== undefined) {
|
|
1307
|
+
if (l.dep === s) {
|
|
1308
|
+
unlink(l, this);
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
l = l.nextDep;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
_linkBatch(sigs) {
|
|
1316
|
+
const set = this._depsSet;
|
|
1317
|
+
let tail = this.deps;
|
|
1318
|
+
if (tail !== undefined) {
|
|
1319
|
+
while (tail.nextDep !== undefined)
|
|
1320
|
+
tail = tail.nextDep;
|
|
1321
|
+
}
|
|
1322
|
+
this.depsTail = tail;
|
|
1323
|
+
for (const s of sigs) {
|
|
1324
|
+
if (set.has(s))
|
|
1325
|
+
continue;
|
|
1326
|
+
set.add(s);
|
|
1327
|
+
link(s, this, ++this._ownCycle);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
/** Build a reactive sub-DAG node with explicit topology. The body fires
|
|
1332
|
+
* when any subscribed dep changes (`dirty` = the changed subset), runs
|
|
1333
|
+
* inside `batch()`, and self-excludes its own writes. Topology is the
|
|
1334
|
+
* deps array + later subscribe/unsubscribe (body reads add no deps).
|
|
1335
|
+
* `flush()` from inside the body throws; `manual: true` defers
|
|
1336
|
+
* auto-firing so only `flush()` advances. */
|
|
1337
|
+
export function network(
|
|
1338
|
+
// biome-ignore lint/suspicious/noExplicitAny: deps come in many flavours
|
|
1339
|
+
deps, body, opts) {
|
|
1340
|
+
const node = new _NetworkNode(body, opts?.manual ?? false);
|
|
1341
|
+
const handle = {
|
|
1342
|
+
dispose: () => node._unwatched(),
|
|
1343
|
+
flush: () => node.flush(),
|
|
1344
|
+
subscribe: (...sigs) => node.subscribe(sigs),
|
|
1345
|
+
unsubscribe: (...sigs) => node.unsubscribe(sigs),
|
|
1346
|
+
};
|
|
1347
|
+
node._initWithHandle(handle, deps);
|
|
1348
|
+
return handle;
|
|
1349
|
+
}
|