bireactive 0.2.4 → 0.3.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/dist/animation/anim.js +4 -0
- package/dist/automerge/doc-cell.d.ts +20 -0
- package/dist/automerge/doc-cell.js +80 -0
- package/dist/automerge/index.d.ts +3 -0
- package/dist/automerge/index.js +12 -0
- package/dist/automerge/reconcile.d.ts +5 -0
- package/dist/automerge/reconcile.js +63 -0
- package/dist/coll.d.ts +7 -7
- package/dist/core/_counts.d.ts +48 -0
- package/dist/core/_counts.js +58 -0
- package/dist/core/cell.d.ts +182 -123
- package/dist/core/cell.js +1140 -721
- package/dist/core/debug.d.ts +25 -0
- package/dist/core/debug.js +121 -0
- package/dist/core/index.d.ts +9 -14
- package/dist/core/index.js +9 -14
- package/dist/core/lenses/aggregates.d.ts +1 -1
- package/dist/core/lenses/aggregates.js +4 -3
- package/dist/core/lenses/closed-form-policies.js +14 -9
- package/dist/core/lenses/decompositions.js +3 -3
- package/dist/core/lenses/domain-aggregates.js +5 -5
- package/dist/core/lenses/geometry.d.ts +1 -1
- package/dist/core/lenses/geometry.js +6 -7
- package/dist/core/lenses/index.d.ts +1 -0
- package/dist/core/lenses/index.js +1 -0
- package/dist/core/lenses/memory.d.ts +2 -2
- package/dist/core/lenses/memory.js +3 -3
- package/dist/core/lenses/snap.d.ts +18 -0
- package/dist/core/lenses/snap.js +145 -0
- package/dist/core/lenses/typed-factor.js +4 -3
- package/dist/core/optic.d.ts +13 -0
- package/dist/core/optic.js +44 -0
- package/dist/core/optics.d.ts +10 -0
- package/dist/core/optics.js +30 -0
- package/dist/core/store.d.ts +10 -0
- package/dist/core/store.js +85 -0
- package/dist/core/traits.d.ts +1 -0
- package/dist/core/values/audio.js +4 -5
- package/dist/core/values/box.js +7 -7
- package/dist/core/values/canvas.js +15 -18
- package/dist/core/values/color.js +5 -5
- package/dist/core/values/field.d.ts +70 -0
- package/dist/core/values/field.js +230 -0
- package/dist/core/values/gpu.d.ts +4 -2
- package/dist/core/values/gpu.js +11 -4
- package/dist/core/values/matrix.js +7 -7
- package/dist/core/values/num.d.ts +1 -1
- package/dist/core/values/num.js +1 -1
- package/dist/core/values/pose.js +4 -4
- package/dist/core/values/range.js +6 -6
- package/dist/core/values/str.js +8 -8
- package/dist/core/values/template.d.ts +1 -1
- package/dist/core/values/template.js +2 -1
- package/dist/core/values/transform.js +7 -7
- package/dist/core/values/tri.js +3 -3
- package/dist/core/values/vec.js +8 -12
- package/dist/ext/timeline.js +2 -2
- package/dist/formats/cst.d.ts +127 -0
- package/dist/formats/cst.js +280 -0
- package/dist/formats/edn.d.ts +2 -0
- package/dist/formats/edn.js +301 -0
- package/dist/formats/index.d.ts +6 -0
- package/dist/formats/index.js +8 -0
- package/dist/formats/json.d.ts +2 -0
- package/dist/formats/json.js +332 -0
- package/dist/formats/lens.d.ts +8 -0
- package/dist/formats/lens.js +51 -0
- package/dist/formats/toml.d.ts +2 -0
- package/dist/formats/toml.js +526 -0
- package/dist/formats/yaml.d.ts +2 -0
- package/dist/formats/yaml.js +661 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +5 -0
- package/dist/jsx-runtime.d.ts +54 -0
- package/dist/jsx-runtime.js +219 -0
- package/dist/learn/data.d.ts +49 -0
- package/dist/learn/data.js +181 -0
- package/dist/learn/index.d.ts +3 -0
- package/dist/learn/index.js +6 -0
- package/dist/learn/lens-net.d.ts +63 -0
- package/dist/learn/lens-net.js +219 -0
- package/dist/learn/mlp.d.ts +77 -0
- package/dist/learn/mlp.js +292 -0
- package/dist/propagators/csp.d.ts +13 -0
- package/dist/propagators/csp.js +52 -0
- package/dist/propagators/flex.d.ts +31 -0
- package/dist/propagators/flex.js +189 -0
- package/dist/propagators/graph.d.ts +73 -0
- package/dist/propagators/graph.js +543 -0
- package/dist/propagators/index.d.ts +8 -6
- package/dist/propagators/index.js +15 -6
- package/dist/propagators/lattice.d.ts +45 -0
- package/dist/propagators/lattice.js +113 -0
- package/dist/propagators/layout.d.ts +1 -27
- package/dist/propagators/layout.js +6 -175
- package/dist/propagators/numeric.d.ts +17 -0
- package/dist/propagators/numeric.js +93 -0
- package/dist/propagators/solver.d.ts +51 -0
- package/dist/propagators/solver.js +175 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/lens.d.ts +121 -0
- package/dist/schema/lens.js +429 -0
- package/dist/shapes/annular-sector.js +4 -4
- package/dist/shapes/button.js +1 -1
- package/dist/shapes/circle.js +1 -1
- package/dist/shapes/drag-behaviors.d.ts +56 -0
- package/dist/shapes/drag-behaviors.js +102 -0
- package/dist/shapes/drag-spec.d.ts +52 -0
- package/dist/shapes/drag-spec.js +112 -0
- package/dist/shapes/handle.js +2 -2
- package/dist/shapes/index.d.ts +3 -1
- package/dist/shapes/index.js +3 -1
- package/dist/shapes/interaction.d.ts +2 -3
- package/dist/shapes/interaction.js +77 -56
- package/dist/shapes/label.js +7 -1
- package/dist/shapes/layout.d.ts +47 -1
- package/dist/shapes/layout.js +60 -2
- package/dist/shapes/rect.js +7 -7
- package/dist/shapes/shape.js +8 -8
- package/dist/web/diagram.js +2 -2
- package/package.json +24 -2
- package/dist/propagators/network.d.ts +0 -52
- package/dist/propagators/network.js +0 -185
- package/dist/propagators/propagator.d.ts +0 -12
- package/dist/propagators/propagator.js +0 -16
- package/dist/propagators/range.d.ts +0 -45
- package/dist/propagators/range.js +0 -147
- package/dist/propagators/relations.d.ts +0 -60
- package/dist/propagators/relations.js +0 -343
package/dist/core/cell.js
CHANGED
|
@@ -1,56 +1,46 @@
|
|
|
1
1
|
// cell.ts — symmetric bidirectional reactive engine.
|
|
2
2
|
//
|
|
3
|
-
// Forward propagation is alien-signals verbatim
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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.
|
|
3
|
+
// Forward propagation is alien-signals verbatim. Backward is the same lazy
|
|
4
|
+
// push-pull run on the transpose of the lens graph, carried by one `LensLink`
|
|
5
|
+
// structure (the backward dual of forward's `Link`): `parentEdges` down,
|
|
6
|
+
// `childEdges` up, created eagerly at lens construction. One traversal each
|
|
7
|
+
// direction:
|
|
11
8
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// stateful-lens complement, not a bespoke engine kind.
|
|
9
|
+
// role forward (source → view) backward (view → source)
|
|
10
|
+
// down edge subs (who reads me) parentEdges (my parents)
|
|
11
|
+
// up edge deps (my deps) childEdges (my lens-children)
|
|
12
|
+
// push (mark) propagate (down `subs`) markDown (down `parentEdges`)
|
|
13
|
+
// pull (resolve) checkDirty (up `deps`) resolveCone (up `childEdges`)
|
|
14
|
+
// commit/compute _update / getter writeBack
|
|
15
|
+
// "dirty" flag F.Dirty (source staged) BF.Dirty (view holds target)
|
|
16
|
+
// "pending" flag F.Pending (on the cone) BF.Pending (on the back-path)
|
|
21
17
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
18
|
+
// Forward flags live on `flags`, backward on a separate `bflags` word, so the
|
|
19
|
+
// two never share a bit. A view write marks the back-path `BF.Pending` and wakes
|
|
20
|
+
// each source's forward cone; nothing runs until a read pulls (source-centric: a
|
|
21
|
+
// source resolves ALL its writers together and commits once). Reads pull only at
|
|
22
|
+
// clean entry points (getter top, source `_update`/`_writeSource`, effect
|
|
23
|
+
// `_run`), never mid-compute. Fan-in is the one non-dual piece: a `merge`
|
|
24
|
+
// accumulates N contributors and folds once, post-order, inside `resolveCone`.
|
|
25
25
|
//
|
|
26
|
-
// Mode table —
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
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
|
-
};
|
|
26
|
+
// Mode table — `getter`/`_bwd` fix forward/writable; the backward shape is read off
|
|
27
|
+
// `_bwd` field presence (`merge` / `stateful` / `parentEdges` / `scatter`):
|
|
28
|
+
// source getter undefined (truth in currentValue)
|
|
29
|
+
// derived getter, no _bwd (read-only derived)
|
|
30
|
+
// lens 1→1 getter + _bwd{ put } (scalar put)
|
|
31
|
+
// multi-out getter + _bwd{ put, scatter } (1→N / N→M tuple put)
|
|
32
|
+
// merge getter + _bwd{ merge } (N→1 backward fold)
|
|
33
|
+
// stateful getter + _bwd{ put, scatter, stateful } (complement-carrying)
|
|
34
|
+
// pin getter + _bwd{} no parentEdges (parentless sink)
|
|
35
|
+
// Writable iff `_bwd !== undefined`. `pendingValue` is a source's staged write
|
|
36
|
+
// (and a view's armed back-target); a derived cell never uses it forward.
|
|
37
|
+
// Counts-first instrumentation: off by default, one branch per site (see _counts).
|
|
38
|
+
import { COUNTS, counts } from "./_counts.js";
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Module state — mutable engine-wide variables and pooled scratch buffers.
|
|
41
|
+
// Every pool is non-reentrant: forward and backward runs never nest, so one
|
|
42
|
+
// shared buffer per role suffices (no per-call allocation).
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
54
44
|
let cycle = 0;
|
|
55
45
|
let runDepth = 0;
|
|
56
46
|
let batchDepth = 0;
|
|
@@ -58,18 +48,189 @@ let notifyIndex = 0;
|
|
|
58
48
|
let queuedLength = 0;
|
|
59
49
|
let activeSub;
|
|
60
50
|
let flushing = false;
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
let
|
|
51
|
+
/** A microtask flush is queued. Effects run asynchronously (end of turn), so a
|
|
52
|
+
* burst of writes wakes each at most once; reads stay synchronous. */
|
|
53
|
+
let scheduled = false;
|
|
54
|
+
/** A `Sync` watcher (a `network`) is queued: a wake flushes the whole queue
|
|
55
|
+
* synchronously (eager solve), so a read right after the write sees post-solve
|
|
56
|
+
* state. Writes that wake plain effects alone defer to the microtask. */
|
|
57
|
+
let syncFlush = false;
|
|
58
|
+
/** The running self-excluding watcher (`Exclude`-mode `Effect`), passed as
|
|
59
|
+
* `propagate`'s `excluding` so its own writes don't re-trigger it. */
|
|
60
|
+
let activeExcluded;
|
|
64
61
|
const queued = [];
|
|
62
|
+
/** Re-entrancy guard: during a back-resolve a `put`'s source read commits
|
|
63
|
+
* normally but must NOT trigger a nested resolve. */
|
|
64
|
+
let draining = false;
|
|
65
|
+
// Pooled backward-traversal buffers (non-reentrant under `draining`, so reused
|
|
66
|
+
// across calls — no per-call allocation).
|
|
67
|
+
/** `backResolve` phase-1 source worklist (collect, then resolve in phase 2). */
|
|
68
|
+
const backSources = [];
|
|
69
|
+
/** Monotone epoch stamped onto `Cell.bEpoch` during a `backResolve` collect, so
|
|
70
|
+
* diamonds visit each node once without a Set (the backward dual of `cycle`). */
|
|
71
|
+
let backCycle = 0;
|
|
72
|
+
/** `writeBack`'s explicit descent stack (depth-first, left-to-right), as two
|
|
73
|
+
* parallel pooled columns. Non-reentrant — a `writeBack` triggers no nested
|
|
74
|
+
* `writeBack` — so one shared stack suffices (no per-call allocation). */
|
|
75
|
+
const wbNode = [];
|
|
76
|
+
const wbTarget = [];
|
|
77
|
+
/** Stateful lenses passed through in a `writeBack`, re-stamped post-order once
|
|
78
|
+
* their sources are written (versions bumped) so the next forward read sees an
|
|
79
|
+
* unchanged sum and skips `step` — own-write provenance. Pooled; non-reentrant. */
|
|
80
|
+
const wbStateful = [];
|
|
81
|
+
/** `resolveCone`'s pooled post-order frame stack: the node and its next-child
|
|
82
|
+
* cursor. Non-reentrant (no nested `resolveCone`), so shared pools suffice. */
|
|
83
|
+
const rcNode = [];
|
|
84
|
+
const rcEdge = [];
|
|
65
85
|
const EMPTY_DIRTY = new Set();
|
|
66
|
-
/**
|
|
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.
|
|
86
|
+
/** Fires on every source value-change. Backward writes reach it via `_writeSource`. */
|
|
72
87
|
let writeHook;
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Internal constants & types — flag bits, mode bits, and the node/edge records.
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
91
|
+
// Forward flag bits (alien-signals v2), on `flags`.
|
|
92
|
+
const F = {
|
|
93
|
+
None: 0,
|
|
94
|
+
Mutable: 1,
|
|
95
|
+
Watching: 2,
|
|
96
|
+
RecursedCheck: 4,
|
|
97
|
+
Recursed: 8,
|
|
98
|
+
Dirty: 16,
|
|
99
|
+
Pending: 32,
|
|
100
|
+
};
|
|
101
|
+
// Backward flag bits, on a Cell's own `bflags` word (the dual of `flags`).
|
|
102
|
+
const BF = {
|
|
103
|
+
None: 0,
|
|
104
|
+
/** Dual of `F.Dirty`: this view holds an unresolved back-target in `pendingValue`. */
|
|
105
|
+
Dirty: 1,
|
|
106
|
+
/** Dual of `F.Pending`: this node is on the back-path to its sources. */
|
|
107
|
+
Pending: 2,
|
|
108
|
+
/** Static (set once at construction): a write armed here is structurally
|
|
109
|
+
* impossible — its mandatory back-spine dead-ends at a sole read-only-derived
|
|
110
|
+
* parent. Checked atop `arm` so the throw lands before any backward mutation. */
|
|
111
|
+
WriteBlocked: 4,
|
|
112
|
+
};
|
|
113
|
+
/** Armed root OR on a back-path — i.e. a read must `backResolve` first. */
|
|
114
|
+
const BACK_MARKED = BF.Dirty | BF.Pending;
|
|
115
|
+
// Effect mode bits (on `Effect.mode`), so one watcher class serves both plain
|
|
116
|
+
// effects (`None`) and `network()` (which sets these).
|
|
117
|
+
const EM = {
|
|
118
|
+
None: 0,
|
|
119
|
+
/** Explicit topology: body reads don't auto-subscribe (no re-link / purge). */
|
|
120
|
+
NoTrack: 1,
|
|
121
|
+
/** Self-exclude the node's own writes (set `activeExcluded` during the body). */
|
|
122
|
+
Exclude: 2,
|
|
123
|
+
/** A wake forces a synchronous flush (eager solve), vs the microtask default. */
|
|
124
|
+
Sync: 4,
|
|
125
|
+
/** Don't auto-fire on a wake; only an explicit `flush()` advances the body. */
|
|
126
|
+
Manual: 8,
|
|
127
|
+
};
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
129
|
+
// Internal helpers — mode predicates, edge wiring, and the write-hook installer.
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// Mode predicates — the single place a cell's role is read off its fields.
|
|
132
|
+
/** Source (truth leaf): no forward derivation. */
|
|
133
|
+
function isSource(c) {
|
|
134
|
+
return c.getter === undefined;
|
|
135
|
+
}
|
|
136
|
+
/** Writable: carries a backward sidecar (lens / multi-out / merge / stateful / pin). */
|
|
137
|
+
function isWritable(c) {
|
|
138
|
+
return c._bwd !== undefined;
|
|
139
|
+
}
|
|
140
|
+
/** Read-only derived: a `derive` with no backward path (back-walk throws on it). */
|
|
141
|
+
function isReadOnlyDerived(c) {
|
|
142
|
+
return !isSource(c) && !isWritable(c);
|
|
143
|
+
}
|
|
144
|
+
/** Forward primal a source-reading `bwd` linearizes at, without a cascading
|
|
145
|
+
* recompute: live/last-settled value for a source or realized derived, else
|
|
146
|
+
* realize once via `.value` (PutGet holds for any source state). */
|
|
147
|
+
function backPrimal(c) {
|
|
148
|
+
if (c.getter === undefined || c.flags & F.Dirty)
|
|
149
|
+
return c.value;
|
|
150
|
+
return c.currentValue;
|
|
151
|
+
}
|
|
152
|
+
/** Create a lens-edge `child →[index] parent`, appending it to `child`'s
|
|
153
|
+
* `parentEdges` (down) eagerly at construction, in tuple order (so
|
|
154
|
+
* `parentEdges` is index-ordered). The up-list (`parent.childEdges`) is spliced
|
|
155
|
+
* lazily on first back-mark (`linkChild`), so child order is arm-order. */
|
|
156
|
+
function linkLens(child, parent, index) {
|
|
157
|
+
const e = {
|
|
158
|
+
index,
|
|
159
|
+
parent,
|
|
160
|
+
child,
|
|
161
|
+
linked: false,
|
|
162
|
+
nextParent: undefined,
|
|
163
|
+
prevChild: undefined,
|
|
164
|
+
nextChild: undefined,
|
|
165
|
+
};
|
|
166
|
+
if (child.parentEdgesTail !== undefined)
|
|
167
|
+
child.parentEdgesTail.nextParent = e;
|
|
168
|
+
else
|
|
169
|
+
child.parentEdges = e;
|
|
170
|
+
child.parentEdgesTail = e;
|
|
171
|
+
}
|
|
172
|
+
/** Splice a lens-edge into its parent's `childEdges` (the up-traversal list),
|
|
173
|
+
* once, on first back-mark — so a parent's child order is arm-order and
|
|
174
|
+
* co-writer resolution is last-write-wins. Idempotent via `linked`. */
|
|
175
|
+
function linkChild(e) {
|
|
176
|
+
if (e.linked)
|
|
177
|
+
return;
|
|
178
|
+
if (COUNTS)
|
|
179
|
+
counts.linkChild++;
|
|
180
|
+
e.linked = true;
|
|
181
|
+
const parent = e.parent;
|
|
182
|
+
e.prevChild = parent.childEdgesTail;
|
|
183
|
+
if (parent.childEdgesTail !== undefined)
|
|
184
|
+
parent.childEdgesTail.nextChild = e;
|
|
185
|
+
else
|
|
186
|
+
parent.childEdges = e;
|
|
187
|
+
parent.childEdgesTail = e;
|
|
188
|
+
}
|
|
189
|
+
/** Remove a lens-edge from its parent's `childEdges` up-list in O(1), and mark it
|
|
190
|
+
* re-linkable — the backward dual of `unlink` dropping a subscriber from `subs`.
|
|
191
|
+
* Called when a view is unwatched, to release the parent→child retaining edge (a
|
|
192
|
+
* later arm re-`linkChild`s). The child's own down-list (`parentEdges`) stays:
|
|
193
|
+
* it's intrinsic to the view and dies with it. */
|
|
194
|
+
function unlinkChild(e) {
|
|
195
|
+
if (COUNTS)
|
|
196
|
+
counts.unlinkChild++;
|
|
197
|
+
const { parent, prevChild, nextChild } = e;
|
|
198
|
+
if (nextChild !== undefined)
|
|
199
|
+
nextChild.prevChild = prevChild;
|
|
200
|
+
else
|
|
201
|
+
parent.childEdgesTail = prevChild;
|
|
202
|
+
if (prevChild !== undefined)
|
|
203
|
+
prevChild.nextChild = nextChild;
|
|
204
|
+
else
|
|
205
|
+
parent.childEdges = nextChild;
|
|
206
|
+
e.linked = false;
|
|
207
|
+
e.prevChild = undefined;
|
|
208
|
+
e.nextChild = undefined;
|
|
209
|
+
}
|
|
210
|
+
/** Precompute `BF.WriteBlocked` once, after a writable's `parentEdges` are linked.
|
|
211
|
+
* Mirrors `markDown`'s descent exactly: a sole read-only-derived parent dead-ends
|
|
212
|
+
* (block); a split routes around a read-only parent; otherwise the block is
|
|
213
|
+
* inherited from any non-read-only parent already flagged. Topology is immutable
|
|
214
|
+
* and parents are built first, so each node's bit is its parents' bits + one scan. */
|
|
215
|
+
function setWriteBlocked(cell) {
|
|
216
|
+
const pe = cell.parentEdges;
|
|
217
|
+
if (pe === undefined)
|
|
218
|
+
return; // parentless sink (pin): absorbs, never dead-ends
|
|
219
|
+
const sole = pe.nextParent === undefined;
|
|
220
|
+
for (let e = pe; e !== undefined; e = e.nextParent) {
|
|
221
|
+
const p = e.parent;
|
|
222
|
+
if (isReadOnlyDerived(p)) {
|
|
223
|
+
if (sole) {
|
|
224
|
+
cell.bflags |= BF.WriteBlocked; // markDown would throw at this dead-end
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (p.bflags & BF.WriteBlocked) {
|
|
229
|
+
cell.bflags |= BF.WriteBlocked; // a descended parent dead-ends deeper
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
73
234
|
/** Install a hook fired on every source value-change; returns a restore fn. */
|
|
74
235
|
export function setCellWriteHook(fn) {
|
|
75
236
|
const prev = writeHook;
|
|
@@ -78,6 +239,9 @@ export function setCellWriteHook(fn) {
|
|
|
78
239
|
writeHook = prev;
|
|
79
240
|
};
|
|
80
241
|
}
|
|
242
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
243
|
+
// Forward graph engine (internal) — alien-signals verbatim.
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
81
245
|
// alien-signals algorithm (verbatim): link / unlink / propagate / checkDirty.
|
|
82
246
|
function link(dep, sub, version) {
|
|
83
247
|
const prevDep = sub.depsTail;
|
|
@@ -92,6 +256,8 @@ function link(dep, sub, version) {
|
|
|
92
256
|
const prevSub = dep.subsTail;
|
|
93
257
|
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub)
|
|
94
258
|
return;
|
|
259
|
+
if (COUNTS)
|
|
260
|
+
counts.link++;
|
|
95
261
|
const isFirstSub = dep.subs === undefined;
|
|
96
262
|
const newLink = (sub.depsTail =
|
|
97
263
|
dep.subsTail =
|
|
@@ -122,6 +288,8 @@ function link(dep, sub, version) {
|
|
|
122
288
|
}
|
|
123
289
|
}
|
|
124
290
|
function unlink(l, sub = l.sub) {
|
|
291
|
+
if (COUNTS)
|
|
292
|
+
counts.unlink++;
|
|
125
293
|
const { dep, prevDep, nextDep, nextSub, prevSub } = l;
|
|
126
294
|
if (nextDep !== undefined)
|
|
127
295
|
nextDep.prevDep = prevDep;
|
|
@@ -142,13 +310,14 @@ function unlink(l, sub = l.sub) {
|
|
|
142
310
|
return nextDep;
|
|
143
311
|
}
|
|
144
312
|
function propagate(start, innerWrite, excluding) {
|
|
313
|
+
if (COUNTS)
|
|
314
|
+
counts.propagate++;
|
|
145
315
|
let l = start;
|
|
146
316
|
let next = start.nextSub;
|
|
147
317
|
let stack;
|
|
148
318
|
top: do {
|
|
149
319
|
const sub = l.sub;
|
|
150
|
-
// `excluding` skips one subscriber (
|
|
151
|
-
// writing a cell it subscribes to doesn't re-trigger itself).
|
|
320
|
+
// `excluding` skips one subscriber (a `network` not re-triggering itself).
|
|
152
321
|
if (sub !== excluding) {
|
|
153
322
|
let flags = sub.flags;
|
|
154
323
|
if (!(flags & (F.RecursedCheck | F.Recursed | F.Dirty | F.Pending))) {
|
|
@@ -199,6 +368,8 @@ function propagate(start, innerWrite, excluding) {
|
|
|
199
368
|
} while (true);
|
|
200
369
|
}
|
|
201
370
|
function checkDirty(startLink, startSub) {
|
|
371
|
+
if (COUNTS)
|
|
372
|
+
counts.checkDirty++;
|
|
202
373
|
let l = startLink, sub = startSub;
|
|
203
374
|
let stack;
|
|
204
375
|
let checkDepth = 0, dirty = false;
|
|
@@ -207,7 +378,14 @@ function checkDirty(startLink, startSub) {
|
|
|
207
378
|
const flags = dep.flags;
|
|
208
379
|
if (sub.flags & F.Dirty)
|
|
209
380
|
dirty = true;
|
|
210
|
-
else if ((flags & (F.Mutable | F.Dirty)) === (F.Mutable | F.Dirty)
|
|
381
|
+
else if ((flags & (F.Mutable | F.Dirty)) === (F.Mutable | F.Dirty) ||
|
|
382
|
+
// A back-`Pending` source looks unchanged until `_update` resolves it
|
|
383
|
+
// (pulls its views, runs the `put`s, stages it) and reports if it moved —
|
|
384
|
+
// like a `Dirty` source. That resolve can re-mark nodes on this pull's
|
|
385
|
+
// stack; the unwind below honors any such `F.Dirty`.
|
|
386
|
+
(flags & F.Mutable &&
|
|
387
|
+
dep.bflags & BF.Pending &&
|
|
388
|
+
isSource(dep))) {
|
|
211
389
|
const subs = dep.subs;
|
|
212
390
|
if (dep._update()) {
|
|
213
391
|
if (subs.nextSub !== undefined)
|
|
@@ -232,11 +410,15 @@ function checkDirty(startLink, startSub) {
|
|
|
232
410
|
while (checkDepth--) {
|
|
233
411
|
l = stack.value;
|
|
234
412
|
stack = stack.prev;
|
|
235
|
-
|
|
413
|
+
// `dirty` tracks change down this branch, but a node may have been marked
|
|
414
|
+
// `F.Dirty` independently (a stateful stash `writeBack` mid-pull) — honor
|
|
415
|
+
// that too, else we'd clear its `F.Pending` without recomputing.
|
|
416
|
+
if (dirty || sub.flags & F.Dirty) {
|
|
236
417
|
const subs = sub.subs;
|
|
237
418
|
if (sub._update()) {
|
|
238
419
|
if (subs.nextSub !== undefined)
|
|
239
420
|
shallowPropagate(subs);
|
|
421
|
+
dirty = true;
|
|
240
422
|
sub = l.sub;
|
|
241
423
|
continue;
|
|
242
424
|
}
|
|
@@ -289,101 +471,65 @@ function disposeAllDepsInReverse(sub) {
|
|
|
289
471
|
l = prev;
|
|
290
472
|
}
|
|
291
473
|
}
|
|
292
|
-
export const DIRECT_SLOT = Symbol("merge:direct-slot");
|
|
293
474
|
class MergeNode {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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;
|
|
475
|
+
foldFn;
|
|
476
|
+
/** Contributions gathered as the cone resolves; folded and cleared in `foldMerge`. */
|
|
477
|
+
contributions = [];
|
|
478
|
+
constructor(fold) {
|
|
479
|
+
this.foldFn = fold;
|
|
327
480
|
}
|
|
328
481
|
}
|
|
329
|
-
// BwdSpec — the backward sidecar
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
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`.
|
|
482
|
+
// BwdSpec — the backward sidecar, off a single `_bwd` pointer so a source/computed
|
|
483
|
+
// stays lean. Only a writable derived cell (lens / multi-out / merge / stateful /
|
|
484
|
+
// pin) carries one; writable iff `_bwd !== undefined`. The backward shape is read
|
|
485
|
+
// off field presence rather than a tag: `merge` set ⇒ fan-in fold; `stateful` set ⇒
|
|
486
|
+
// complement-carrying; no `parentEdges` ⇒ pin sink; `scatter` ⇒ tuple `put`. The
|
|
487
|
+
// one bit that isn't recoverable from topology is scalar-vs-tuple `put` (a 1-parent
|
|
488
|
+
// split still takes a tuple), hence `scatter`.
|
|
344
489
|
class BwdSpec {
|
|
345
|
-
/**
|
|
346
|
-
|
|
347
|
-
|
|
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`. */
|
|
490
|
+
/** Lens `put` (dual of `getter`): `put(target)` for 1→1 / multi-out (a
|
|
491
|
+
* source-reading lens reads its parents at walk time), `put(target, sources, c)`
|
|
492
|
+
* for stateful. `undefined` for a merge (folds) or pin (absorbs). */
|
|
351
493
|
// biome-ignore lint/suspicious/noExplicitAny: put fn is opaque shape
|
|
352
494
|
put = undefined;
|
|
353
|
-
/**
|
|
495
|
+
/** Fold payload; present ⇒ a fan-in merge. */
|
|
354
496
|
merge = undefined;
|
|
355
|
-
/** Complement
|
|
497
|
+
/** Complement state; present ⇒ a complement-carrying (stateful) lens. */
|
|
356
498
|
stateful = undefined;
|
|
357
|
-
/**
|
|
358
|
-
*
|
|
359
|
-
|
|
499
|
+
/** `put` yields a per-parent tuple (split / stateful) vs a scalar (1→1). The
|
|
500
|
+
* only discriminant not derivable from topology (a 1-parent split is a tuple). */
|
|
501
|
+
scatter = false;
|
|
360
502
|
}
|
|
361
|
-
/** Runtime state of a
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* complement and the closures that project from / advance it. */
|
|
503
|
+
/** Runtime state of a symmetric-lens complement, kept off `BwdSpec` so plain
|
|
504
|
+
* lenses don't carry its slots. See the stateful-lens header for the theory
|
|
505
|
+
* (symmetric/edit lenses) and the version-stamp provenance. */
|
|
365
506
|
class StatefulCore {
|
|
366
507
|
/** Engine-owned memory the view discards. */
|
|
367
508
|
complement;
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
fwd;
|
|
371
|
-
/** Advance the complement: `step(sources, complement, external)`. */
|
|
509
|
+
/** Advance the complement: `step(sources, complement)`. Run only when the
|
|
510
|
+
* sources actually moved (the engine gates it; see the stateful header). */
|
|
372
511
|
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
373
512
|
step;
|
|
374
|
-
/**
|
|
375
|
-
*
|
|
376
|
-
|
|
513
|
+
/** Sum of the parents' `version`s as of the last sync. Sources moved iff the
|
|
514
|
+
* live sum differs — the lazy own-vs-external provenance that replaces a value
|
|
515
|
+
* witness. A read syncs it after stepping; a back-write re-stamps it post-order
|
|
516
|
+
* (own writes don't re-step, so `bwd` must leave the complement consistent).
|
|
517
|
+
* Seeded to `-1` (sums are ≥ 0) so the first use always folds the sources in. */
|
|
518
|
+
stamp = -1;
|
|
377
519
|
constructor(complement,
|
|
378
|
-
// biome-ignore lint/suspicious/noExplicitAny: opaque fwd shape
|
|
379
|
-
fwd,
|
|
380
520
|
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
381
521
|
step) {
|
|
382
522
|
this.complement = complement;
|
|
383
|
-
this.fwd = fwd;
|
|
384
523
|
this.step = step;
|
|
385
524
|
}
|
|
386
525
|
}
|
|
526
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
527
|
+
// Public API — sentinels, read/write shapes, and value-coercion helpers.
|
|
528
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
529
|
+
/** Multi-out / stateful back-write sentinel: "leave this parent untouched."
|
|
530
|
+
* Every non-`SKIP` slot is written verbatim, `undefined` included; a short array
|
|
531
|
+
* skips the trailing parents. (1→1 `put` always writes its one parent.) */
|
|
532
|
+
export const SKIP = Symbol("bireactive.SKIP");
|
|
387
533
|
/** Snapshot a `Val<T>` to plain `T` (one-shot, no tracking). */
|
|
388
534
|
export function readNow(v) {
|
|
389
535
|
if (v instanceof Cell)
|
|
@@ -413,35 +559,58 @@ export const isCell = (v) => v instanceof Cell;
|
|
|
413
559
|
export const isLens = (v) => v instanceof Cell && v.getter !== undefined && v._bwd !== undefined;
|
|
414
560
|
/** Read-only mode: derived with no backward path. */
|
|
415
561
|
export const isReadonly = (v) => v instanceof Cell && v.getter !== undefined && v._bwd === undefined;
|
|
562
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
563
|
+
// Public API — the Cell class (the one user-facing reactive primitive).
|
|
564
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
416
565
|
export class Cell {
|
|
417
|
-
|
|
566
|
+
/** @internal */
|
|
567
|
+
flags;
|
|
568
|
+
/** @internal */
|
|
418
569
|
subs;
|
|
570
|
+
/** @internal */
|
|
419
571
|
subsTail;
|
|
572
|
+
/** @internal */
|
|
420
573
|
deps;
|
|
574
|
+
/** @internal */
|
|
421
575
|
depsTail;
|
|
422
|
-
/** Forward derivation (computed/lens/merge). `undefined` ⇒ source. */
|
|
576
|
+
/** @internal Forward derivation (computed/lens/merge). `undefined` ⇒ source. */
|
|
423
577
|
getter;
|
|
424
|
-
/** Per-instance equality
|
|
425
|
-
* construction) so hot paths call it without an `undefined` branch. */
|
|
578
|
+
/** @internal Per-instance equality; always defined (defaults to `Object.is`). */
|
|
426
579
|
_equals;
|
|
427
|
-
/** First-subscriber / last-subscriber lifecycle hooks. */
|
|
580
|
+
/** @internal First-subscriber / last-subscriber lifecycle hooks. */
|
|
428
581
|
_watched;
|
|
582
|
+
/** @internal */
|
|
429
583
|
_unwatchedHook;
|
|
430
|
-
/** Source:
|
|
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. */
|
|
584
|
+
/** @internal Source: committed value + staged write. */
|
|
434
585
|
currentValue;
|
|
586
|
+
/** @internal */
|
|
435
587
|
pendingValue;
|
|
436
|
-
/** Backward sidecar
|
|
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`. */
|
|
588
|
+
/** @internal Backward sidecar; `undefined` iff read-only. Writability is `_bwd !== undefined`. */
|
|
440
589
|
_bwd;
|
|
590
|
+
/** @internal Lens-edges to my back-targets (down); dual of `deps`. `markDown`/
|
|
591
|
+
* `backResolve` descend this toward sources. Index-ordered. */
|
|
592
|
+
parentEdges;
|
|
593
|
+
/** @internal */
|
|
594
|
+
parentEdgesTail;
|
|
595
|
+
/** @internal Lens-edges to my lens-children (up); dual of `subs`. `resolveCone`
|
|
596
|
+
* ascends this toward the armed views. */
|
|
597
|
+
childEdges;
|
|
598
|
+
/** @internal */
|
|
599
|
+
childEdgesTail;
|
|
600
|
+
/** @internal Backward flag word (`BF`), dual of forward `flags`. */
|
|
601
|
+
bflags;
|
|
602
|
+
/** @internal Visit epoch for `backResolve`'s collect phase (dedups diamonds
|
|
603
|
+
* without a Set; compared against the global `backCycle`). */
|
|
604
|
+
bEpoch;
|
|
605
|
+
/** @internal Monotone committed-change counter. A stateful lens sums its
|
|
606
|
+
* parents' versions to detect "did my sources move since I last synced?" —
|
|
607
|
+
* the lazy provenance that replaces a value witness (see the stateful header). */
|
|
608
|
+
version;
|
|
609
|
+
/** Optional debug label (`cell(0, { name })`); used by errors and graph dumps. */
|
|
610
|
+
name;
|
|
611
|
+
// Every slot assigned once, in declaration order, for a stable V8 hidden class.
|
|
441
612
|
constructor(initial, opts) {
|
|
442
|
-
this.
|
|
443
|
-
this.pendingValue = initial;
|
|
444
|
-
// Pre-init every optional slot for a stable V8 hidden class across variants.
|
|
613
|
+
this.flags = F.Mutable;
|
|
445
614
|
this.subs = undefined;
|
|
446
615
|
this.subsTail = undefined;
|
|
447
616
|
this.deps = undefined;
|
|
@@ -450,7 +619,17 @@ export class Cell {
|
|
|
450
619
|
this._equals = Object.is;
|
|
451
620
|
this._watched = undefined;
|
|
452
621
|
this._unwatchedHook = undefined;
|
|
622
|
+
this.currentValue = initial;
|
|
623
|
+
this.pendingValue = initial;
|
|
453
624
|
this._bwd = undefined;
|
|
625
|
+
this.parentEdges = undefined;
|
|
626
|
+
this.parentEdgesTail = undefined;
|
|
627
|
+
this.childEdges = undefined;
|
|
628
|
+
this.childEdgesTail = undefined;
|
|
629
|
+
this.bflags = BF.None;
|
|
630
|
+
this.bEpoch = 0;
|
|
631
|
+
this.version = 0;
|
|
632
|
+
this.name = undefined;
|
|
454
633
|
if (opts !== undefined) {
|
|
455
634
|
if (opts.equals !== undefined)
|
|
456
635
|
this._equals = opts.equals;
|
|
@@ -458,32 +637,37 @@ export class Cell {
|
|
|
458
637
|
this._watched = opts.watched;
|
|
459
638
|
if (opts.unwatched !== undefined)
|
|
460
639
|
this._unwatchedHook = opts.unwatched;
|
|
640
|
+
if (opts.name !== undefined)
|
|
641
|
+
this.name = opts.name;
|
|
461
642
|
}
|
|
462
643
|
}
|
|
463
|
-
|
|
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. */
|
|
644
|
+
/** @internal Single write-commit point; self-excludes the active network. */
|
|
470
645
|
_writeSource(next) {
|
|
646
|
+
// Resolve any pending back-write first, so the later forward write wins (LWW).
|
|
647
|
+
if (this.bflags & BF.Pending && !draining)
|
|
648
|
+
backResolve(this);
|
|
471
649
|
const prev = this.pendingValue;
|
|
472
650
|
this.pendingValue = next;
|
|
473
651
|
if (!this._equals(prev, next)) {
|
|
652
|
+
this.version++; // stamp the change for stateful-lens provenance (sum-of-versions)
|
|
474
653
|
this.flags = F.Mutable | F.Dirty;
|
|
475
654
|
if (writeHook !== undefined)
|
|
476
655
|
writeHook(this);
|
|
477
656
|
const subs = this.subs;
|
|
478
|
-
if (subs !== undefined)
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
657
|
+
if (subs !== undefined) {
|
|
658
|
+
// Convert the cone's arm-time `Pending` into `Dirty` so a second observer
|
|
659
|
+
// (not just the first reader) sees the change. If this lands mid-pull, the
|
|
660
|
+
// freshly-`Dirty` nodes are honored by `checkDirty`'s unwind.
|
|
661
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
662
|
+
autoFlush();
|
|
663
|
+
}
|
|
482
664
|
}
|
|
483
665
|
}
|
|
666
|
+
/** @internal */
|
|
484
667
|
_update() {
|
|
485
668
|
if (this.getter !== undefined) {
|
|
486
|
-
|
|
669
|
+
if (COUNTS)
|
|
670
|
+
counts.recompute++;
|
|
487
671
|
this.depsTail = undefined;
|
|
488
672
|
this.flags = F.Mutable | F.RecursedCheck;
|
|
489
673
|
const prev = activeSub;
|
|
@@ -494,7 +678,10 @@ export class Cell {
|
|
|
494
678
|
const old = this.currentValue;
|
|
495
679
|
const next = (this.currentValue = this.getter());
|
|
496
680
|
threw = false;
|
|
497
|
-
|
|
681
|
+
const changed = !this._equals(old, next);
|
|
682
|
+
if (changed)
|
|
683
|
+
this.version++; // derived commit: stamp for stateful provenance
|
|
684
|
+
return changed;
|
|
498
685
|
}
|
|
499
686
|
finally {
|
|
500
687
|
activeSub = prev;
|
|
@@ -502,13 +689,30 @@ export class Cell {
|
|
|
502
689
|
purgeDeps(this);
|
|
503
690
|
}
|
|
504
691
|
}
|
|
692
|
+
// A back-`Pending` source resolves its armed back-write first, so
|
|
693
|
+
// `pendingValue` reflects it before we commit.
|
|
694
|
+
if (this.bflags & BF.Pending && !draining)
|
|
695
|
+
backResolve(this);
|
|
505
696
|
this.flags = F.Mutable;
|
|
506
697
|
const prevV = this.currentValue;
|
|
507
698
|
this.currentValue = this.pendingValue;
|
|
508
699
|
return !this._equals(prevV, this.currentValue);
|
|
509
700
|
}
|
|
701
|
+
/** @internal */
|
|
510
702
|
_notify() { }
|
|
703
|
+
/** @internal */
|
|
511
704
|
_unwatched() {
|
|
705
|
+
// Backward dual of `unlink` clearing us from each parent's `subs`: release
|
|
706
|
+
// the parent→child retaining edge (the `childEdges` up-list) so a disposed
|
|
707
|
+
// view stops being pinned by a long-lived source. Our own down-list
|
|
708
|
+
// (`parentEdges`) stays — a later arm re-links via `markDown`. Skip a still
|
|
709
|
+
// back-marked view (a pending write needs its edge); rare and bounded.
|
|
710
|
+
if (!(this.bflags & BACK_MARKED)) {
|
|
711
|
+
for (let e = this.parentEdges; e !== undefined; e = e.nextParent) {
|
|
712
|
+
if (e.linked)
|
|
713
|
+
unlinkChild(e);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
512
716
|
if (this.getter !== undefined && this.depsTail !== undefined) {
|
|
513
717
|
this.flags = F.Mutable | F.Dirty;
|
|
514
718
|
disposeAllDepsInReverse(this);
|
|
@@ -527,28 +731,32 @@ export class Cell {
|
|
|
527
731
|
activeSub = prev;
|
|
528
732
|
}
|
|
529
733
|
}
|
|
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
734
|
// Construction helpers build via `new this()` so a subclass static
|
|
535
735
|
// (`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
736
|
/** Endomorphic lens. A 2-arg `bwd(view, current)` consults the current
|
|
539
737
|
* source; a 1-arg `bwd(view)` reconstructs it from the view alone. */
|
|
540
738
|
lens(fwd, bwd) {
|
|
541
|
-
return
|
|
739
|
+
return buildLens(this.constructor, [this, fwd, bwd]);
|
|
542
740
|
}
|
|
543
741
|
/** Read-only same-type view: the RO dual of the endo `.lens`. For a cross-type view use the typed static
|
|
544
742
|
* `Target.derive(src, fn)`. */
|
|
545
743
|
derive(fn) {
|
|
546
744
|
return buildDerived(this.constructor, () => fn(this.value));
|
|
547
745
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
746
|
+
// biome-ignore lint/suspicious/noExplicitAny: heterogeneous optic chain
|
|
747
|
+
through(...optics) {
|
|
748
|
+
// Fold via each optic's own `through` (no import of optic.ts → no cycle).
|
|
749
|
+
const o = optics.length === 1 ? optics[0] : optics.reduce((a, b) => a.through(b));
|
|
750
|
+
// Preserve put arity: a source-reading optic binds 2-arg; an iso binds 1-arg
|
|
751
|
+
// (reconstruct, no source read), matching `lens`'s `bwd.length` dispatch.
|
|
752
|
+
const bwd = o.readsSource
|
|
753
|
+
? (target, cur) => o.put(target, cur)
|
|
754
|
+
: (target) => o.put(target, undefined);
|
|
755
|
+
return lens(this, o.get, bwd);
|
|
756
|
+
}
|
|
757
|
+
/** Backward fan-in: forward the identity view of its parent, backward the point
|
|
758
|
+
* where N contributors fold into one value. `fold` defaults to last-writer-wins. */
|
|
759
|
+
merge(fold) {
|
|
552
760
|
if (this.getter !== undefined && this._bwd === undefined) {
|
|
553
761
|
throw new TypeError("merge: receiver is read-only");
|
|
554
762
|
}
|
|
@@ -557,28 +765,27 @@ export class Cell {
|
|
|
557
765
|
cell.flags = F.Mutable | F.Dirty;
|
|
558
766
|
cell.getter = () => parent.value;
|
|
559
767
|
const b = (cell._bwd = new BwdSpec());
|
|
560
|
-
b.
|
|
561
|
-
|
|
768
|
+
b.merge = new MergeNode(fold);
|
|
769
|
+
linkLens(cell, parent, 0);
|
|
770
|
+
setWriteBlocked(cell);
|
|
562
771
|
return cell;
|
|
563
772
|
}
|
|
564
773
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
565
774
|
static derive(...args) {
|
|
566
|
-
return
|
|
775
|
+
return buildDerive(this, args);
|
|
567
776
|
}
|
|
568
777
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
569
778
|
static lens(...args) {
|
|
570
|
-
return
|
|
779
|
+
return buildLens(this, args);
|
|
571
780
|
}
|
|
572
781
|
/** Type predicate against this class: `Vec.is(x)` narrows `x` to `Vec`.
|
|
573
782
|
* Inherited static; works for any subclass via polymorphic `this`. */
|
|
574
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
575
783
|
static is(v) {
|
|
576
784
|
return v instanceof this;
|
|
577
785
|
}
|
|
578
|
-
/**
|
|
786
|
+
/** Coerce `Val<Inner<Cls>>` → `Cls`: instance → identity, RO cell →
|
|
579
787
|
* tracked `derive`, literal → fresh seed. */
|
|
580
|
-
|
|
581
|
-
static from(v) {
|
|
788
|
+
static coerce(v) {
|
|
582
789
|
if (v instanceof this)
|
|
583
790
|
return v;
|
|
584
791
|
if (v instanceof Cell) {
|
|
@@ -589,32 +796,37 @@ export class Cell {
|
|
|
589
796
|
}
|
|
590
797
|
/** Writable-shaped constant: always reads `v`, absorbs writes
|
|
591
798
|
* (parentless sink lens), for APIs demanding bidirectionality. */
|
|
592
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
593
799
|
static pin(v) {
|
|
594
800
|
const cell = new this();
|
|
595
801
|
cell.flags = F.Mutable | F.Dirty;
|
|
596
802
|
cell.getter = () => v;
|
|
597
|
-
|
|
598
|
-
|
|
803
|
+
// Parentless `_bwd`: `writeBack` absorbs it (no parent edges, no closure).
|
|
804
|
+
cell._bwd = new BwdSpec();
|
|
599
805
|
return cell;
|
|
600
806
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (ro) {
|
|
613
|
-
return buildDerived(ctor, () => get(parent.value));
|
|
614
|
-
}
|
|
615
|
-
// Spread-replace reads the current source ⇒ source-reading (lens) form.
|
|
616
|
-
return buildLens1(ctor, parent, get, (v, s) => ({ ...s, [key]: v }), true);
|
|
807
|
+
}
|
|
808
|
+
/** Typed field lens onto `parent.value[key]`. RO parent → RO derive;
|
|
809
|
+
* writable parent → bidirectional lens with spread-replace `put`. */
|
|
810
|
+
export function fieldOf(
|
|
811
|
+
// biome-ignore lint/suspicious/noExplicitAny: parent is contravariant on put
|
|
812
|
+
parent, key, Cls) {
|
|
813
|
+
const ctor = Cls;
|
|
814
|
+
const get = (s) => s[key];
|
|
815
|
+
const ro = parent.getter !== undefined && parent._bwd === undefined;
|
|
816
|
+
if (ro) {
|
|
817
|
+
return buildDerived(ctor, () => get(parent.value));
|
|
617
818
|
}
|
|
819
|
+
// Spread-replace put, array-aware: cloning an array with object spread would
|
|
820
|
+
// demote it to a plain record, so copy via `slice` and set the index.
|
|
821
|
+
const put = (v, s) => {
|
|
822
|
+
if (Array.isArray(s)) {
|
|
823
|
+
const next = s.slice();
|
|
824
|
+
next[key] = v;
|
|
825
|
+
return next;
|
|
826
|
+
}
|
|
827
|
+
return { ...s, [key]: v };
|
|
828
|
+
};
|
|
829
|
+
return buildLens(ctor, [parent, get, put]);
|
|
618
830
|
}
|
|
619
831
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
620
832
|
function buildDerived(Cls, getter) {
|
|
@@ -623,40 +835,95 @@ function buildDerived(Cls, getter) {
|
|
|
623
835
|
cell.flags = F.Mutable | F.Dirty;
|
|
624
836
|
return cell;
|
|
625
837
|
}
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
cell.getter = (() => fwd(parent.value));
|
|
631
|
-
const b = (cell._bwd = new BwdSpec());
|
|
632
|
-
// Source-reading lenses bake the (non-committing) current source into the
|
|
633
|
-
// closure so the engine always calls the 1-arg form (no arity branch).
|
|
634
|
-
b.put = readsSource ? (t) => bwd(t, settled(parent)) : bwd;
|
|
635
|
-
b.parent = parent;
|
|
636
|
-
return cell;
|
|
637
|
-
}
|
|
638
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
639
|
-
function buildLensN(Cls, parents, fwd, bwd, readsSource) {
|
|
838
|
+
// Shared N-input read getter: refill a construction-owned buffer from the parents
|
|
839
|
+
// each read (no per-read alloc), then apply `fwd`. Identical hot closure whether
|
|
840
|
+
// the node is a read-only derive-N or a writable split lens.
|
|
841
|
+
function arrayGetter(parents, fwd) {
|
|
640
842
|
const n = parents.length;
|
|
641
843
|
const vals = new Array(n);
|
|
642
|
-
|
|
643
|
-
cell.flags = F.Mutable | F.Dirty;
|
|
644
|
-
cell.getter = (() => {
|
|
844
|
+
return () => {
|
|
645
845
|
for (let i = 0; i < n; i++)
|
|
646
846
|
vals[i] = parents[i].value;
|
|
647
847
|
return fwd(vals);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
// One writable-lens constructor for both shapes. The `Array.isArray` branch is paid
|
|
851
|
+
// once at construction and installs the matching specialized hot closures — scalar
|
|
852
|
+
// scalar `getter`/`put` for 1→1, the buffer-loop getter + tuple `put` (`scatter`)
|
|
853
|
+
// for N→M — so neither hot path changes. A 2-arg call is the
|
|
854
|
+
// complement-carrying form and routes to `buildStateful`. `bwd` is always present
|
|
855
|
+
// (a read-only N view is a `derive`, built via `buildDerive`).
|
|
856
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch over the untyped call forms
|
|
857
|
+
function buildLens(Cls, args) {
|
|
858
|
+
const parent0 = args[0];
|
|
859
|
+
if (args.length === 2) {
|
|
860
|
+
return Array.isArray(parent0)
|
|
861
|
+
? buildStateful(Cls, parent0, args[1])
|
|
862
|
+
: buildStateful1(Cls, parent0, args[1]);
|
|
863
|
+
}
|
|
864
|
+
let parent = parent0;
|
|
865
|
+
let fwd = args[1];
|
|
866
|
+
let bwd = args[2];
|
|
867
|
+
// Object-keyed parents → rewrite to the positional array form (key order
|
|
868
|
+
// fixed once; omitted backward keys become SKIP). The tuple fast path below
|
|
869
|
+
// is untouched.
|
|
870
|
+
if (parent0 !== null &&
|
|
871
|
+
typeof parent0 === "object" &&
|
|
872
|
+
!Array.isArray(parent0) &&
|
|
873
|
+
!(parent0 instanceof Cell)) {
|
|
874
|
+
const keys = Object.keys(parent0);
|
|
875
|
+
const rec = parent0;
|
|
876
|
+
const fwdObj = fwd;
|
|
877
|
+
const bwdObj = bwd;
|
|
878
|
+
const toObj = (vals) => {
|
|
879
|
+
const o = {};
|
|
880
|
+
for (let i = 0; i < keys.length; i++)
|
|
881
|
+
o[keys[i]] = vals[i];
|
|
882
|
+
return o;
|
|
883
|
+
};
|
|
884
|
+
parent = keys.map(k => rec[k]);
|
|
885
|
+
fwd = (vals) => fwdObj(toObj(vals));
|
|
886
|
+
bwd = ((t, vals) => {
|
|
887
|
+
const o = bwdObj(t, toObj(vals));
|
|
888
|
+
return keys.map(k => (k in o ? o[k] : SKIP));
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
const readsSource = bwd.length >= 2;
|
|
892
|
+
const cell = new Cls();
|
|
893
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
651
894
|
const b = (cell._bwd = new BwdSpec());
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
895
|
+
if (Array.isArray(parent)) {
|
|
896
|
+
const parents = parent;
|
|
897
|
+
const n = parents.length;
|
|
898
|
+
cell.getter = arrayGetter(parents, fwd);
|
|
899
|
+
b.scatter = true;
|
|
900
|
+
for (let i = 0; i < n; i++)
|
|
901
|
+
linkLens(cell, parents[i], i);
|
|
902
|
+
if (readsSource) {
|
|
903
|
+
// Own reused buffer (not the getter's) to avoid aliasing; `bwd` consumes it
|
|
904
|
+
// synchronously and must not retain it.
|
|
905
|
+
const argbuf = new Array(n);
|
|
906
|
+
const bwdN = bwd;
|
|
907
|
+
b.put = (target) => {
|
|
908
|
+
for (let i = 0; i < n; i++)
|
|
909
|
+
argbuf[i] = backPrimal(parents[i]);
|
|
910
|
+
return bwdN(target, argbuf);
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
const bwd0 = bwd;
|
|
915
|
+
b.put = (target) => bwd0(target);
|
|
658
916
|
}
|
|
659
|
-
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
const p = parent;
|
|
920
|
+
cell.getter = (() => fwd(p.value));
|
|
921
|
+
// Source-reading lenses linearize at the parent's primal (`backPrimal`), so the
|
|
922
|
+
// engine always calls the 1-arg form and never recomputes the parent's cone.
|
|
923
|
+
b.put = readsSource ? (t) => bwd(t, backPrimal(p)) : bwd;
|
|
924
|
+
linkLens(cell, p, 0);
|
|
925
|
+
}
|
|
926
|
+
setWriteBlocked(cell);
|
|
660
927
|
return cell;
|
|
661
928
|
}
|
|
662
929
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
@@ -671,35 +938,41 @@ spec) {
|
|
|
671
938
|
const seed = new Array(n);
|
|
672
939
|
for (let i = 0; i < n; i++)
|
|
673
940
|
seed[i] = parents[i].peek();
|
|
674
|
-
|
|
941
|
+
// Default `step` is the memoryless refresh (`init`); the engine runs it only on
|
|
942
|
+
// an outside change, so the `external ? init(s) : c` idiom needs no user `step`.
|
|
943
|
+
const init = spec.init;
|
|
944
|
+
const step = (spec.step ?? init);
|
|
945
|
+
const sc = (b.stateful = new StatefulCore(spec.init(seed), step));
|
|
946
|
+
// Sentinel: version sums are ≥ 0, so the first read (or back-write) always
|
|
947
|
+
// steps once, folding the initial sources into the seed complement. The
|
|
948
|
+
// `init`/`step` split is seed-then-fold: `init` need not see the sources.
|
|
949
|
+
sc.stamp = -1;
|
|
950
|
+
const fwd = spec.fwd;
|
|
675
951
|
b.put = spec.bwd;
|
|
676
|
-
b.
|
|
952
|
+
b.scatter = true;
|
|
953
|
+
for (let i = 0; i < n; i++)
|
|
954
|
+
linkLens(cell, parents[i], i);
|
|
677
955
|
cell.getter = (() => {
|
|
678
|
-
|
|
956
|
+
let ver = 0;
|
|
957
|
+
for (let i = 0; i < n; i++) {
|
|
679
958
|
vals[i] = parents[i].value;
|
|
680
|
-
|
|
681
|
-
// back-write.
|
|
682
|
-
let external = true;
|
|
683
|
-
const lb = sc.lastBwd;
|
|
684
|
-
if (lb !== undefined) {
|
|
685
|
-
external = false;
|
|
686
|
-
for (let i = 0; i < n; i++) {
|
|
687
|
-
if (vals[i] !== lb[i]) {
|
|
688
|
-
external = true;
|
|
689
|
-
break;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
959
|
+
ver += parents[i].version;
|
|
692
960
|
}
|
|
693
|
-
|
|
694
|
-
|
|
961
|
+
// Step only when sources moved since the last sync (lazy own-vs-external).
|
|
962
|
+
if (ver !== sc.stamp) {
|
|
963
|
+
if (COUNTS)
|
|
964
|
+
counts.step++;
|
|
965
|
+
sc.complement = sc.step(vals, sc.complement);
|
|
966
|
+
sc.stamp = ver;
|
|
967
|
+
}
|
|
968
|
+
return fwd(vals, sc.complement);
|
|
695
969
|
});
|
|
970
|
+
setWriteBlocked(cell);
|
|
696
971
|
return cell;
|
|
697
972
|
}
|
|
698
|
-
// Single-source stateful
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
// `vals` still feeds the closures and `b.parent` stays an array for the
|
|
702
|
-
// shared split backward path.
|
|
973
|
+
// Single-source stateful fast-path: one parent, so no `vals` buffer and a scalar
|
|
974
|
+
// `step`/`fwd`/`bwd` — the version stamp is just the parent's `version`. Same
|
|
975
|
+
// provenance and laziness as the N-source `buildStateful`, minus the array work.
|
|
703
976
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
704
977
|
function buildStateful1(Cls, parent,
|
|
705
978
|
// biome-ignore lint/suspicious/noExplicitAny: opaque spec
|
|
@@ -707,52 +980,54 @@ spec) {
|
|
|
707
980
|
const cell = new Cls();
|
|
708
981
|
cell.flags = F.Mutable | F.Dirty;
|
|
709
982
|
const b = (cell._bwd = new BwdSpec());
|
|
710
|
-
const
|
|
983
|
+
const init = spec.init;
|
|
984
|
+
const step = (spec.step ?? init);
|
|
985
|
+
const sc = (b.stateful = new StatefulCore(init(parent.peek()), step));
|
|
986
|
+
sc.stamp = -1; // sentinel: first use folds the source in (see `buildStateful`)
|
|
987
|
+
const fwd = spec.fwd;
|
|
711
988
|
b.put = spec.bwd;
|
|
712
|
-
|
|
713
|
-
|
|
989
|
+
// `scatter` stays false: writeBack routes this through the scalar stateful branch.
|
|
990
|
+
linkLens(cell, parent, 0);
|
|
714
991
|
cell.getter = (() => {
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
992
|
+
const x = parent.value;
|
|
993
|
+
const ver = parent.version;
|
|
994
|
+
if (ver !== sc.stamp) {
|
|
995
|
+
if (COUNTS)
|
|
996
|
+
counts.step++;
|
|
997
|
+
sc.complement = sc.step(x, sc.complement);
|
|
998
|
+
sc.stamp = ver;
|
|
999
|
+
}
|
|
1000
|
+
return fwd(x, sc.complement);
|
|
720
1001
|
});
|
|
1002
|
+
setWriteBlocked(cell);
|
|
721
1003
|
return cell;
|
|
722
1004
|
}
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
function dispatchDerive(ctor, args) {
|
|
1005
|
+
// One read-only-derive constructor: a bare closure (`derive(fn)`), a single tracked
|
|
1006
|
+
// read (`derive(p, fn)`), or an N-parent read (`derive(ps, fn)`) — each lands in
|
|
1007
|
+
// `buildDerived` with the matching getter. (Writable `lens(...)` is `buildLens`;
|
|
1008
|
+
// statics pass the typed subclass, free functions plain `Cell`, so neither drifts.)
|
|
1009
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch over the untyped call forms
|
|
1010
|
+
function buildDerive(Cls, args) {
|
|
730
1011
|
if (args.length === 1)
|
|
731
|
-
return buildDerived(
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
return buildLensN(ctor, parent, fn, undefined, false);
|
|
735
|
-
return buildDerived(ctor, () => fn(parent.value));
|
|
736
|
-
}
|
|
737
|
-
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
738
|
-
function dispatchLens(ctor, args) {
|
|
739
|
-
const [parent, a, b] = args;
|
|
740
|
-
if (args.length === 2) {
|
|
741
|
-
const ps = Array.isArray(parent) ? parent : [parent];
|
|
742
|
-
return ps.length === 1 ? buildStateful1(ctor, ps[0], a) : buildStateful(ctor, ps, a);
|
|
743
|
-
}
|
|
744
|
-
const readsSource = b.length >= 2;
|
|
1012
|
+
return buildDerived(Cls, args[0]);
|
|
1013
|
+
const parent = args[0];
|
|
1014
|
+
const fn = args[1];
|
|
745
1015
|
if (Array.isArray(parent))
|
|
746
|
-
return
|
|
747
|
-
return
|
|
1016
|
+
return buildDerived(Cls, arrayGetter(parent, fn));
|
|
1017
|
+
return buildDerived(Cls, () => fn(parent.value));
|
|
748
1018
|
}
|
|
749
|
-
//
|
|
1019
|
+
// Installed on the prototype (not a class accessor): V8 JITs it better and keeps
|
|
1020
|
+
// the field-only class shape for a stable hidden class.
|
|
750
1021
|
Object.defineProperty(Cell.prototype, "value", {
|
|
751
1022
|
get() {
|
|
1023
|
+
// Reading is the PULL: a back-marked cell resolves here, before its own
|
|
1024
|
+
// compute, so a source-reading `put` never re-enters a half-computed cell.
|
|
1025
|
+
if (this.bflags & BACK_MARKED && !draining)
|
|
1026
|
+
backResolve(this);
|
|
752
1027
|
const flags = this.flags;
|
|
753
1028
|
if (this.getter !== undefined) {
|
|
754
1029
|
if (flags & F.RecursedCheck) {
|
|
755
|
-
throw new RangeError(`Cyclic computed: ${this.constructor.name ?? "?"} read its own value`);
|
|
1030
|
+
throw new RangeError(`Cyclic computed: ${this.name ?? this.constructor.name ?? "?"} read its own value`);
|
|
756
1031
|
}
|
|
757
1032
|
if (flags & F.Dirty ||
|
|
758
1033
|
(flags & F.Pending &&
|
|
@@ -763,26 +1038,11 @@ Object.defineProperty(Cell.prototype, "value", {
|
|
|
763
1038
|
shallowPropagate(subs);
|
|
764
1039
|
}
|
|
765
1040
|
}
|
|
766
|
-
else if (!flags) {
|
|
767
|
-
// First read: lazy init.
|
|
768
|
-
this.flags = F.Mutable | F.RecursedCheck;
|
|
769
|
-
const prev = activeSub;
|
|
770
|
-
activeSub = this;
|
|
771
|
-
let threw = true;
|
|
772
|
-
try {
|
|
773
|
-
this.currentValue = this.getter();
|
|
774
|
-
threw = false;
|
|
775
|
-
}
|
|
776
|
-
finally {
|
|
777
|
-
activeSub = prev;
|
|
778
|
-
this.flags = threw ? F.Mutable | F.Dirty : this.flags & ~F.RecursedCheck;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
1041
|
if (activeSub !== undefined)
|
|
782
1042
|
link(this, activeSub, cycle);
|
|
783
1043
|
return this.currentValue;
|
|
784
1044
|
}
|
|
785
|
-
//
|
|
1045
|
+
// Source path.
|
|
786
1046
|
if (flags & F.Dirty) {
|
|
787
1047
|
this.flags = F.Mutable;
|
|
788
1048
|
const prevV = this.currentValue;
|
|
@@ -802,248 +1062,441 @@ Object.defineProperty(Cell.prototype, "value", {
|
|
|
802
1062
|
this._writeSource(next);
|
|
803
1063
|
return;
|
|
804
1064
|
}
|
|
805
|
-
// Backward write. Deferred while batching/flushing so repeated writes
|
|
806
|
-
// coalesce (last-write-wins) and merge folds wait for all
|
|
807
|
-
// contributors; eager + synchronous otherwise. Exception: inside a
|
|
808
|
-
// network body writes are eager so the body's fixpoint loop observes
|
|
809
|
-
// its own edits via `peek()` between steps.
|
|
810
1065
|
const b = this._bwd;
|
|
811
1066
|
if (b === undefined) {
|
|
812
1067
|
throw new TypeError("Cannot write to a computed");
|
|
813
1068
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
this._enqueueBwd();
|
|
819
|
-
else
|
|
820
|
-
bwdUntracked(this, undefined, false);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
if (deferred && (Array.isArray(b.parent) || b.stateful !== undefined)) {
|
|
824
|
-
// Multi-parent / stateful: defer to flush so a split coalesces, a
|
|
825
|
-
// merge folds after all contributors land, and a complement steps
|
|
826
|
-
// once. Reuse `pendingValue` (unused by a getter's forward path) as
|
|
827
|
-
// the deferred backward target; drained by flush. Entry no-op vs the
|
|
828
|
-
// current view (GetPut) skips the walk.
|
|
829
|
-
if (this._equals(next, this.peek()))
|
|
830
|
-
return;
|
|
831
|
-
this.pendingValue = next;
|
|
832
|
-
this._enqueueBwd();
|
|
1069
|
+
// GetPut for a multi-parent split: absorb a write equal to the current view
|
|
1070
|
+
// (its `put` could redistribute sources past per-source equality). Stateful
|
|
1071
|
+
// excluded (`scatter` but `stateful` set) — peeking would step its complement.
|
|
1072
|
+
if (b.scatter && b.stateful === undefined && this._equals(next, this.peek())) {
|
|
833
1073
|
return;
|
|
834
1074
|
}
|
|
835
|
-
|
|
836
|
-
// stages the source (Dirty + pending) and defers only the flush, so
|
|
837
|
-
// the view reads back consistently and a later write supersedes via
|
|
838
|
-
// the source's pending value — last-write-wins, no queue, no lost
|
|
839
|
-
// revert. We must NOT peek the view here when batching: that would
|
|
840
|
-
// commit the source's pending value and break net-zero revert
|
|
841
|
-
// coalescing — `propagateBwd`'s `settled` no-op stop prunes a true
|
|
842
|
-
// no-op without committing. Outside a batch, the O(1) GetPut check is
|
|
843
|
-
// safe (no source is staged) and worth keeping.
|
|
844
|
-
if (!deferred && this._equals(next, this.peek()))
|
|
845
|
-
return;
|
|
846
|
-
bwdUntracked(this, next, deferred);
|
|
1075
|
+
arm(this, next);
|
|
847
1076
|
},
|
|
848
1077
|
enumerable: false,
|
|
849
1078
|
configurable: false,
|
|
850
1079
|
});
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
activeSub = undefined;
|
|
865
|
-
try {
|
|
866
|
-
propagateBwd(cell, target, deferred);
|
|
1080
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1081
|
+
// Backward graph engine (internal) — arm / markDown / resolveCone / writeBack.
|
|
1082
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1083
|
+
/** Backward push: arm a back-write of `target` on view `node` (dual of a source
|
|
1084
|
+
* `set`). A re-write of a still-armed view keeps only the last target (the path
|
|
1085
|
+
* is already marked); `autoFlush` wakes the effects the push woke. */
|
|
1086
|
+
function arm(node, target) {
|
|
1087
|
+
// Structural reject first: a write whose back-spine dead-ends throws before
|
|
1088
|
+
// touching any backward state (atomic — nothing armed, nothing marked).
|
|
1089
|
+
if (node.bflags & BF.WriteBlocked) {
|
|
1090
|
+
if (COUNTS)
|
|
1091
|
+
counts.armBlocked++;
|
|
1092
|
+
throw new TypeError("Cannot write through to a computed");
|
|
867
1093
|
}
|
|
868
|
-
|
|
869
|
-
|
|
1094
|
+
if (COUNTS)
|
|
1095
|
+
counts.arm++;
|
|
1096
|
+
if (!(node.bflags & BF.Dirty)) {
|
|
1097
|
+
markDown(node); // flag path + wake cones FIRST (a throw arms nothing)
|
|
1098
|
+
node.bflags |= BF.Dirty;
|
|
870
1099
|
}
|
|
1100
|
+
node.pendingValue = target;
|
|
1101
|
+
autoFlush();
|
|
871
1102
|
}
|
|
872
|
-
/**
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1103
|
+
/** MARK (push), dual of `propagate`: descend `start`'s static back-path down
|
|
1104
|
+
* `parentEdges` to its sources, flag each `BF.Pending`, and wake every source's
|
|
1105
|
+
* forward cone. Runs no `put`.
|
|
1106
|
+
*
|
|
1107
|
+
* `BF.Pending` self-dedups: an already-marked node has its subtree marked, so
|
|
1108
|
+
* descent stops (diamonds cost one visit). A read-only-derived parent is skipped
|
|
1109
|
+
* (a split routes around it; a sole one is pre-rejected by `arm`'s
|
|
1110
|
+
* `BF.WriteBlocked` check). The 1→1 spine allocates nothing. */
|
|
1111
|
+
function markDown(start) {
|
|
1112
|
+
let node = start;
|
|
1113
|
+
let stack;
|
|
1114
|
+
for (;;) {
|
|
1115
|
+
if (COUNTS)
|
|
1116
|
+
counts.markDownVisit++;
|
|
1117
|
+
let next;
|
|
1118
|
+
if (isSource(node)) {
|
|
1119
|
+
// Leaf (dual of a `Dirty` source): wake its cone ONCE.
|
|
1120
|
+
if (!(node.bflags & BF.Pending)) {
|
|
1121
|
+
node.bflags |= BF.Pending;
|
|
1122
|
+
const subs = node.subs;
|
|
1123
|
+
if (subs !== undefined)
|
|
1124
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
1125
|
+
}
|
|
894
1126
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1127
|
+
else if (node === start || !(node.bflags & BF.Pending)) {
|
|
1128
|
+
// On the back-path. An already-marked intermediate (≠ start) has its
|
|
1129
|
+
// subtree marked — stop (diamond dedup).
|
|
1130
|
+
if (node !== start)
|
|
1131
|
+
node.bflags |= BF.Pending;
|
|
1132
|
+
for (let e = node.parentEdges; e !== undefined; e = e.nextParent) {
|
|
1133
|
+
linkChild(e); // register this view on the parent's up-list (arm-order)
|
|
1134
|
+
const p = e.parent;
|
|
1135
|
+
// Read-only parent: a split routes around it (its `put` SKIPs it). A sole
|
|
1136
|
+
// read-only parent can't be routed — but that's `BF.WriteBlocked`, already
|
|
1137
|
+
// rejected in `arm`, so the descent never reaches such a node here.
|
|
1138
|
+
if (isReadOnlyDerived(p))
|
|
1139
|
+
continue;
|
|
1140
|
+
if (next === undefined)
|
|
1141
|
+
next = p;
|
|
1142
|
+
else
|
|
1143
|
+
(stack ??= []).push(p);
|
|
1144
|
+
}
|
|
900
1145
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
// into `put` at build time (see `buildLens1`); `pin` ignores `v`.
|
|
904
|
-
push = cb.put(v);
|
|
1146
|
+
if (next !== undefined) {
|
|
1147
|
+
node = next;
|
|
905
1148
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
return;
|
|
909
|
-
// Concrete no-op stop: if the parent already holds `push`, committing
|
|
910
|
-
// changes nothing upstream, so the walk stops. Sound for ANY topology
|
|
911
|
-
// (no speculation). A lossy lens hides an off-grid edit by returning
|
|
912
|
-
// the current source from `put`. Merge parents fold instead. `settled`
|
|
913
|
-
// reads a batched source's pending value WITHOUT committing it, so a
|
|
914
|
-
// net-zero revert leaves the source unchanged and downstream un-fired.
|
|
915
|
-
const pb = parent._bwd;
|
|
916
|
-
const parentMerge = pb !== undefined ? pb.merge : undefined;
|
|
917
|
-
if (parentMerge === undefined && parent._equals(push, settled(parent)))
|
|
918
|
-
return;
|
|
919
|
-
if (parentMerge !== undefined) {
|
|
920
|
-
parentMerge.receive(cell, push);
|
|
921
|
-
if (deferred) {
|
|
922
|
-
if (!(parent.flags & F.BwdQueued))
|
|
923
|
-
parent._enqueueBwd();
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
cell = parent;
|
|
927
|
-
continue;
|
|
1149
|
+
else if (stack !== undefined && stack.length > 0) {
|
|
1150
|
+
node = stack.pop();
|
|
928
1151
|
}
|
|
929
|
-
|
|
930
|
-
// Source: commit + forward-propagate (the forward write).
|
|
931
|
-
parent._writeSource(push);
|
|
1152
|
+
else {
|
|
932
1153
|
return;
|
|
933
1154
|
}
|
|
934
|
-
// Parent is a lens: keep walking, carrying its new view value.
|
|
935
|
-
cell = parent;
|
|
936
|
-
v = push;
|
|
937
1155
|
}
|
|
938
1156
|
}
|
|
939
|
-
/**
|
|
940
|
-
*
|
|
941
|
-
* each
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1157
|
+
/** RESOLVE (pull), dual of `checkDirty`: resolve one node's whole back-cone.
|
|
1158
|
+
* Ascend `childEdges` (only `BACK_MARKED` children) to the armed views,
|
|
1159
|
+
* `writeBack`ing each. Source-centric — a source reflects all its writers, so a
|
|
1160
|
+
* call on it resolves every co-writer together and commits once.
|
|
1161
|
+
*
|
|
1162
|
+
* Iterative post-order over the back-cone (was a recursion bounded by lens-nesting
|
|
1163
|
+
* depth), via an explicit frame stack of {node, next-child cursor}. On entering a
|
|
1164
|
+
* node (pre): clear a merge's contributions, then `writeBack` if it holds an armed
|
|
1165
|
+
* target. After its children drain (post): clear `BF.Pending`, then fold a merge.
|
|
1166
|
+
* Children are walked in forward `childEdges` order (so a co-writer's last write
|
|
1167
|
+
* wins) and a per-call `bEpoch` dedups diamonds — the merge fold lands at its true
|
|
1168
|
+
* post-order position, interleaved with sibling writes, not deferred.
|
|
1169
|
+
* Idempotent, so phase-2 of `backResolve` can call it unconditionally. */
|
|
1170
|
+
function resolveCone(root) {
|
|
1171
|
+
const epoch = ++backCycle;
|
|
1172
|
+
root.bEpoch = epoch;
|
|
1173
|
+
enterCone(root);
|
|
1174
|
+
rcNode[0] = root;
|
|
1175
|
+
rcEdge[0] = root.childEdges;
|
|
1176
|
+
let fp = 1;
|
|
1177
|
+
while (fp > 0) {
|
|
1178
|
+
let e = rcEdge[fp - 1];
|
|
1179
|
+
let descended = false;
|
|
1180
|
+
while (e !== undefined) {
|
|
1181
|
+
const c = e.child;
|
|
1182
|
+
e = e.nextChild;
|
|
1183
|
+
if (c.bflags & BACK_MARKED && c.bEpoch !== epoch) {
|
|
1184
|
+
c.bEpoch = epoch;
|
|
1185
|
+
rcEdge[fp - 1] = e; // resume here when we pop back to this frame
|
|
1186
|
+
enterCone(c);
|
|
1187
|
+
rcNode[fp] = c;
|
|
1188
|
+
rcEdge[fp] = c.childEdges;
|
|
1189
|
+
fp++;
|
|
1190
|
+
descended = true;
|
|
1191
|
+
break;
|
|
973
1192
|
}
|
|
974
1193
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
if (
|
|
983
|
-
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
++batchDepth;
|
|
987
|
-
try {
|
|
988
|
-
forkInto(parents, updates, n);
|
|
989
|
-
}
|
|
990
|
-
finally {
|
|
991
|
-
if (!--batchDepth)
|
|
992
|
-
flush();
|
|
993
|
-
}
|
|
994
|
-
return;
|
|
1194
|
+
if (descended)
|
|
1195
|
+
continue;
|
|
1196
|
+
// Children exhausted → post-order work for this frame's node.
|
|
1197
|
+
const node = rcNode[--fp];
|
|
1198
|
+
node.bflags &= ~BF.Pending;
|
|
1199
|
+
const b = node._bwd;
|
|
1200
|
+
// A merge has exactly one parent-edge; fold its gathered contributions to it.
|
|
1201
|
+
if (b !== undefined && b.merge !== undefined)
|
|
1202
|
+
foldMerge(node.parentEdges.parent, b.merge);
|
|
995
1203
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1204
|
+
}
|
|
1205
|
+
/** `resolveCone` pre-order work: reset a merge's buffer, drive an armed target. */
|
|
1206
|
+
function enterCone(node) {
|
|
1207
|
+
if (COUNTS)
|
|
1208
|
+
counts.resolveConeVisit++;
|
|
1209
|
+
const b = node._bwd;
|
|
1210
|
+
if (b !== undefined && b.merge !== undefined)
|
|
1211
|
+
b.merge.contributions.length = 0;
|
|
1212
|
+
if (node.bflags & BF.Dirty) {
|
|
1213
|
+
node.bflags &= ~BF.Dirty;
|
|
1214
|
+
writeBack(node, node.pendingValue);
|
|
1003
1215
|
}
|
|
1216
|
+
}
|
|
1217
|
+
/** PULL entry for a back-marked `start`. A source resolves its own cone; a view
|
|
1218
|
+
* first descends its marked back-path to the sources, then resolves each. The
|
|
1219
|
+
* `draining` guard stops a `put`'s source read from re-entering.
|
|
1220
|
+
*
|
|
1221
|
+
* Two-phase: phase 1 collects the distinct sources (clearing nothing), phase 2
|
|
1222
|
+
* `resolveCone`s each. Capturing the full source set before any `writeBack` runs
|
|
1223
|
+
* means a sibling commit can't drop a co-writer's source from the worklist. A
|
|
1224
|
+
* per-call `bEpoch` stamp dedups the descent (diamonds visit each node once). */
|
|
1225
|
+
function backResolve(start) {
|
|
1226
|
+
draining = true;
|
|
1004
1227
|
++batchDepth;
|
|
1228
|
+
const prev = activeSub;
|
|
1229
|
+
activeSub = undefined;
|
|
1230
|
+
const sourcesBase = backSources.length;
|
|
1231
|
+
const epoch = ++backCycle;
|
|
1005
1232
|
try {
|
|
1006
|
-
|
|
1233
|
+
if (isSource(start)) {
|
|
1234
|
+
resolveCone(start);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
// Phase 1 (collect): descend the `BF.Pending` cone, gathering distinct
|
|
1238
|
+
// sources. `reached` = a source was found (else `start` is a `pin` sink).
|
|
1239
|
+
let node = start;
|
|
1240
|
+
let stack;
|
|
1241
|
+
let reached = false;
|
|
1242
|
+
for (;;) {
|
|
1243
|
+
let next;
|
|
1244
|
+
for (let e = node.parentEdges; e !== undefined; e = e.nextParent) {
|
|
1245
|
+
const p = e.parent;
|
|
1246
|
+
if (!(p.bflags & BF.Pending) || p.bEpoch === epoch)
|
|
1247
|
+
continue;
|
|
1248
|
+
p.bEpoch = epoch;
|
|
1249
|
+
if (isSource(p)) {
|
|
1250
|
+
reached = true;
|
|
1251
|
+
backSources.push(p);
|
|
1252
|
+
}
|
|
1253
|
+
else if (next === undefined)
|
|
1254
|
+
next = p;
|
|
1255
|
+
else
|
|
1256
|
+
(stack ??= []).push(p);
|
|
1257
|
+
}
|
|
1258
|
+
if (next !== undefined)
|
|
1259
|
+
node = next;
|
|
1260
|
+
else if (stack !== undefined && stack.length > 0)
|
|
1261
|
+
node = stack.pop();
|
|
1262
|
+
else
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
// Phase 2 (resolve): each collected source's whole cone, once.
|
|
1266
|
+
for (let i = sourcesBase; i < backSources.length; i++)
|
|
1267
|
+
resolveCone(backSources[i]);
|
|
1268
|
+
if (!reached && start.bflags & BF.Dirty) {
|
|
1269
|
+
start.bflags &= ~BF.Dirty;
|
|
1270
|
+
writeBack(start, start.pendingValue);
|
|
1271
|
+
}
|
|
1007
1272
|
}
|
|
1008
1273
|
finally {
|
|
1009
|
-
|
|
1010
|
-
|
|
1274
|
+
backSources.length = sourcesBase;
|
|
1275
|
+
activeSub = prev;
|
|
1276
|
+
--batchDepth;
|
|
1277
|
+
draining = false;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/** Resolve any back-write a woken node reads directly. `checkDirty` catches
|
|
1281
|
+
* back-writes that move a source, but a stateful stash moves only the VIEW (no
|
|
1282
|
+
* source changes) — invisible to a source-based check, so resolve this node's
|
|
1283
|
+
* back-marked deps here. A forward-only wake walks no cone and pays nothing. */
|
|
1284
|
+
function resolveBackDeps(node) {
|
|
1285
|
+
for (let l = node.deps; l !== undefined; l = l.nextDep) {
|
|
1286
|
+
const d = l.dep;
|
|
1287
|
+
if (d.bflags & BACK_MARKED && !draining)
|
|
1288
|
+
backResolve(d);
|
|
1011
1289
|
}
|
|
1012
1290
|
}
|
|
1013
|
-
/**
|
|
1014
|
-
* lens
|
|
1015
|
-
*
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1291
|
+
/** Backward commit/compute (dual of `_update`): drive a back-write of `target`
|
|
1292
|
+
* toward the sources, applying each lens's `put` and staging each source as it's
|
|
1293
|
+
* reached (so a later sibling composes rather than clobbers). A `SKIP` slot prunes
|
|
1294
|
+
* a branch; every other slot is written verbatim, `undefined` included.
|
|
1295
|
+
*
|
|
1296
|
+
* Iterative depth-first, left-to-right (children pushed in reverse onto the
|
|
1297
|
+
* pooled `wbNode`/`wbTarget` stack), so a sibling read sees a prior sibling's
|
|
1298
|
+
* staged write exactly as the old recursion did — bounded by stack memory, not
|
|
1299
|
+
* the call stack. */
|
|
1300
|
+
function writeBack(node, target) {
|
|
1301
|
+
wbNode[0] = node;
|
|
1302
|
+
wbTarget[0] = target;
|
|
1303
|
+
let top = 1;
|
|
1304
|
+
let sTop = 0;
|
|
1305
|
+
while (top > 0) {
|
|
1306
|
+
if (COUNTS)
|
|
1307
|
+
counts.writeBackVisit++;
|
|
1308
|
+
const cur = wbNode[--top];
|
|
1309
|
+
const tgt = wbTarget[top];
|
|
1310
|
+
if (isSource(cur)) {
|
|
1311
|
+
cur._writeSource(tgt); // staged now, visible to later siblings
|
|
1312
|
+
// Clear this source's `BF.Pending`, then re-assert iff a lens-child is STILL
|
|
1313
|
+
// armed (an overlapping co-writer) — else that write is lost, and leaving it
|
|
1314
|
+
// set unconditionally would strand `BF.Pending` on every fan-in source.
|
|
1315
|
+
// Scan from the TAIL: `resolveCone` drives children head→tail, so the last
|
|
1316
|
+
// still-armed co-writer sits near the tail — found in O(1) until the final
|
|
1317
|
+
// one, turning a fan-in's re-assert from O(N²) into O(N). (Order is
|
|
1318
|
+
// irrelevant; this is a find-any.)
|
|
1319
|
+
cur.bflags &= ~BF.Pending;
|
|
1320
|
+
for (let e = cur.childEdgesTail; e !== undefined; e = e.prevChild) {
|
|
1321
|
+
if (COUNTS)
|
|
1322
|
+
counts.reassertScan++;
|
|
1323
|
+
if (e.child.bflags & BACK_MARKED) {
|
|
1324
|
+
cur.bflags |= BF.Pending;
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
cur.bflags &= ~BF.Pending; // passing through clears the path marker
|
|
1331
|
+
const b = cur._bwd;
|
|
1332
|
+
if (b === undefined)
|
|
1333
|
+
throw new TypeError("Cannot write through to a computed");
|
|
1334
|
+
const mn = b.merge;
|
|
1335
|
+
if (mn !== undefined) {
|
|
1336
|
+
mn.contributions.push(tgt); // gathered here; `resolveCone` folds post-order
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const pe = cur.parentEdges;
|
|
1340
|
+
if (pe === undefined)
|
|
1341
|
+
continue; // pin sink (parentless): absorb
|
|
1342
|
+
const sc = b.stateful;
|
|
1343
|
+
if (sc !== undefined && !b.scatter) {
|
|
1344
|
+
// Single-source stateful fast-path (scalar `bwd`); one index-0 parent edge.
|
|
1345
|
+
const p = pe.parent;
|
|
1346
|
+
const x = p.value;
|
|
1347
|
+
const ver = p.version;
|
|
1348
|
+
if (ver !== sc.stamp) {
|
|
1349
|
+
if (COUNTS)
|
|
1350
|
+
counts.step++;
|
|
1351
|
+
sc.complement = sc.step(x, sc.complement);
|
|
1352
|
+
}
|
|
1353
|
+
if (COUNTS)
|
|
1354
|
+
counts.put++;
|
|
1355
|
+
const res = b.put(tgt, x, sc.complement);
|
|
1356
|
+
sc.complement = res.complement;
|
|
1357
|
+
wbStateful[sTop++] = cur;
|
|
1358
|
+
const u = res.update;
|
|
1359
|
+
if (u !== SKIP) {
|
|
1360
|
+
wbNode[top] = p;
|
|
1361
|
+
wbTarget[top] = u;
|
|
1362
|
+
top++;
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
// Stash: the view moved through the complement alone (see the scatter case).
|
|
1366
|
+
cur.flags |= F.Dirty;
|
|
1367
|
+
const subs = cur.subs;
|
|
1368
|
+
if (subs !== undefined)
|
|
1369
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
1370
|
+
}
|
|
1020
1371
|
continue;
|
|
1021
|
-
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1372
|
+
}
|
|
1373
|
+
if (b.scatter) {
|
|
1374
|
+
// Gather ordered parents (index-ordered edges) for the tuple `put`.
|
|
1375
|
+
let n = 0;
|
|
1376
|
+
for (let e = pe; e !== undefined; e = e.nextParent)
|
|
1377
|
+
n++;
|
|
1378
|
+
const parents = new Array(n);
|
|
1379
|
+
for (let e = pe; e !== undefined; e = e.nextParent)
|
|
1380
|
+
parents[e.index] = e.parent;
|
|
1381
|
+
let out;
|
|
1382
|
+
if (sc !== undefined) {
|
|
1383
|
+
const vals = new Array(n);
|
|
1384
|
+
let ver = 0;
|
|
1385
|
+
for (let i = 0; i < n; i++) {
|
|
1386
|
+
vals[i] = parents[i].value;
|
|
1387
|
+
ver += parents[i].version;
|
|
1388
|
+
}
|
|
1389
|
+
// Refresh the complement only if a source moved since the last sync — e.g.
|
|
1390
|
+
// a prior sibling co-writer bumped a shared source. A pure own re-write
|
|
1391
|
+
// (sum unchanged) skips it: `bwd` already gets the settled complement.
|
|
1392
|
+
if (ver !== sc.stamp) {
|
|
1393
|
+
if (COUNTS)
|
|
1394
|
+
counts.step++;
|
|
1395
|
+
sc.complement = sc.step(vals, sc.complement);
|
|
1396
|
+
}
|
|
1397
|
+
if (COUNTS)
|
|
1398
|
+
counts.put++;
|
|
1399
|
+
const res = b.put(tgt, vals, sc.complement);
|
|
1400
|
+
const upd = res.updates;
|
|
1401
|
+
// Commit `bwd`'s complement directly; it must be consistent with `updates`
|
|
1402
|
+
// (no reliance on a post-write `step`). The stamp is re-set post-order (after
|
|
1403
|
+
// the sources are written and their versions bumped) so the next forward read
|
|
1404
|
+
// sees an unchanged sum and skips `step` — own-write provenance.
|
|
1405
|
+
sc.complement = res.complement;
|
|
1406
|
+
wbStateful[sTop++] = cur;
|
|
1407
|
+
out = upd;
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
if (COUNTS)
|
|
1411
|
+
counts.put++;
|
|
1412
|
+
out = b.put(tgt);
|
|
1413
|
+
}
|
|
1414
|
+
// Push non-SKIP children in REVERSE so index 0 is popped (processed) first
|
|
1415
|
+
// — depth-first, left-to-right. A short `out` skips the trailing parents.
|
|
1416
|
+
let wrote = false;
|
|
1417
|
+
const m = out.length < n ? out.length : n;
|
|
1418
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
1419
|
+
const u = out[i];
|
|
1420
|
+
if (u !== SKIP) {
|
|
1421
|
+
wrote = true;
|
|
1422
|
+
wbNode[top] = parents[i];
|
|
1423
|
+
wbTarget[top] = u;
|
|
1424
|
+
top++;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
// A stateful lens can change its VIEW through the complement alone, moving no
|
|
1428
|
+
// source (a "stash"; `!wrote` ⇒ no children pushed). The forward cone never
|
|
1429
|
+
// fires, so invalidate this node's cache and propagate to its observers here.
|
|
1430
|
+
if (!wrote && sc !== undefined) {
|
|
1431
|
+
cur.flags |= F.Dirty;
|
|
1432
|
+
const subs = cur.subs;
|
|
1433
|
+
if (subs !== undefined)
|
|
1434
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
1435
|
+
}
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
// 1→1 lens (single index-0 parent-edge).
|
|
1439
|
+
if (COUNTS)
|
|
1440
|
+
counts.put++;
|
|
1441
|
+
wbNode[top] = pe.parent;
|
|
1442
|
+
wbTarget[top] = b.put(tgt);
|
|
1443
|
+
top++;
|
|
1444
|
+
}
|
|
1445
|
+
// Post-order re-stamp: now the sources are written (versions bumped), record
|
|
1446
|
+
// each on-path stateful lens's parent-version sum, so its next forward read
|
|
1447
|
+
// sees an unchanged sum and skips `step`. Integers only — no `fwd`, no commit.
|
|
1448
|
+
for (let i = 0; i < sTop; i++) {
|
|
1449
|
+
const sc = wbStateful[i]._bwd.stateful;
|
|
1450
|
+
let ver = 0;
|
|
1451
|
+
for (let e = wbStateful[i].parentEdges; e !== undefined; e = e.nextParent) {
|
|
1452
|
+
ver += e.parent.version;
|
|
1453
|
+
}
|
|
1454
|
+
sc.stamp = ver;
|
|
1455
|
+
wbStateful[i] = undefined;
|
|
1026
1456
|
}
|
|
1027
1457
|
}
|
|
1458
|
+
/** Fold a merge's contributions once (policy; default last-writer-wins) and write
|
|
1459
|
+
* the result up to its parent. Called post-order from `resolveCone`. */
|
|
1460
|
+
function foldMerge(parent, mn) {
|
|
1461
|
+
if (COUNTS)
|
|
1462
|
+
counts.fold++;
|
|
1463
|
+
const vals = mn.contributions;
|
|
1464
|
+
const fold = mn.foldFn;
|
|
1465
|
+
let folded;
|
|
1466
|
+
if (fold !== undefined)
|
|
1467
|
+
folded = fold(vals);
|
|
1468
|
+
else if (vals.length > 0)
|
|
1469
|
+
folded = vals[vals.length - 1];
|
|
1470
|
+
else
|
|
1471
|
+
return; // last-writer-wins with no contributor: leave the parent
|
|
1472
|
+
vals.length = 0; // reuse the merge-owned buffer in place (fold must not retain it)
|
|
1473
|
+
writeBack(parent, folded);
|
|
1474
|
+
}
|
|
1475
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1476
|
+
// Public API — factories (cell / derive / lens) over the builders above.
|
|
1477
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1028
1478
|
/** Writable source; passes an existing `Writable` through (idempotent). */
|
|
1029
1479
|
export function cell(initial, opts) {
|
|
1030
1480
|
if (initial instanceof Cell)
|
|
1031
1481
|
return initial;
|
|
1032
1482
|
return new Cell(initial, opts);
|
|
1033
1483
|
}
|
|
1034
|
-
// Bare (untyped) factories
|
|
1035
|
-
// from the closures (the polymorphic-`this` statics are for typed
|
|
1036
|
-
// subclasses like `Vec.lens`).
|
|
1484
|
+
// Bare (untyped) factories: plain `Cell`, inferring `R` from the closures.
|
|
1037
1485
|
const CELL_CTOR = Cell;
|
|
1038
1486
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
1039
1487
|
export function derive(...args) {
|
|
1040
|
-
return
|
|
1488
|
+
return buildDerive(CELL_CTOR, args);
|
|
1041
1489
|
}
|
|
1042
1490
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
1043
1491
|
export function lens(...args) {
|
|
1044
|
-
return
|
|
1492
|
+
return buildLens(CELL_CTOR, args);
|
|
1045
1493
|
}
|
|
1046
|
-
//
|
|
1494
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1495
|
+
// Effects & schedulers — the Effect watcher (internal) and the public
|
|
1496
|
+
// effect / batch / network / flush surface built on it.
|
|
1497
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1498
|
+
// Effect — one watcher class for both auto-tracked effects and explicit-topology
|
|
1499
|
+
// networks: alien-signals' effect plus the `EM` mode toggles `network()` needs.
|
|
1047
1500
|
class Effect {
|
|
1048
1501
|
flags = F.Watching | F.RecursedCheck;
|
|
1049
1502
|
subs = undefined;
|
|
@@ -1052,26 +1505,31 @@ class Effect {
|
|
|
1052
1505
|
depsTail = undefined;
|
|
1053
1506
|
fn;
|
|
1054
1507
|
cleanup = undefined;
|
|
1055
|
-
|
|
1508
|
+
/** Watcher-behavior bits (`EM`); `EM.None` for a plain effect. */
|
|
1509
|
+
mode;
|
|
1510
|
+
constructor(fn, mode = EM.None) {
|
|
1056
1511
|
this.fn = fn;
|
|
1057
|
-
|
|
1058
|
-
activeSub = this;
|
|
1059
|
-
try {
|
|
1060
|
-
++runDepth;
|
|
1061
|
-
const ret = fn();
|
|
1062
|
-
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1063
|
-
}
|
|
1064
|
-
finally {
|
|
1065
|
-
--runDepth;
|
|
1066
|
-
activeSub = prev;
|
|
1067
|
-
this.flags &= ~F.RecursedCheck;
|
|
1068
|
-
}
|
|
1512
|
+
this.mode = mode;
|
|
1069
1513
|
}
|
|
1070
1514
|
_update() {
|
|
1071
1515
|
this.flags = F.Mutable;
|
|
1072
1516
|
return true;
|
|
1073
1517
|
}
|
|
1074
1518
|
_notify() {
|
|
1519
|
+
const mode = this.mode;
|
|
1520
|
+
if (mode & EM.Manual) {
|
|
1521
|
+
this.flags |= F.Watching; // re-arm but don't queue; only `flush()` advances
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (mode & EM.Sync) {
|
|
1525
|
+
// Eager watcher (network): append + force a synchronous flush.
|
|
1526
|
+
queued[queuedLength++] = this;
|
|
1527
|
+
syncFlush = true;
|
|
1528
|
+
this.flags &= ~F.Watching;
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
// Plain effect: batch-insert this effect and any subscribed to it, in
|
|
1532
|
+
// dependency order (alien-signals).
|
|
1075
1533
|
let e = this;
|
|
1076
1534
|
let insertIndex = queuedLength;
|
|
1077
1535
|
const firstInsertedIndex = insertIndex;
|
|
@@ -1101,6 +1559,10 @@ class Effect {
|
|
|
1101
1559
|
this._runCleanup();
|
|
1102
1560
|
}
|
|
1103
1561
|
_run() {
|
|
1562
|
+
// Resolve back-writes this node reads directly (incl. view-only stashes);
|
|
1563
|
+
// `checkDirty` resolves any back-`Pending` source reached deeper.
|
|
1564
|
+
if (this.deps !== undefined)
|
|
1565
|
+
resolveBackDeps(this);
|
|
1104
1566
|
const flags = this.flags;
|
|
1105
1567
|
if (flags & F.Dirty || (flags & F.Pending && checkDirty(this.deps, this))) {
|
|
1106
1568
|
if (this.cleanup) {
|
|
@@ -1108,27 +1570,39 @@ class Effect {
|
|
|
1108
1570
|
if (!this.flags)
|
|
1109
1571
|
return;
|
|
1110
1572
|
}
|
|
1111
|
-
this.
|
|
1112
|
-
this.flags = F.Watching | F.RecursedCheck;
|
|
1113
|
-
const prev = activeSub;
|
|
1114
|
-
activeSub = this;
|
|
1115
|
-
try {
|
|
1116
|
-
++cycle;
|
|
1117
|
-
++runDepth;
|
|
1118
|
-
const ret = this.fn();
|
|
1119
|
-
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1120
|
-
}
|
|
1121
|
-
finally {
|
|
1122
|
-
--runDepth;
|
|
1123
|
-
activeSub = prev;
|
|
1124
|
-
this.flags &= ~F.RecursedCheck;
|
|
1125
|
-
purgeDeps(this);
|
|
1126
|
-
}
|
|
1573
|
+
this._invoke();
|
|
1127
1574
|
}
|
|
1128
1575
|
else if (this.deps !== undefined) {
|
|
1129
1576
|
this.flags = F.Watching;
|
|
1130
1577
|
}
|
|
1131
1578
|
}
|
|
1579
|
+
/** Run the body — the single path for first fire, scheduled re-run, and manual
|
|
1580
|
+
* `flush()`. Auto-tracks deps unless `NoTrack`; self-excludes writes under `Exclude`. */
|
|
1581
|
+
_invoke() {
|
|
1582
|
+
const noTrack = this.mode & EM.NoTrack;
|
|
1583
|
+
if (!noTrack)
|
|
1584
|
+
this.depsTail = undefined;
|
|
1585
|
+
this.flags = F.Watching | F.RecursedCheck;
|
|
1586
|
+
const prevSub = activeSub;
|
|
1587
|
+
const prevExc = activeExcluded;
|
|
1588
|
+
activeSub = noTrack ? undefined : this;
|
|
1589
|
+
if (this.mode & EM.Exclude)
|
|
1590
|
+
activeExcluded = this;
|
|
1591
|
+
try {
|
|
1592
|
+
++cycle;
|
|
1593
|
+
++runDepth;
|
|
1594
|
+
const ret = this.fn();
|
|
1595
|
+
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1596
|
+
}
|
|
1597
|
+
finally {
|
|
1598
|
+
--runDepth;
|
|
1599
|
+
activeSub = prevSub;
|
|
1600
|
+
activeExcluded = prevExc;
|
|
1601
|
+
this.flags &= ~F.RecursedCheck;
|
|
1602
|
+
if (!noTrack)
|
|
1603
|
+
purgeDeps(this);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1132
1606
|
_runCleanup() {
|
|
1133
1607
|
const c = this.cleanup;
|
|
1134
1608
|
this.cleanup = undefined;
|
|
@@ -1144,49 +1618,76 @@ class Effect {
|
|
|
1144
1618
|
}
|
|
1145
1619
|
export function effect(fn) {
|
|
1146
1620
|
const e = new Effect(fn);
|
|
1621
|
+
e._invoke();
|
|
1147
1622
|
return () => e._unwatched();
|
|
1148
1623
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
// more bwd entries). Loops until both queues are exhausted.
|
|
1624
|
+
/** Run effects woken by a write. Backward work is pulled lazily per read, so
|
|
1625
|
+
* flush owns no backward bookkeeping — just the effect queue. */
|
|
1152
1626
|
function flush() {
|
|
1153
1627
|
if (flushing)
|
|
1154
1628
|
return;
|
|
1155
1629
|
flushing = true;
|
|
1156
|
-
|
|
1630
|
+
// Error locality: one effect throwing must not strand its siblings. Drain the
|
|
1631
|
+
// whole queue, catching each body; surface the first error after the queue is
|
|
1632
|
+
// empty (later errors are dropped — the engine stays consistent, the user still
|
|
1633
|
+
// sees a failure). A throwing body isn't re-queued (its `F.Watching` is already
|
|
1634
|
+
// cleared); it re-arms on the next wake.
|
|
1635
|
+
let err;
|
|
1636
|
+
let threw = false;
|
|
1157
1637
|
try {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const cb = cell._bwd;
|
|
1163
|
-
if (cb.queueIdx !== bwdIndex || !(cell.flags & F.BwdQueued)) {
|
|
1164
|
-
bwdIndex++;
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
bwdIndex++;
|
|
1168
|
-
cell.flags &= ~F.BwdQueued;
|
|
1169
|
-
if (cb.merge !== undefined) {
|
|
1170
|
-
bwdUntracked(cell, undefined, true);
|
|
1171
|
-
}
|
|
1172
|
-
else {
|
|
1173
|
-
bwdUntracked(cell, cell.pendingValue, true);
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
while (notifyIndex < queuedLength) {
|
|
1177
|
-
const e = queued[notifyIndex];
|
|
1178
|
-
queued[notifyIndex++] = undefined;
|
|
1638
|
+
while (notifyIndex < queuedLength) {
|
|
1639
|
+
const e = queued[notifyIndex];
|
|
1640
|
+
queued[notifyIndex++] = undefined;
|
|
1641
|
+
try {
|
|
1179
1642
|
e._run();
|
|
1180
1643
|
}
|
|
1644
|
+
catch (ex) {
|
|
1645
|
+
if (!threw) {
|
|
1646
|
+
err = ex;
|
|
1647
|
+
threw = true;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1181
1650
|
}
|
|
1182
1651
|
}
|
|
1183
1652
|
finally {
|
|
1184
|
-
bwdQueue.length = 0;
|
|
1185
1653
|
notifyIndex = 0;
|
|
1186
1654
|
queuedLength = 0;
|
|
1655
|
+
syncFlush = false;
|
|
1187
1656
|
flushing = false;
|
|
1188
1657
|
}
|
|
1658
|
+
if (threw)
|
|
1659
|
+
throw err;
|
|
1189
1660
|
}
|
|
1661
|
+
/** Queue an effect flush for the end of the current microtask turn (idempotent).
|
|
1662
|
+
* A write wakes effects asynchronously; many writes in one turn coalesce. */
|
|
1663
|
+
function schedule() {
|
|
1664
|
+
if (scheduled)
|
|
1665
|
+
return;
|
|
1666
|
+
scheduled = true;
|
|
1667
|
+
queueMicrotask(() => {
|
|
1668
|
+
scheduled = false;
|
|
1669
|
+
flush();
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
/** Resolve the queue after a write: no-op inside a `batch`/flush (the barrier
|
|
1673
|
+
* owns flushing), else synchronously if a `Sync` watcher is waiting (eager
|
|
1674
|
+
* solve) or deferred to the microtask (coalesced effects). */
|
|
1675
|
+
function autoFlush() {
|
|
1676
|
+
if (batchDepth !== 0 || flushing)
|
|
1677
|
+
return;
|
|
1678
|
+
if (syncFlush)
|
|
1679
|
+
flush();
|
|
1680
|
+
else
|
|
1681
|
+
schedule();
|
|
1682
|
+
}
|
|
1683
|
+
/** Run all pending effects now, synchronously — the escape hatch for code that
|
|
1684
|
+
* must observe effect side-effects before yielding. Reads never need it. */
|
|
1685
|
+
export function settle() {
|
|
1686
|
+
flush();
|
|
1687
|
+
}
|
|
1688
|
+
/** Group writes and flush effects synchronously at the end of `fn`. Effects
|
|
1689
|
+
* coalesce on the microtask turn anyway; reach for `batch` only to run the woken
|
|
1690
|
+
* effects before the call returns. */
|
|
1190
1691
|
export function batch(fn) {
|
|
1191
1692
|
++batchDepth;
|
|
1192
1693
|
try {
|
|
@@ -1207,191 +1708,114 @@ export function untracked(fn) {
|
|
|
1207
1708
|
activeSub = prev;
|
|
1208
1709
|
}
|
|
1209
1710
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
disposed = false;
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
this.body = body;
|
|
1227
|
-
this.manual = manual;
|
|
1228
|
-
}
|
|
1229
|
-
/** Two-phase init so the body sees its own handle on the first fire. */
|
|
1230
|
-
_initWithHandle(handle, initialDeps) {
|
|
1231
|
-
this._handle = handle;
|
|
1232
|
-
this._linkBatch(initialDeps);
|
|
1233
|
-
this._runBody(EMPTY_DIRTY);
|
|
1234
|
-
}
|
|
1235
|
-
_update() {
|
|
1236
|
-
this.flags = F.Mutable;
|
|
1237
|
-
return true;
|
|
1238
|
-
}
|
|
1239
|
-
_notify() {
|
|
1240
|
-
if (this.manual) {
|
|
1241
|
-
this.pending = true;
|
|
1242
|
-
this.flags |= F.Watching;
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
queued[queuedLength++] = this;
|
|
1246
|
-
this.flags &= ~F.Watching;
|
|
1247
|
-
}
|
|
1248
|
-
_unwatched() {
|
|
1249
|
-
this.disposed = true;
|
|
1250
|
-
this.flags = F.None;
|
|
1251
|
-
disposeAllDepsInReverse(this);
|
|
1252
|
-
const sub = this.subs;
|
|
1253
|
-
if (sub !== undefined)
|
|
1254
|
-
unlink(sub);
|
|
1255
|
-
this.lastValues.clear();
|
|
1256
|
-
}
|
|
1257
|
-
_run() {
|
|
1258
|
-
if (this.disposed)
|
|
1259
|
-
return;
|
|
1260
|
-
const flags = this.flags;
|
|
1261
|
-
if (flags & F.Dirty || (flags & F.Pending && checkDirty(this.deps, this))) {
|
|
1262
|
-
this._runBody(this._computeDirty());
|
|
1263
|
-
}
|
|
1264
|
-
else if (this.deps !== undefined) {
|
|
1265
|
-
this.flags = F.Watching;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
_computeDirty() {
|
|
1711
|
+
/** Build a reactive sub-DAG. The body fires when any subscribed dep changes
|
|
1712
|
+
* (`dirty` = the changed subset), self-excludes its own writes, and (auto mode)
|
|
1713
|
+
* resolves synchronously. `manual: true` defers firing so only `flush()` advances;
|
|
1714
|
+
* `flush()` from inside the body throws. Network-specific state (last-values,
|
|
1715
|
+
* handle) lives in this closure, so the shared `Effect` carries none of it. */
|
|
1716
|
+
export function network(
|
|
1717
|
+
// biome-ignore lint/suspicious/noExplicitAny: deps come in many flavours
|
|
1718
|
+
deps, body, opts) {
|
|
1719
|
+
const lastValues = new Map();
|
|
1720
|
+
const depsSet = new Set();
|
|
1721
|
+
let ownCycle = 0;
|
|
1722
|
+
let disposed = false;
|
|
1723
|
+
// Forward-declared so the closures below can reach the node; assigned before
|
|
1724
|
+
// any runs (the first `_invoke` happens after construction).
|
|
1725
|
+
let node;
|
|
1726
|
+
const computeDirty = () => {
|
|
1269
1727
|
let dirty;
|
|
1270
|
-
for (const [
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1273
|
-
dirty = new Set();
|
|
1274
|
-
dirty.add(cell);
|
|
1275
|
-
}
|
|
1728
|
+
for (const [c, last] of lastValues) {
|
|
1729
|
+
if (c.peek() !== last)
|
|
1730
|
+
(dirty ??= new Set()).add(c);
|
|
1276
1731
|
}
|
|
1277
1732
|
return dirty ?? EMPTY_DIRTY;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
}
|
|
1291
|
-
finally {
|
|
1292
|
-
if (!--batchDepth)
|
|
1293
|
-
flush();
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
finally {
|
|
1297
|
-
--runDepth;
|
|
1298
|
-
activeNetwork = prevSettler;
|
|
1299
|
-
this.flags &= ~F.RecursedCheck;
|
|
1300
|
-
this.lastValues.clear();
|
|
1301
|
-
let l = this.deps;
|
|
1302
|
-
while (l !== undefined) {
|
|
1303
|
-
const cell = l.dep;
|
|
1304
|
-
this.lastValues.set(cell, cell.peek());
|
|
1305
|
-
l = l.nextDep;
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
this.pending = false;
|
|
1309
|
-
}
|
|
1310
|
-
flush() {
|
|
1311
|
-
if (this.disposed)
|
|
1312
|
-
return;
|
|
1313
|
-
if (this.flags & F.RecursedCheck) {
|
|
1314
|
-
throw new Error("network: flush() called from inside body — would recurse infinitely. " +
|
|
1315
|
-
"Return from the body and let the next dep change drive the next fire.");
|
|
1733
|
+
};
|
|
1734
|
+
const linkDeps = (cells) => {
|
|
1735
|
+
let tail = node.deps;
|
|
1736
|
+
if (tail !== undefined)
|
|
1737
|
+
while (tail.nextDep !== undefined)
|
|
1738
|
+
tail = tail.nextDep;
|
|
1739
|
+
node.depsTail = tail;
|
|
1740
|
+
for (const s of cells) {
|
|
1741
|
+
if (depsSet.has(s))
|
|
1742
|
+
continue;
|
|
1743
|
+
depsSet.add(s);
|
|
1744
|
+
link(s, node, ++ownCycle);
|
|
1316
1745
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
subscribe(cells) {
|
|
1320
|
-
if (this.disposed)
|
|
1321
|
-
return;
|
|
1322
|
-
this._linkBatch(cells);
|
|
1323
|
-
}
|
|
1324
|
-
unsubscribe(cells) {
|
|
1325
|
-
if (this.disposed)
|
|
1326
|
-
return;
|
|
1327
|
-
const set = this._depsSet;
|
|
1746
|
+
};
|
|
1747
|
+
const unlinkDeps = (cells) => {
|
|
1328
1748
|
for (const s of cells) {
|
|
1329
|
-
if (!
|
|
1749
|
+
if (!depsSet.has(s))
|
|
1330
1750
|
continue;
|
|
1331
|
-
|
|
1332
|
-
let l =
|
|
1333
|
-
while (l !== undefined) {
|
|
1751
|
+
depsSet.delete(s);
|
|
1752
|
+
for (let l = node.deps; l !== undefined; l = l.nextDep) {
|
|
1334
1753
|
if (l.dep === s) {
|
|
1335
|
-
unlink(l,
|
|
1754
|
+
unlink(l, node);
|
|
1336
1755
|
break;
|
|
1337
1756
|
}
|
|
1338
|
-
l = l.nextDep;
|
|
1339
1757
|
}
|
|
1340
1758
|
}
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1759
|
+
};
|
|
1760
|
+
const handle = {
|
|
1761
|
+
dispose: () => {
|
|
1762
|
+
if (disposed)
|
|
1763
|
+
return;
|
|
1764
|
+
disposed = true;
|
|
1765
|
+
node._unwatched();
|
|
1766
|
+
lastValues.clear();
|
|
1767
|
+
},
|
|
1768
|
+
flush: () => {
|
|
1769
|
+
if (disposed)
|
|
1770
|
+
return;
|
|
1771
|
+
// RecursedCheck doubles as the "body running" guard.
|
|
1772
|
+
if (node.flags & F.RecursedCheck) {
|
|
1773
|
+
throw new Error("network: flush() called from inside body — would recurse infinitely.");
|
|
1774
|
+
}
|
|
1775
|
+
batch(() => node._invoke());
|
|
1776
|
+
},
|
|
1777
|
+
subscribe: (...cells) => {
|
|
1778
|
+
if (!disposed)
|
|
1779
|
+
linkDeps(cells);
|
|
1780
|
+
},
|
|
1781
|
+
unsubscribe: (...cells) => {
|
|
1782
|
+
if (!disposed)
|
|
1783
|
+
unlinkDeps(cells);
|
|
1784
|
+
},
|
|
1785
|
+
};
|
|
1786
|
+
// The Effect body: hand the changed subset to the user body, then re-snapshot
|
|
1787
|
+
// the deps for the next fire.
|
|
1788
|
+
const run = () => {
|
|
1789
|
+
const dirty = computeDirty();
|
|
1790
|
+
try {
|
|
1791
|
+
body(dirty, handle);
|
|
1348
1792
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1793
|
+
finally {
|
|
1794
|
+
lastValues.clear();
|
|
1795
|
+
for (let l = node.deps; l !== undefined; l = l.nextDep) {
|
|
1796
|
+
const c = l.dep;
|
|
1797
|
+
lastValues.set(c, c.peek());
|
|
1798
|
+
}
|
|
1355
1799
|
}
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
/** Build a reactive sub-DAG node with explicit topology. The body fires
|
|
1359
|
-
* when any subscribed dep changes (`dirty` = the changed subset), runs
|
|
1360
|
-
* inside `batch()`, and self-excludes its own writes. Topology is the
|
|
1361
|
-
* deps array + later subscribe/unsubscribe (body reads add no deps).
|
|
1362
|
-
* `flush()` from inside the body throws; `manual: true` defers
|
|
1363
|
-
* auto-firing so only `flush()` advances. */
|
|
1364
|
-
export function network(
|
|
1365
|
-
// biome-ignore lint/suspicious/noExplicitAny: deps come in many flavours
|
|
1366
|
-
deps, body, opts) {
|
|
1367
|
-
const node = new _NetworkNode(body, opts?.manual ?? false);
|
|
1368
|
-
const handle = {
|
|
1369
|
-
dispose: () => node._unwatched(),
|
|
1370
|
-
flush: () => node.flush(),
|
|
1371
|
-
subscribe: (...cells) => node.subscribe(cells),
|
|
1372
|
-
unsubscribe: (...cells) => node.unsubscribe(cells),
|
|
1373
1800
|
};
|
|
1374
|
-
node
|
|
1801
|
+
node = new Effect(run, EM.NoTrack | EM.Exclude | (opts?.manual ? EM.Manual : EM.Sync));
|
|
1802
|
+
linkDeps(deps);
|
|
1803
|
+
batch(() => node._invoke()); // first fire (lastValues empty ⇒ EMPTY_DIRTY)
|
|
1375
1804
|
return handle;
|
|
1376
1805
|
}
|
|
1377
|
-
// MISC stuff used by a few places, to revisit...
|
|
1378
1806
|
// ── value-class authoring helpers ──────────────────────────────────
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
// The choice between them IS the local declaration of writability at each
|
|
1382
|
-
// getter (mirroring `: this` invertible method returns). For arbitrary
|
|
1807
|
+
// `fieldLens`/`cachedDerive` are the two getter forms a value class declares;
|
|
1808
|
+
// the choice between them is the local declaration of writability. For arbitrary
|
|
1383
1809
|
// cached views, use `lazy()` directly.
|
|
1384
|
-
/** Bidirectional field lens onto `parent.value[key]
|
|
1385
|
-
*
|
|
1386
|
-
* conditional: `Writable<Cls>` on a writable parent, bare `Cls` on RO.
|
|
1810
|
+
/** Bidirectional field lens onto `parent.value[key]` (write spread-replaces),
|
|
1811
|
+
* cached per (instance, key). `Writable<Cls>` on a writable parent, bare `Cls` on RO.
|
|
1387
1812
|
*
|
|
1388
|
-
* get x() { return
|
|
1389
|
-
export function
|
|
1390
|
-
return lazy(parent, key, () =>
|
|
1813
|
+
* get x() { return fieldLens(this, "x", Num); } */
|
|
1814
|
+
export function fieldLens(parent, key, Cls) {
|
|
1815
|
+
return lazy(parent, key, () => fieldOf(parent, key, Cls));
|
|
1391
1816
|
}
|
|
1392
1817
|
/** Read-only derived view via `Cls.derive(parent, fn)`, memoized per
|
|
1393
|
-
* (instance, key)
|
|
1394
|
-
* getter form, not a new kind of cell.
|
|
1818
|
+
* (instance, key). The cache is the point.
|
|
1395
1819
|
*
|
|
1396
1820
|
* get magnitude() {
|
|
1397
1821
|
* return cachedDerive(this, "magnitude", Num, v => Math.hypot(v.x, v.y));
|
|
@@ -1401,12 +1825,8 @@ export function cachedDerive(parent, key, Cls, fn) {
|
|
|
1401
1825
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.derive
|
|
1402
1826
|
return lazy(parent, key, () => Cls.derive(parent, fn));
|
|
1403
1827
|
}
|
|
1404
|
-
/** Every cell `s` transitively depends on, including itself
|
|
1405
|
-
*
|
|
1406
|
-
* peeking each Computed to populate deps; the `seen` set breaks cycles.
|
|
1407
|
-
* Used by `Propagators` to expand declared reads into their transitive
|
|
1408
|
-
* parent set. Inspection is safe: it only reads engine state and peeks
|
|
1409
|
-
* `.value` (idempotent for lazy Computeds). */
|
|
1828
|
+
/** Every cell `s` transitively depends on, including itself (BFS, peeking each
|
|
1829
|
+
* computed to populate deps; `seen` breaks cycles). */
|
|
1410
1830
|
export function transitiveDeps(s) {
|
|
1411
1831
|
const seen = new Set();
|
|
1412
1832
|
const queue = [s];
|
|
@@ -1415,7 +1835,6 @@ export function transitiveDeps(s) {
|
|
|
1415
1835
|
if (seen.has(cur))
|
|
1416
1836
|
continue;
|
|
1417
1837
|
seen.add(cur);
|
|
1418
|
-
// Cast to reach engine fields the typed Cell<T> shape doesn't surface.
|
|
1419
1838
|
const c = cur;
|
|
1420
1839
|
if (c.getter !== undefined) {
|
|
1421
1840
|
void cur.value;
|