bireactive 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- 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/core/_counts.d.ts +48 -0
- package/dist/core/_counts.js +51 -0
- package/dist/core/cell.d.ts +148 -112
- package/dist/core/cell.js +945 -768
- package/dist/core/debug.d.ts +25 -0
- package/dist/core/debug.js +121 -0
- package/dist/core/derived-geometry.js +4 -7
- package/dist/core/index.d.ts +9 -2
- package/dist/core/index.js +8 -1
- package/dist/core/lenses/aggregates.d.ts +42 -52
- package/dist/core/lenses/aggregates.js +225 -116
- package/dist/core/lenses/geometry.d.ts +22 -4
- package/dist/core/lenses/geometry.js +59 -27
- package/dist/core/lenses/index.d.ts +6 -6
- package/dist/core/lenses/index.js +6 -6
- package/dist/core/lenses/memory.js +4 -17
- package/dist/core/lenses/numerical.d.ts +100 -0
- package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
- package/dist/core/lenses/point-cloud.d.ts +67 -0
- package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +226 -84
- package/dist/core/lenses/snap.d.ts +18 -0
- package/dist/core/lenses/snap.js +138 -0
- package/dist/core/lenses/text.d.ts +40 -0
- package/dist/core/lenses/text.js +202 -0
- package/dist/core/lifecycle.js +3 -6
- package/dist/core/linalg.js +5 -11
- package/dist/core/optic.d.ts +13 -0
- package/dist/core/optic.js +39 -0
- package/dist/core/optics.d.ts +10 -0
- package/dist/core/optics.js +26 -0
- package/dist/core/store.d.ts +9 -0
- package/dist/core/store.js +77 -0
- package/dist/core/traits.d.ts +4 -7
- package/dist/core/traits.js +8 -12
- package/dist/core/values/anchor.js +0 -4
- package/dist/core/values/arr.d.ts +110 -0
- package/dist/core/values/arr.js +336 -0
- package/dist/core/values/audio.d.ts +8 -9
- package/dist/core/values/audio.js +11 -28
- package/dist/core/values/bool.d.ts +11 -11
- package/dist/core/values/bool.js +12 -22
- package/dist/core/values/box.d.ts +15 -20
- package/dist/core/values/box.js +20 -33
- package/dist/core/values/canvas.d.ts +18 -25
- package/dist/core/values/canvas.js +32 -66
- package/dist/core/values/color.d.ts +5 -7
- package/dist/core/values/color.js +5 -11
- package/dist/core/values/field.d.ts +6 -7
- package/dist/core/values/field.js +10 -35
- package/dist/core/values/flags.d.ts +1 -2
- package/dist/core/values/flags.js +1 -17
- package/dist/core/values/gpu.d.ts +6 -10
- package/dist/core/values/gpu.js +8 -22
- package/dist/core/values/matrix.d.ts +2 -4
- package/dist/core/values/matrix.js +2 -12
- package/dist/core/values/num.d.ts +19 -28
- package/dist/core/values/num.js +23 -41
- package/dist/core/values/pose.d.ts +2 -4
- package/dist/core/values/pose.js +3 -12
- package/dist/core/values/range.d.ts +18 -26
- package/dist/core/values/range.js +22 -39
- package/dist/core/values/reg/ambiguity.d.ts +8 -0
- package/dist/core/values/reg/ambiguity.js +131 -0
- package/dist/core/values/reg/engine.d.ts +91 -0
- package/dist/core/values/reg/engine.js +373 -0
- package/dist/core/values/reg/nfa.d.ts +42 -0
- package/dist/core/values/reg/nfa.js +391 -0
- package/dist/core/values/reg/regex.d.ts +7 -0
- package/dist/core/values/reg/regex.js +318 -0
- package/dist/core/values/reg/types.d.ts +60 -0
- package/dist/core/values/reg/types.js +3 -0
- package/dist/core/values/reg.d.ts +250 -0
- package/dist/core/values/reg.js +649 -0
- package/dist/core/values/str.d.ts +16 -60
- package/dist/core/values/str.js +133 -315
- package/dist/core/values/template.js +1 -24
- package/dist/core/values/transform.d.ts +3 -5
- package/dist/core/values/transform.js +3 -12
- package/dist/core/values/tri.d.ts +9 -10
- package/dist/core/values/tri.js +9 -15
- package/dist/core/values/vec.d.ts +9 -24
- package/dist/core/values/vec.js +9 -64
- package/dist/formats/lens.js +6 -9
- package/dist/index.d.ts +0 -11
- package/dist/index.js +1 -11
- 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/schema/lens.js +5 -5
- 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/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 +6 -0
- package/dist/shapes/layout.d.ts +47 -1
- package/dist/shapes/layout.js +59 -1
- package/package.json +22 -1
- package/dist/coll.d.ts +0 -74
- package/dist/coll.js +0 -210
- package/dist/core/lenses/closed-form-policies.d.ts +0 -57
- package/dist/core/lenses/decompositions.d.ts +0 -14
- package/dist/core/lenses/decompositions.js +0 -224
- package/dist/core/lenses/domain-aggregates.d.ts +0 -42
- package/dist/core/lenses/domain-aggregates.js +0 -245
- package/dist/core/lenses/typed-factor.d.ts +0 -40
package/dist/core/cell.js
CHANGED
|
@@ -1,78 +1,94 @@
|
|
|
1
1
|
// cell.ts — symmetric bidirectional reactive engine.
|
|
2
2
|
//
|
|
3
|
-
// Forward propagation is alien-signals verbatim
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// STATIC adjacencies that mirror forward's `deps`/`subs`. The lens graph is
|
|
9
|
-
// stored both ways at construction: `_bwd.parent` is the down edge (a view's
|
|
10
|
-
// declared parents, the dual of `deps`) and `_lensSubs` is its transpose, the
|
|
11
|
-
// up edge (a cell's direct lens-children, the dual of `subs`). Backward is then
|
|
12
|
-
// the forward engine run on this reverse graph — one traversal each direction:
|
|
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:
|
|
13
8
|
//
|
|
14
9
|
// role forward (source → view) backward (view → source)
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// pull (resolve) checkDirty (up `deps`) resolveCone (up `_lensSubs`)
|
|
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`)
|
|
20
14
|
// commit/compute _update / getter writeBack
|
|
21
15
|
// "dirty" flag F.Dirty (source staged) BF.Dirty (view holds target)
|
|
22
16
|
// "pending" flag F.Pending (on the cone) BF.Pending (on the back-path)
|
|
23
17
|
//
|
|
24
|
-
// Forward flags live on `flags`, backward on a
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// `BF.Dirty`, the dual of a source's `F.Dirty`), descend the static backward
|
|
32
|
-
// path flagging each node down to its sources `BF.Pending` (the dual of
|
|
33
|
-
// `F.Pending`), and wake each source's forward cone so observers re-fire. No `put`
|
|
34
|
-
// runs, no source moves. The work is PULLED per read, source-CENTRIC: a read
|
|
35
|
-
// that reaches a back-marked cell `backResolve`s — for a source directly, or by
|
|
36
|
-
// DESCENDING a view's marked back-path to its sources first. Each source then
|
|
37
|
-
// `resolveCone`s: ASCEND `_lensSubs` through the `BF.Pending` cone to the armed
|
|
38
|
-
// views (the dual of `checkDirty` ascending `deps`) and `writeBack` each,
|
|
39
|
-
// applying its lens's `put` and staging the source via the SAME forward
|
|
40
|
-
// `_writeSource`. A source reflects ALL its writers (they compose into one
|
|
41
|
-
// committed value), so a source's cone resolves together and commits once.
|
|
42
|
-
// Because resolution follows only the sources the read DEPENDS on, reading one
|
|
43
|
-
// chain leaves unrelated sources armed (GRANULAR — does only the work a read
|
|
44
|
-
// demands); overlapping co-writers on a shared source all compose; an
|
|
45
|
-
// UNOBSERVED write does no backward work; re-writing a view before any read
|
|
46
|
-
// keeps only the last target. Reads pull at clean entry points
|
|
47
|
-
// (getter top, source `_update`/`_writeSource`, effect `_run`) — never
|
|
48
|
-
// mid-compute, so a source-reading `put` never re-enters a half-computed cell.
|
|
49
|
-
// Views are never sticky (a view is always `get(source)`; lossy lenses snap),
|
|
50
|
-
// no-op deltas short-circuit via equality (the GetPut law).
|
|
51
|
-
//
|
|
52
|
-
// The ONE irreducibly non-dual ingredient is fan-in accumulation: where the
|
|
53
|
-
// forward engine broadcasts one source to N readers, the backward engine
|
|
54
|
-
// ACCUMULATES N contributors into one value (the transpose of fan-out). A
|
|
55
|
-
// `merge` folds POST-ORDER inside `resolveCone`: its contributors are resolved
|
|
56
|
-
// first (each cascades a `put` into the node's `MergeNode`), then it folds ONCE
|
|
57
|
-
// by the node's policy (default last-writer-wins) and writes to its parent — the
|
|
58
|
-
// dual of a forward getter reading its deps then computing. Everything else —
|
|
59
|
-
// 1→1 chains, multi-parent splits (1→N / N→M, e.g. mean/diff), complement-
|
|
60
|
-
// carrying stateful lenses — resolves during the walk.
|
|
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`.
|
|
61
25
|
//
|
|
62
|
-
// Mode table —
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
//
|
|
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
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
44
|
+
let cycle = 0;
|
|
45
|
+
let runDepth = 0;
|
|
46
|
+
let batchDepth = 0;
|
|
47
|
+
let notifyIndex = 0;
|
|
48
|
+
let queuedLength = 0;
|
|
49
|
+
let activeSub;
|
|
50
|
+
let flushing = false;
|
|
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;
|
|
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 = [];
|
|
85
|
+
const EMPTY_DIRTY = new Set();
|
|
86
|
+
/** Fires on every source value-change. Backward writes reach it via `_writeSource`. */
|
|
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`.
|
|
76
92
|
const F = {
|
|
77
93
|
None: 0,
|
|
78
94
|
Mutable: 1,
|
|
@@ -85,31 +101,34 @@ const F = {
|
|
|
85
101
|
// Backward flag bits, on a Cell's own `bflags` word (the dual of `flags`).
|
|
86
102
|
const BF = {
|
|
87
103
|
None: 0,
|
|
88
|
-
/**
|
|
89
|
-
* in `pendingValue` (a getter cell never uses that field forward). The root
|
|
90
|
-
* of a pending back-write; `writeBack` consumes the target and clears it. */
|
|
104
|
+
/** Dual of `F.Dirty`: this view holds an unresolved back-target in `pendingValue`. */
|
|
91
105
|
Dirty: 1,
|
|
92
|
-
/**
|
|
93
|
-
* `BF.Dirty` view down to its sources. `markDown` sets it on descent; it gates
|
|
94
|
-
* `resolveCone`, which ascends `_lensSubs` only through `BF.Pending` nodes (so
|
|
95
|
-
* a read resolves only its own back-cone), and `writeBack` clears it on the
|
|
96
|
-
* way back down. */
|
|
106
|
+
/** Dual of `F.Pending`: this node is on the back-path to its sources. */
|
|
97
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,
|
|
98
112
|
};
|
|
99
|
-
// Named mask (legibility): a cell's whole backward state in one test.
|
|
100
113
|
/** Armed root OR on a back-path — i.e. a read must `backResolve` first. */
|
|
101
114
|
const BACK_MARKED = BF.Dirty | BF.Pending;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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.
|
|
113
132
|
/** Source (truth leaf): no forward derivation. */
|
|
114
133
|
function isSource(c) {
|
|
115
134
|
return c.getter === undefined;
|
|
@@ -118,68 +137,100 @@ function isSource(c) {
|
|
|
118
137
|
function isWritable(c) {
|
|
119
138
|
return c._bwd !== undefined;
|
|
120
139
|
}
|
|
121
|
-
/** Read-only derived: a `derive` with no backward path
|
|
122
|
-
* it; a sole parent has nowhere to land (the back-walk throws). */
|
|
140
|
+
/** Read-only derived: a `derive` with no backward path (back-walk throws on it). */
|
|
123
141
|
function isReadOnlyDerived(c) {
|
|
124
142
|
return !isSource(c) && !isWritable(c);
|
|
125
143
|
}
|
|
126
|
-
/** Forward primal a source-reading `bwd` linearizes at,
|
|
127
|
-
* recompute
|
|
128
|
-
*
|
|
129
|
-
* holds for any source state, so a stale primal still round-trips); unrealized
|
|
130
|
-
* derived (`Dirty`) → realize once via `.value`, seeding `currentValue`. */
|
|
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). */
|
|
131
147
|
function backPrimal(c) {
|
|
132
148
|
if (c.getter === undefined || c.flags & F.Dirty)
|
|
133
149
|
return c.value;
|
|
134
150
|
return c.currentValue;
|
|
135
151
|
}
|
|
136
|
-
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
145
177
|
return;
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
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;
|
|
149
185
|
else
|
|
150
|
-
|
|
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
|
+
}
|
|
151
233
|
}
|
|
152
|
-
let cycle = 0;
|
|
153
|
-
let runDepth = 0;
|
|
154
|
-
let batchDepth = 0;
|
|
155
|
-
let notifyIndex = 0;
|
|
156
|
-
let queuedLength = 0;
|
|
157
|
-
let activeSub;
|
|
158
|
-
let flushing = false;
|
|
159
|
-
/** A microtask is queued to flush the effect queue. Effects run ASYNCHRONOUSLY
|
|
160
|
-
* (end of the current microtask turn), so a burst of synchronous writes wakes
|
|
161
|
-
* each effect at most once — no `batch()` needed for coalescing. Reads stay
|
|
162
|
-
* synchronous and pull-based (a `.value` reflects writes immediately); only
|
|
163
|
-
* EFFECT side-effects defer. `settle()` forces a synchronous flush. */
|
|
164
|
-
let scheduled = false;
|
|
165
|
-
/** A `network` (constraint solver / explicit reactive sub-DAG) is in the queue.
|
|
166
|
-
* Networks are EAGER — a write that wakes one flushes the whole queue
|
|
167
|
-
* synchronously, so a read right after the write sees post-solve state (and any
|
|
168
|
-
* effect woken alongside runs after the network, in queue order). Only writes
|
|
169
|
-
* that wake effects ALONE defer to the microtask. */
|
|
170
|
-
let networkQueued = false;
|
|
171
|
-
/** Network running its body, if any. Source writes self-exclude it so a
|
|
172
|
-
* network reading+writing a cell doesn't re-trigger itself. */
|
|
173
|
-
let activeNetwork;
|
|
174
|
-
const queued = [];
|
|
175
|
-
/** Re-entrancy guard: while a back-resolve runs, a `put`'s source read commits
|
|
176
|
-
* normally (in-order composition) but must NOT trigger a nested resolve. */
|
|
177
|
-
let draining = false;
|
|
178
|
-
const EMPTY_DIRTY = new Set();
|
|
179
|
-
// Fires on every SOURCE value-change (the one place truth mutates).
|
|
180
|
-
// Backward writes reach it via `_writeSource`, attributing lens edits to
|
|
181
|
-
// the source they resolve to.
|
|
182
|
-
let writeHook;
|
|
183
234
|
/** Install a hook fired on every source value-change; returns a restore fn. */
|
|
184
235
|
export function setCellWriteHook(fn) {
|
|
185
236
|
const prev = writeHook;
|
|
@@ -188,6 +239,9 @@ export function setCellWriteHook(fn) {
|
|
|
188
239
|
writeHook = prev;
|
|
189
240
|
};
|
|
190
241
|
}
|
|
242
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
243
|
+
// Forward graph engine (internal) — alien-signals verbatim.
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
191
245
|
// alien-signals algorithm (verbatim): link / unlink / propagate / checkDirty.
|
|
192
246
|
function link(dep, sub, version) {
|
|
193
247
|
const prevDep = sub.depsTail;
|
|
@@ -202,6 +256,8 @@ function link(dep, sub, version) {
|
|
|
202
256
|
const prevSub = dep.subsTail;
|
|
203
257
|
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub)
|
|
204
258
|
return;
|
|
259
|
+
if (COUNTS)
|
|
260
|
+
counts.link++;
|
|
205
261
|
const isFirstSub = dep.subs === undefined;
|
|
206
262
|
const newLink = (sub.depsTail =
|
|
207
263
|
dep.subsTail =
|
|
@@ -232,6 +288,8 @@ function link(dep, sub, version) {
|
|
|
232
288
|
}
|
|
233
289
|
}
|
|
234
290
|
function unlink(l, sub = l.sub) {
|
|
291
|
+
if (COUNTS)
|
|
292
|
+
counts.unlink++;
|
|
235
293
|
const { dep, prevDep, nextDep, nextSub, prevSub } = l;
|
|
236
294
|
if (nextDep !== undefined)
|
|
237
295
|
nextDep.prevDep = prevDep;
|
|
@@ -252,13 +310,14 @@ function unlink(l, sub = l.sub) {
|
|
|
252
310
|
return nextDep;
|
|
253
311
|
}
|
|
254
312
|
function propagate(start, innerWrite, excluding) {
|
|
313
|
+
if (COUNTS)
|
|
314
|
+
counts.propagate++;
|
|
255
315
|
let l = start;
|
|
256
316
|
let next = start.nextSub;
|
|
257
317
|
let stack;
|
|
258
318
|
top: do {
|
|
259
319
|
const sub = l.sub;
|
|
260
|
-
// `excluding` skips one subscriber (
|
|
261
|
-
// writing a cell it subscribes to doesn't re-trigger itself).
|
|
320
|
+
// `excluding` skips one subscriber (a `network` not re-triggering itself).
|
|
262
321
|
if (sub !== excluding) {
|
|
263
322
|
let flags = sub.flags;
|
|
264
323
|
if (!(flags & (F.RecursedCheck | F.Recursed | F.Dirty | F.Pending))) {
|
|
@@ -309,6 +368,8 @@ function propagate(start, innerWrite, excluding) {
|
|
|
309
368
|
} while (true);
|
|
310
369
|
}
|
|
311
370
|
function checkDirty(startLink, startSub) {
|
|
371
|
+
if (COUNTS)
|
|
372
|
+
counts.checkDirty++;
|
|
312
373
|
let l = startLink, sub = startSub;
|
|
313
374
|
let stack;
|
|
314
375
|
let checkDepth = 0, dirty = false;
|
|
@@ -318,11 +379,10 @@ function checkDirty(startLink, startSub) {
|
|
|
318
379
|
if (sub.flags & F.Dirty)
|
|
319
380
|
dirty = true;
|
|
320
381
|
else if ((flags & (F.Mutable | F.Dirty)) === (F.Mutable | F.Dirty) ||
|
|
321
|
-
// A back-`Pending`
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
// getter and fall through to the `Pending` recurse below.
|
|
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`.
|
|
326
386
|
(flags & F.Mutable &&
|
|
327
387
|
dep.bflags & BF.Pending &&
|
|
328
388
|
isSource(dep))) {
|
|
@@ -350,12 +410,9 @@ function checkDirty(startLink, startSub) {
|
|
|
350
410
|
while (checkDepth--) {
|
|
351
411
|
l = stack.value;
|
|
352
412
|
stack = stack.prev;
|
|
353
|
-
// `dirty` tracks change down
|
|
354
|
-
// `F.Dirty` independently
|
|
355
|
-
//
|
|
356
|
-
// sees, since sources commit before the pull). Honor that bit too, else we'd
|
|
357
|
-
// clear its `F.Pending` below without recomputing — stranding a `Dirty` node
|
|
358
|
-
// whose observers never re-run.
|
|
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.
|
|
359
416
|
if (dirty || sub.flags & F.Dirty) {
|
|
360
417
|
const subs = sub.subs;
|
|
361
418
|
if (sub._update()) {
|
|
@@ -416,62 +473,49 @@ function disposeAllDepsInReverse(sub) {
|
|
|
416
473
|
}
|
|
417
474
|
class MergeNode {
|
|
418
475
|
foldFn;
|
|
419
|
-
/** Contributions gathered as
|
|
420
|
-
* in `foldMerge` (the merge-owned buffer, mutated in place). The parent it
|
|
421
|
-
* writes to is just `b.parent` (a merge's `b.parent` IS its fold target), so
|
|
422
|
-
* this node carries only the policy + buffer — no duplicate edge. */
|
|
476
|
+
/** Contributions gathered as the cone resolves; folded and cleared in `foldMerge`. */
|
|
423
477
|
contributions = [];
|
|
424
478
|
constructor(fold) {
|
|
425
479
|
this.foldFn = fold;
|
|
426
480
|
}
|
|
427
481
|
}
|
|
428
|
-
// BwdSpec — the backward sidecar
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
// path never touches them, and a plain node drops ~64 B. A cell is
|
|
436
|
-
// writable iff `_bwd !== undefined`.
|
|
437
|
-
//
|
|
438
|
-
// Two mode payloads hang off named fields rather than one union, so each
|
|
439
|
-
// stays distinctly typed: `merge` (the N→1 fold node) and `stateful` (the
|
|
440
|
-
// complement machinery of a complement-carrying lens). Both are rare, so
|
|
441
|
-
// a plain 1→1 / multi-out lens leaves them `undefined` and pays only
|
|
442
|
-
// `parent` + `put`.
|
|
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`.
|
|
443
489
|
class BwdSpec {
|
|
444
|
-
/**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
* lens is called as `put(target)` (a source-reading lens reads the current
|
|
448
|
-
* parent(s) at walk time, untracked); multi-out returns a per-parent update
|
|
449
|
-
* array. Stateful is the spec's `bwd`, called `put(target, sources, c)`. */
|
|
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). */
|
|
450
493
|
// biome-ignore lint/suspicious/noExplicitAny: put fn is opaque shape
|
|
451
494
|
put = undefined;
|
|
452
|
-
/**
|
|
495
|
+
/** Fold payload; present ⇒ a fan-in merge. */
|
|
453
496
|
merge = undefined;
|
|
454
|
-
/** Complement
|
|
497
|
+
/** Complement state; present ⇒ a complement-carrying (stateful) lens. */
|
|
455
498
|
stateful = undefined;
|
|
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;
|
|
456
502
|
}
|
|
457
|
-
/** Runtime state of a
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* 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. */
|
|
461
506
|
class StatefulCore {
|
|
462
507
|
/** Engine-owned memory the view discards. */
|
|
463
508
|
complement;
|
|
464
|
-
/** Advance the complement: `step(sources, complement
|
|
465
|
-
*
|
|
466
|
-
* is only ever read there — so it costs no slot here.) */
|
|
509
|
+
/** Advance the complement: `step(sources, complement)`. Run only when the
|
|
510
|
+
* sources actually moved (the engine gates it; see the stateful header). */
|
|
467
511
|
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
468
512
|
step;
|
|
469
|
-
/**
|
|
470
|
-
* live
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
|
|
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;
|
|
475
519
|
constructor(complement,
|
|
476
520
|
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
477
521
|
step) {
|
|
@@ -479,6 +523,13 @@ class StatefulCore {
|
|
|
479
523
|
this.step = step;
|
|
480
524
|
}
|
|
481
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");
|
|
482
533
|
/** Snapshot a `Val<T>` to plain `T` (one-shot, no tracking). */
|
|
483
534
|
export function readNow(v) {
|
|
484
535
|
if (v instanceof Cell)
|
|
@@ -508,6 +559,9 @@ export const isCell = (v) => v instanceof Cell;
|
|
|
508
559
|
export const isLens = (v) => v instanceof Cell && v.getter !== undefined && v._bwd !== undefined;
|
|
509
560
|
/** Read-only mode: derived with no backward path. */
|
|
510
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
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
511
565
|
export class Cell {
|
|
512
566
|
/** @internal */
|
|
513
567
|
flags;
|
|
@@ -533,14 +587,28 @@ export class Cell {
|
|
|
533
587
|
pendingValue;
|
|
534
588
|
/** @internal Backward sidecar; `undefined` iff read-only. Writability is `_bwd !== undefined`. */
|
|
535
589
|
_bwd;
|
|
536
|
-
/** @internal
|
|
537
|
-
|
|
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;
|
|
538
600
|
/** @internal Backward flag word (`BF`), dual of forward `flags`. */
|
|
539
601
|
bflags;
|
|
540
|
-
/** @internal
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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.
|
|
544
612
|
constructor(initial, opts) {
|
|
545
613
|
this.flags = F.Mutable;
|
|
546
614
|
this.subs = undefined;
|
|
@@ -554,9 +622,14 @@ export class Cell {
|
|
|
554
622
|
this.currentValue = initial;
|
|
555
623
|
this.pendingValue = initial;
|
|
556
624
|
this._bwd = undefined;
|
|
557
|
-
this.
|
|
625
|
+
this.parentEdges = undefined;
|
|
626
|
+
this.parentEdgesTail = undefined;
|
|
627
|
+
this.childEdges = undefined;
|
|
628
|
+
this.childEdgesTail = undefined;
|
|
558
629
|
this.bflags = BF.None;
|
|
559
|
-
this.
|
|
630
|
+
this.bEpoch = 0;
|
|
631
|
+
this.version = 0;
|
|
632
|
+
this.name = undefined;
|
|
560
633
|
if (opts !== undefined) {
|
|
561
634
|
if (opts.equals !== undefined)
|
|
562
635
|
this._equals = opts.equals;
|
|
@@ -564,23 +637,28 @@ export class Cell {
|
|
|
564
637
|
this._watched = opts.watched;
|
|
565
638
|
if (opts.unwatched !== undefined)
|
|
566
639
|
this._unwatchedHook = opts.unwatched;
|
|
640
|
+
if (opts.name !== undefined)
|
|
641
|
+
this.name = opts.name;
|
|
567
642
|
}
|
|
568
643
|
}
|
|
569
644
|
/** @internal Single write-commit point; self-excludes the active network. */
|
|
570
645
|
_writeSource(next) {
|
|
571
|
-
//
|
|
572
|
-
// it FIRST, so the later forward write wins (LWW).
|
|
646
|
+
// Resolve any pending back-write first, so the later forward write wins (LWW).
|
|
573
647
|
if (this.bflags & BF.Pending && !draining)
|
|
574
648
|
backResolve(this);
|
|
575
649
|
const prev = this.pendingValue;
|
|
576
650
|
this.pendingValue = next;
|
|
577
651
|
if (!this._equals(prev, next)) {
|
|
652
|
+
this.version++; // stamp the change for stateful-lens provenance (sum-of-versions)
|
|
578
653
|
this.flags = F.Mutable | F.Dirty;
|
|
579
654
|
if (writeHook !== undefined)
|
|
580
655
|
writeHook(this);
|
|
581
656
|
const subs = this.subs;
|
|
582
657
|
if (subs !== undefined) {
|
|
583
|
-
|
|
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);
|
|
584
662
|
autoFlush();
|
|
585
663
|
}
|
|
586
664
|
}
|
|
@@ -588,6 +666,8 @@ export class Cell {
|
|
|
588
666
|
/** @internal */
|
|
589
667
|
_update() {
|
|
590
668
|
if (this.getter !== undefined) {
|
|
669
|
+
if (COUNTS)
|
|
670
|
+
counts.recompute++;
|
|
591
671
|
this.depsTail = undefined;
|
|
592
672
|
this.flags = F.Mutable | F.RecursedCheck;
|
|
593
673
|
const prev = activeSub;
|
|
@@ -598,7 +678,10 @@ export class Cell {
|
|
|
598
678
|
const old = this.currentValue;
|
|
599
679
|
const next = (this.currentValue = this.getter());
|
|
600
680
|
threw = false;
|
|
601
|
-
|
|
681
|
+
const changed = !this._equals(old, next);
|
|
682
|
+
if (changed)
|
|
683
|
+
this.version++; // derived commit: stamp for stateful provenance
|
|
684
|
+
return changed;
|
|
602
685
|
}
|
|
603
686
|
finally {
|
|
604
687
|
activeSub = prev;
|
|
@@ -606,10 +689,8 @@ export class Cell {
|
|
|
606
689
|
purgeDeps(this);
|
|
607
690
|
}
|
|
608
691
|
}
|
|
609
|
-
// A back-`Pending` source
|
|
610
|
-
//
|
|
611
|
-
// `_writeSource`) — so its `pendingValue` reflects the back-write before we
|
|
612
|
-
// commit it.
|
|
692
|
+
// A back-`Pending` source resolves its armed back-write first, so
|
|
693
|
+
// `pendingValue` reflects it before we commit.
|
|
613
694
|
if (this.bflags & BF.Pending && !draining)
|
|
614
695
|
backResolve(this);
|
|
615
696
|
this.flags = F.Mutable;
|
|
@@ -621,6 +702,17 @@ export class Cell {
|
|
|
621
702
|
_notify() { }
|
|
622
703
|
/** @internal */
|
|
623
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
|
+
}
|
|
624
716
|
if (this.getter !== undefined && this.depsTail !== undefined) {
|
|
625
717
|
this.flags = F.Mutable | F.Dirty;
|
|
626
718
|
disposeAllDepsInReverse(this);
|
|
@@ -641,22 +733,29 @@ export class Cell {
|
|
|
641
733
|
}
|
|
642
734
|
// Construction helpers build via `new this()` so a subclass static
|
|
643
735
|
// (`Vec.lens(...)`) yields a `Vec` with its constructor-set equality.
|
|
644
|
-
// Every lens has a structural backward target (`_bwd.parent`), which is
|
|
645
|
-
// what makes the backward pass well-defined.
|
|
646
736
|
/** Endomorphic lens. A 2-arg `bwd(view, current)` consults the current
|
|
647
737
|
* source; a 1-arg `bwd(view)` reconstructs it from the view alone. */
|
|
648
738
|
lens(fwd, bwd) {
|
|
649
|
-
return
|
|
739
|
+
return buildLens(this.constructor, [this, fwd, bwd]);
|
|
650
740
|
}
|
|
651
741
|
/** Read-only same-type view: the RO dual of the endo `.lens`. For a cross-type view use the typed static
|
|
652
742
|
* `Target.derive(src, fn)`. */
|
|
653
743
|
derive(fn) {
|
|
654
744
|
return buildDerived(this.constructor, () => fn(this.value));
|
|
655
745
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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: forwards its parent's value unchanged; on write, folds N
|
|
758
|
+
* contributors into one value. `fold` defaults to last-writer-wins. */
|
|
660
759
|
merge(fold) {
|
|
661
760
|
if (this.getter !== undefined && this._bwd === undefined) {
|
|
662
761
|
throw new TypeError("merge: receiver is read-only");
|
|
@@ -666,27 +765,26 @@ export class Cell {
|
|
|
666
765
|
cell.flags = F.Mutable | F.Dirty;
|
|
667
766
|
cell.getter = () => parent.value;
|
|
668
767
|
const b = (cell._bwd = new BwdSpec());
|
|
669
|
-
b.parent = parent;
|
|
670
768
|
b.merge = new MergeNode(fold);
|
|
769
|
+
linkLens(cell, parent, 0);
|
|
770
|
+
setWriteBlocked(cell);
|
|
671
771
|
return cell;
|
|
672
772
|
}
|
|
673
773
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
674
774
|
static derive(...args) {
|
|
675
|
-
return
|
|
775
|
+
return buildDerive(this, args);
|
|
676
776
|
}
|
|
677
777
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
678
778
|
static lens(...args) {
|
|
679
|
-
return
|
|
779
|
+
return buildLens(this, args);
|
|
680
780
|
}
|
|
681
781
|
/** Type predicate against this class: `Vec.is(x)` narrows `x` to `Vec`.
|
|
682
782
|
* Inherited static; works for any subclass via polymorphic `this`. */
|
|
683
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
684
783
|
static is(v) {
|
|
685
784
|
return v instanceof this;
|
|
686
785
|
}
|
|
687
786
|
/** Coerce `Val<Inner<Cls>>` → `Cls`: instance → identity, RO cell →
|
|
688
787
|
* tracked `derive`, literal → fresh seed. */
|
|
689
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
690
788
|
static coerce(v) {
|
|
691
789
|
if (v instanceof this)
|
|
692
790
|
return v;
|
|
@@ -698,20 +796,17 @@ export class Cell {
|
|
|
698
796
|
}
|
|
699
797
|
/** Writable-shaped constant: always reads `v`, absorbs writes
|
|
700
798
|
* (parentless sink lens), for APIs demanding bidirectionality. */
|
|
701
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
702
799
|
static pin(v) {
|
|
703
800
|
const cell = new this();
|
|
704
801
|
cell.flags = F.Mutable | F.Dirty;
|
|
705
802
|
cell.getter = () => v;
|
|
706
|
-
// Parentless `_bwd`: `writeBack` absorbs
|
|
707
|
-
// `put`, so the sink needs no closure — writability is just `_bwd !== undefined`.
|
|
803
|
+
// Parentless `_bwd`: `writeBack` absorbs it (no parent edges, no closure).
|
|
708
804
|
cell._bwd = new BwdSpec();
|
|
709
805
|
return cell;
|
|
710
806
|
}
|
|
711
807
|
}
|
|
712
808
|
/** Typed field lens onto `parent.value[key]`. RO parent → RO derive;
|
|
713
809
|
* writable parent → bidirectional lens with spread-replace `put`. */
|
|
714
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
715
810
|
export function fieldOf(
|
|
716
811
|
// biome-ignore lint/suspicious/noExplicitAny: parent is contravariant on put
|
|
717
812
|
parent, key, Cls) {
|
|
@@ -721,7 +816,17 @@ parent, key, Cls) {
|
|
|
721
816
|
if (ro) {
|
|
722
817
|
return buildDerived(ctor, () => get(parent.value));
|
|
723
818
|
}
|
|
724
|
-
|
|
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]);
|
|
725
830
|
}
|
|
726
831
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
727
832
|
function buildDerived(Cls, getter) {
|
|
@@ -730,50 +835,95 @@ function buildDerived(Cls, getter) {
|
|
|
730
835
|
cell.flags = F.Mutable | F.Dirty;
|
|
731
836
|
return cell;
|
|
732
837
|
}
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
cell.getter = (() => fwd(parent.value));
|
|
738
|
-
const b = (cell._bwd = new BwdSpec());
|
|
739
|
-
// Source-reading lenses linearize at the parent's primal (`backPrimal`: the
|
|
740
|
-
// last-settled value for a derived, the staged truth for a source) so the
|
|
741
|
-
// engine always calls the 1-arg form (no arity branch) and never recomputes a
|
|
742
|
-
// derived parent's cone just to read it back.
|
|
743
|
-
b.put = readsSource ? (t) => bwd(t, backPrimal(parent)) : bwd;
|
|
744
|
-
b.parent = parent;
|
|
745
|
-
return cell;
|
|
746
|
-
}
|
|
747
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
748
|
-
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) {
|
|
749
842
|
const n = parents.length;
|
|
750
843
|
const vals = new Array(n);
|
|
751
|
-
|
|
752
|
-
cell.flags = F.Mutable | F.Dirty;
|
|
753
|
-
cell.getter = (() => {
|
|
844
|
+
return () => {
|
|
754
845
|
for (let i = 0; i < n; i++)
|
|
755
846
|
vals[i] = parents[i].value;
|
|
756
847
|
return fwd(vals);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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;
|
|
772
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;
|
|
894
|
+
const b = (cell._bwd = new BwdSpec());
|
|
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);
|
|
916
|
+
}
|
|
773
917
|
}
|
|
774
918
|
else {
|
|
775
|
-
|
|
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);
|
|
776
925
|
}
|
|
926
|
+
setWriteBlocked(cell);
|
|
777
927
|
return cell;
|
|
778
928
|
}
|
|
779
929
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
@@ -788,36 +938,41 @@ spec) {
|
|
|
788
938
|
const seed = new Array(n);
|
|
789
939
|
for (let i = 0; i < n; i++)
|
|
790
940
|
seed[i] = parents[i].peek();
|
|
791
|
-
|
|
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;
|
|
792
950
|
const fwd = spec.fwd;
|
|
793
951
|
b.put = spec.bwd;
|
|
794
|
-
b.
|
|
952
|
+
b.scatter = true;
|
|
953
|
+
for (let i = 0; i < n; i++)
|
|
954
|
+
linkLens(cell, parents[i], i);
|
|
795
955
|
cell.getter = (() => {
|
|
796
|
-
|
|
956
|
+
let ver = 0;
|
|
957
|
+
for (let i = 0; i < n; i++) {
|
|
797
958
|
vals[i] = parents[i].value;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
external = true;
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
959
|
+
ver += parents[i].version;
|
|
960
|
+
}
|
|
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;
|
|
810
967
|
}
|
|
811
|
-
sc.complement = sc.step(vals, sc.complement, external);
|
|
812
968
|
return fwd(vals, sc.complement);
|
|
813
969
|
});
|
|
970
|
+
setWriteBlocked(cell);
|
|
814
971
|
return cell;
|
|
815
972
|
}
|
|
816
|
-
// Single-source stateful
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
// `vals` still feeds the closures and `b.parent` stays an array for the
|
|
820
|
-
// 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.
|
|
821
976
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
822
977
|
function buildStateful1(Cls, parent,
|
|
823
978
|
// biome-ignore lint/suspicious/noExplicitAny: opaque spec
|
|
@@ -825,61 +980,54 @@ spec) {
|
|
|
825
980
|
const cell = new Cls();
|
|
826
981
|
cell.flags = F.Mutable | F.Dirty;
|
|
827
982
|
const b = (cell._bwd = new BwdSpec());
|
|
828
|
-
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`)
|
|
829
987
|
const fwd = spec.fwd;
|
|
830
988
|
b.put = spec.bwd;
|
|
831
|
-
|
|
832
|
-
|
|
989
|
+
// `scatter` stays false: writeBack routes this through the scalar stateful branch.
|
|
990
|
+
linkLens(cell, parent, 0);
|
|
833
991
|
cell.getter = (() => {
|
|
834
|
-
const
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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);
|
|
839
1001
|
});
|
|
1002
|
+
setWriteBlocked(cell);
|
|
840
1003
|
return cell;
|
|
841
1004
|
}
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
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) {
|
|
849
1011
|
if (args.length === 1)
|
|
850
|
-
return buildDerived(
|
|
851
|
-
const
|
|
1012
|
+
return buildDerived(Cls, args[0]);
|
|
1013
|
+
const parent = args[0];
|
|
1014
|
+
const fn = args[1];
|
|
852
1015
|
if (Array.isArray(parent))
|
|
853
|
-
return
|
|
854
|
-
return buildDerived(
|
|
1016
|
+
return buildDerived(Cls, arrayGetter(parent, fn));
|
|
1017
|
+
return buildDerived(Cls, () => fn(parent.value));
|
|
855
1018
|
}
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
const [parent, a, b] = args;
|
|
859
|
-
if (args.length === 2) {
|
|
860
|
-
const ps = Array.isArray(parent) ? parent : [parent];
|
|
861
|
-
return ps.length === 1 ? buildStateful1(ctor, ps[0], a) : buildStateful(ctor, ps, a);
|
|
862
|
-
}
|
|
863
|
-
const readsSource = b.length >= 2;
|
|
864
|
-
if (Array.isArray(parent))
|
|
865
|
-
return buildLensN(ctor, parent, a, b, readsSource);
|
|
866
|
-
return buildLens1(ctor, parent, a, b, readsSource);
|
|
867
|
-
}
|
|
868
|
-
// Installed on the prototype (not a class accessor): V8 JITs a prototype getter
|
|
869
|
-
// better, and it keeps the field-only class shape for a stable hidden class.
|
|
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.
|
|
870
1021
|
Object.defineProperty(Cell.prototype, "value", {
|
|
871
1022
|
get() {
|
|
872
|
-
// Reading is the PULL
|
|
873
|
-
//
|
|
874
|
-
// half-computed cell. `backResolve` descends `_bwd` to the sources and pulls
|
|
875
|
-
// the writers registered there, touching only this cell's back-cone
|
|
876
|
-
// (granular — sibling chains untouched).
|
|
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.
|
|
877
1025
|
if (this.bflags & BACK_MARKED && !draining)
|
|
878
1026
|
backResolve(this);
|
|
879
1027
|
const flags = this.flags;
|
|
880
1028
|
if (this.getter !== undefined) {
|
|
881
1029
|
if (flags & F.RecursedCheck) {
|
|
882
|
-
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`);
|
|
883
1031
|
}
|
|
884
1032
|
if (flags & F.Dirty ||
|
|
885
1033
|
(flags & F.Pending &&
|
|
@@ -918,11 +1066,10 @@ Object.defineProperty(Cell.prototype, "value", {
|
|
|
918
1066
|
if (b === undefined) {
|
|
919
1067
|
throw new TypeError("Cannot write to a computed");
|
|
920
1068
|
}
|
|
921
|
-
// GetPut for a multi-parent split:
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
if (Array.isArray(b.parent) && b.stateful === undefined && this._equals(next, this.peek())) {
|
|
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())) {
|
|
926
1073
|
return;
|
|
927
1074
|
}
|
|
928
1075
|
arm(this, next);
|
|
@@ -930,32 +1077,43 @@ Object.defineProperty(Cell.prototype, "value", {
|
|
|
930
1077
|
enumerable: false,
|
|
931
1078
|
configurable: false,
|
|
932
1079
|
});
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
*
|
|
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. */
|
|
938
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");
|
|
1093
|
+
}
|
|
1094
|
+
if (COUNTS)
|
|
1095
|
+
counts.arm++;
|
|
939
1096
|
if (!(node.bflags & BF.Dirty)) {
|
|
940
|
-
markDown(node); // flag path + wake cones
|
|
1097
|
+
markDown(node); // flag path + wake cones FIRST (a throw arms nothing)
|
|
941
1098
|
node.bflags |= BF.Dirty;
|
|
942
1099
|
}
|
|
943
|
-
node.pendingValue = target;
|
|
1100
|
+
node.pendingValue = target;
|
|
944
1101
|
autoFlush();
|
|
945
1102
|
}
|
|
946
|
-
/** MARK (push), dual of
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
* `put` — an over-approximation `resolveCone` later resolves precisely.
|
|
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`.
|
|
950
1106
|
*
|
|
951
|
-
* `BF.Pending`
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
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. */
|
|
955
1111
|
function markDown(start) {
|
|
956
1112
|
let node = start;
|
|
957
1113
|
let stack;
|
|
958
1114
|
for (;;) {
|
|
1115
|
+
if (COUNTS)
|
|
1116
|
+
counts.markDownVisit++;
|
|
959
1117
|
let next;
|
|
960
1118
|
if (isSource(node)) {
|
|
961
1119
|
// Leaf (dual of a `Dirty` source): wake its cone ONCE.
|
|
@@ -963,40 +1121,26 @@ function markDown(start) {
|
|
|
963
1121
|
node.bflags |= BF.Pending;
|
|
964
1122
|
const subs = node.subs;
|
|
965
1123
|
if (subs !== undefined)
|
|
966
|
-
propagate(subs, runDepth > 0,
|
|
1124
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
967
1125
|
}
|
|
968
1126
|
}
|
|
969
1127
|
else if (node === start || !(node.bflags & BF.Pending)) {
|
|
970
|
-
// On the back-path
|
|
971
|
-
//
|
|
1128
|
+
// On the back-path. An already-marked intermediate (≠ start) has its
|
|
1129
|
+
// subtree marked — stop (diamond dedup).
|
|
972
1130
|
if (node !== start)
|
|
973
1131
|
node.bflags |= BF.Pending;
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
throw new TypeError("Cannot write through to a computed");
|
|
987
|
-
}
|
|
988
|
-
else if (next === undefined)
|
|
989
|
-
next = p;
|
|
990
|
-
else
|
|
991
|
-
(stack ??= []).push(p);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
else if (isReadOnlyDerived(parent)) {
|
|
995
|
-
throw new TypeError("Cannot write through to a computed");
|
|
996
|
-
}
|
|
997
|
-
else {
|
|
998
|
-
next = parent;
|
|
999
|
-
}
|
|
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);
|
|
1000
1144
|
}
|
|
1001
1145
|
}
|
|
1002
1146
|
if (next !== undefined) {
|
|
@@ -1010,115 +1154,133 @@ function markDown(start) {
|
|
|
1010
1154
|
}
|
|
1011
1155
|
}
|
|
1012
1156
|
}
|
|
1013
|
-
/** RESOLVE (pull), dual of
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
* so a call on the source resolves every co-writer together and commits once.
|
|
1018
|
-
* `BF.Dirty` is the dedup.
|
|
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.
|
|
1019
1161
|
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
|
|
1162
|
+
* Iterative post-order over the back-cone, via an explicit frame stack of
|
|
1163
|
+
* {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;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
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);
|
|
1203
|
+
}
|
|
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++;
|
|
1025
1209
|
const b = node._bwd;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
merge.contributions.length = 0; // self-heal contributions left by a prior throw
|
|
1029
|
-
// Drive this node's own armed target first (composes before child writes —
|
|
1030
|
-
// preserves last-writer-wins order among co-writers of a shared parent).
|
|
1210
|
+
if (b !== undefined && b.merge !== undefined)
|
|
1211
|
+
b.merge.contributions.length = 0;
|
|
1031
1212
|
if (node.bflags & BF.Dirty) {
|
|
1032
1213
|
node.bflags &= ~BF.Dirty;
|
|
1033
1214
|
writeBack(node, node.pendingValue);
|
|
1034
1215
|
}
|
|
1035
|
-
const children = node._lensSubs;
|
|
1036
|
-
if (children !== undefined) {
|
|
1037
|
-
for (let i = 0; i < children.length; i++) {
|
|
1038
|
-
const c = children[i];
|
|
1039
|
-
if (c.bflags & BACK_MARKED)
|
|
1040
|
-
resolveCone(c);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
node.bflags &= ~BF.Pending; // resolved (idempotent with `writeBack`'s clear)
|
|
1044
|
-
if (merge !== undefined)
|
|
1045
|
-
foldMerge(b.parent, merge); // contributors in → fold once
|
|
1046
1216
|
}
|
|
1047
|
-
/** PULL entry for a back-marked `start`.
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
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). */
|
|
1052
1225
|
function backResolve(start) {
|
|
1053
1226
|
draining = true;
|
|
1054
1227
|
++batchDepth;
|
|
1055
1228
|
const prev = activeSub;
|
|
1056
1229
|
activeSub = undefined;
|
|
1230
|
+
const sourcesBase = backSources.length;
|
|
1231
|
+
const epoch = ++backCycle;
|
|
1057
1232
|
try {
|
|
1058
1233
|
if (isSource(start)) {
|
|
1059
1234
|
resolveCone(start);
|
|
1235
|
+
return;
|
|
1060
1236
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
reached = true;
|
|
1077
|
-
if (p.bflags & BF.Pending)
|
|
1078
|
-
resolveCone(p);
|
|
1079
|
-
}
|
|
1080
|
-
else if (p.bflags & BF.Pending) {
|
|
1081
|
-
if (next === undefined)
|
|
1082
|
-
next = p;
|
|
1083
|
-
else
|
|
1084
|
-
(stack ??= []).push(p);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
else if (isSource(parent)) {
|
|
1089
|
-
reached = true;
|
|
1090
|
-
if (parent.bflags & BF.Pending)
|
|
1091
|
-
resolveCone(parent);
|
|
1092
|
-
}
|
|
1093
|
-
else if (parent.bflags & BF.Pending) {
|
|
1094
|
-
next = parent;
|
|
1095
|
-
}
|
|
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);
|
|
1096
1252
|
}
|
|
1097
|
-
if (next
|
|
1098
|
-
|
|
1099
|
-
else if (stack !== undefined && stack.length > 0)
|
|
1100
|
-
node = stack.pop();
|
|
1253
|
+
else if (next === undefined)
|
|
1254
|
+
next = p;
|
|
1101
1255
|
else
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
if (!reached && start.bflags & BF.Dirty) {
|
|
1105
|
-
start.bflags &= ~BF.Dirty;
|
|
1106
|
-
writeBack(start, start.pendingValue);
|
|
1256
|
+
(stack ??= []).push(p);
|
|
1107
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);
|
|
1108
1271
|
}
|
|
1109
1272
|
}
|
|
1110
1273
|
finally {
|
|
1274
|
+
backSources.length = sourcesBase;
|
|
1111
1275
|
activeSub = prev;
|
|
1112
1276
|
--batchDepth;
|
|
1113
1277
|
draining = false;
|
|
1114
1278
|
}
|
|
1115
1279
|
}
|
|
1116
|
-
/** Resolve any back-write a woken
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
* back
|
|
1120
|
-
* node's back-marked deps here makes that view-change visible before the
|
|
1121
|
-
* dirtiness check. Granular: only this node's own deps. */
|
|
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. */
|
|
1122
1284
|
function resolveBackDeps(node) {
|
|
1123
1285
|
for (let l = node.deps; l !== undefined; l = l.nextDep) {
|
|
1124
1286
|
const d = l.dep;
|
|
@@ -1126,116 +1288,177 @@ function resolveBackDeps(node) {
|
|
|
1126
1288
|
backResolve(d);
|
|
1127
1289
|
}
|
|
1128
1290
|
}
|
|
1129
|
-
/** Backward commit/compute (dual of
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
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 — bounded by pooled stack memory, not the call stack. */
|
|
1136
1299
|
function writeBack(node, target) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1300
|
+
wbNode[0] = node;
|
|
1301
|
+
wbTarget[0] = target;
|
|
1302
|
+
let top = 1;
|
|
1303
|
+
let sTop = 0;
|
|
1304
|
+
while (top > 0) {
|
|
1305
|
+
if (COUNTS)
|
|
1306
|
+
counts.writeBackVisit++;
|
|
1307
|
+
const cur = wbNode[--top];
|
|
1308
|
+
const tgt = wbTarget[top];
|
|
1309
|
+
if (isSource(cur)) {
|
|
1310
|
+
cur._writeSource(tgt); // staged now, visible to later siblings
|
|
1311
|
+
// Clear this source's `BF.Pending`, then re-assert iff a lens-child is STILL
|
|
1312
|
+
// armed (an overlapping co-writer) — else that write is lost, and leaving it
|
|
1313
|
+
// set unconditionally would strand `BF.Pending` on every fan-in source.
|
|
1314
|
+
// Scan from the TAIL: `resolveCone` drives children head→tail, so the last
|
|
1315
|
+
// still-armed co-writer sits near the tail — found in O(1) until the final
|
|
1316
|
+
// one, turning a fan-in's re-assert from O(N²) into O(N). (Order is
|
|
1317
|
+
// irrelevant; this is a find-any.)
|
|
1318
|
+
cur.bflags &= ~BF.Pending;
|
|
1319
|
+
for (let e = cur.childEdgesTail; e !== undefined; e = e.prevChild) {
|
|
1320
|
+
if (COUNTS)
|
|
1321
|
+
counts.reassertScan++;
|
|
1322
|
+
if (e.child.bflags & BACK_MARKED) {
|
|
1323
|
+
cur.bflags |= BF.Pending;
|
|
1151
1324
|
break;
|
|
1152
1325
|
}
|
|
1153
|
-
}
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
node.bflags &= ~BF.Pending; // passing through clears the path marker
|
|
1157
|
-
const b = node._bwd;
|
|
1158
|
-
if (b === undefined)
|
|
1159
|
-
throw new TypeError("Cannot write through to a computed");
|
|
1160
|
-
if (b.merge !== undefined) {
|
|
1161
|
-
b.merge.contributions.push(target); // gathered here; `resolveCone` folds post-order
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
const parent = b.parent;
|
|
1165
|
-
if (parent === undefined)
|
|
1166
|
-
return; // pin sink: absorb
|
|
1167
|
-
if (Array.isArray(parent)) {
|
|
1168
|
-
const n = parent.length;
|
|
1169
|
-
let out;
|
|
1170
|
-
const sc = b.stateful;
|
|
1171
|
-
if (sc !== undefined) {
|
|
1172
|
-
const vals = new Array(n);
|
|
1173
|
-
for (let i = 0; i < n; i++)
|
|
1174
|
-
vals[i] = parent[i].value;
|
|
1175
|
-
// Bring the complement to the current sources before `bwd` (the dual of
|
|
1176
|
-
// the forward getter's step) so it measures devs/fracs from a prior
|
|
1177
|
-
// sibling write, not a stale snapshot. External unless the sources still
|
|
1178
|
-
// equal this lens's own last back-write.
|
|
1179
|
-
const last = sc.last;
|
|
1180
|
-
let external = last === undefined;
|
|
1181
|
-
if (last !== undefined) {
|
|
1182
|
-
for (let i = 0; i < n; i++)
|
|
1183
|
-
if (vals[i] !== last[i]) {
|
|
1184
|
-
external = true;
|
|
1185
|
-
break;
|
|
1186
|
-
}
|
|
1187
1326
|
}
|
|
1188
|
-
|
|
1189
|
-
const res = b.put(target, vals, sc.complement);
|
|
1190
|
-
const upd = res.updates;
|
|
1191
|
-
// Build the committed candidate IN `vals` (its source reads are spent) and
|
|
1192
|
-
// keep it as `last` for the next own-vs-external comparison. A `SKIP` slot —
|
|
1193
|
-
// or a slot past a short `upd` — leaves that parent at its current `vals[i]`.
|
|
1194
|
-
const um = upd.length < n ? upd.length : n;
|
|
1195
|
-
for (let i = 0; i < um; i++)
|
|
1196
|
-
if (upd[i] !== SKIP)
|
|
1197
|
-
vals[i] = upd[i];
|
|
1198
|
-
sc.complement = sc.step(vals, res.complement, false);
|
|
1199
|
-
sc.last = vals;
|
|
1200
|
-
out = upd;
|
|
1327
|
+
continue;
|
|
1201
1328
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1329
|
+
cur.bflags &= ~BF.Pending; // passing through clears the path marker
|
|
1330
|
+
const b = cur._bwd;
|
|
1331
|
+
if (b === undefined)
|
|
1332
|
+
throw new TypeError("Cannot write through to a computed");
|
|
1333
|
+
const mn = b.merge;
|
|
1334
|
+
if (mn !== undefined) {
|
|
1335
|
+
mn.contributions.push(tgt); // gathered here; `resolveCone` folds post-order
|
|
1336
|
+
continue;
|
|
1204
1337
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
const
|
|
1338
|
+
const pe = cur.parentEdges;
|
|
1339
|
+
if (pe === undefined)
|
|
1340
|
+
continue; // pin sink (parentless): absorb
|
|
1341
|
+
const sc = b.stateful;
|
|
1342
|
+
if (sc !== undefined && !b.scatter) {
|
|
1343
|
+
// Single-source stateful fast-path (scalar `bwd`); one index-0 parent edge.
|
|
1344
|
+
const p = pe.parent;
|
|
1345
|
+
const x = p.value;
|
|
1346
|
+
const ver = p.version;
|
|
1347
|
+
if (ver !== sc.stamp) {
|
|
1348
|
+
if (COUNTS)
|
|
1349
|
+
counts.step++;
|
|
1350
|
+
sc.complement = sc.step(x, sc.complement);
|
|
1351
|
+
}
|
|
1352
|
+
if (COUNTS)
|
|
1353
|
+
counts.put++;
|
|
1354
|
+
const res = b.put(tgt, x, sc.complement);
|
|
1355
|
+
sc.complement = res.complement;
|
|
1356
|
+
wbStateful[sTop++] = cur;
|
|
1357
|
+
const u = res.update;
|
|
1212
1358
|
if (u !== SKIP) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1359
|
+
wbNode[top] = p;
|
|
1360
|
+
wbTarget[top] = u;
|
|
1361
|
+
top++;
|
|
1215
1362
|
}
|
|
1363
|
+
else {
|
|
1364
|
+
// Stash: the view moved through the complement alone (see the scatter case).
|
|
1365
|
+
cur.flags |= F.Dirty;
|
|
1366
|
+
const subs = cur.subs;
|
|
1367
|
+
if (subs !== undefined)
|
|
1368
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
1369
|
+
}
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (b.scatter) {
|
|
1373
|
+
// Gather ordered parents (index-ordered edges) for the tuple `put`.
|
|
1374
|
+
let n = 0;
|
|
1375
|
+
for (let e = pe; e !== undefined; e = e.nextParent)
|
|
1376
|
+
n++;
|
|
1377
|
+
const parents = new Array(n);
|
|
1378
|
+
for (let e = pe; e !== undefined; e = e.nextParent)
|
|
1379
|
+
parents[e.index] = e.parent;
|
|
1380
|
+
let out;
|
|
1381
|
+
if (sc !== undefined) {
|
|
1382
|
+
const vals = new Array(n);
|
|
1383
|
+
let ver = 0;
|
|
1384
|
+
for (let i = 0; i < n; i++) {
|
|
1385
|
+
vals[i] = parents[i].value;
|
|
1386
|
+
ver += parents[i].version;
|
|
1387
|
+
}
|
|
1388
|
+
// Refresh the complement only if a source moved since the last sync — e.g.
|
|
1389
|
+
// a prior sibling co-writer bumped a shared source. A pure own re-write
|
|
1390
|
+
// (sum unchanged) skips it: `bwd` already gets the settled complement.
|
|
1391
|
+
if (ver !== sc.stamp) {
|
|
1392
|
+
if (COUNTS)
|
|
1393
|
+
counts.step++;
|
|
1394
|
+
sc.complement = sc.step(vals, sc.complement);
|
|
1395
|
+
}
|
|
1396
|
+
if (COUNTS)
|
|
1397
|
+
counts.put++;
|
|
1398
|
+
const res = b.put(tgt, vals, sc.complement);
|
|
1399
|
+
const upd = res.updates;
|
|
1400
|
+
// Commit `bwd`'s complement directly; it must be consistent with `updates`
|
|
1401
|
+
// (no reliance on a post-write `step`). The stamp is re-set post-order (after
|
|
1402
|
+
// the sources are written and their versions bumped) so the next forward read
|
|
1403
|
+
// sees an unchanged sum and skips `step` — own-write provenance.
|
|
1404
|
+
sc.complement = res.complement;
|
|
1405
|
+
wbStateful[sTop++] = cur;
|
|
1406
|
+
out = upd;
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
if (COUNTS)
|
|
1410
|
+
counts.put++;
|
|
1411
|
+
out = b.put(tgt);
|
|
1412
|
+
}
|
|
1413
|
+
// Push non-SKIP children in REVERSE so index 0 is popped (processed) first
|
|
1414
|
+
// — depth-first, left-to-right. A short `out` skips the trailing parents.
|
|
1415
|
+
let wrote = false;
|
|
1416
|
+
const m = out.length < n ? out.length : n;
|
|
1417
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
1418
|
+
const u = out[i];
|
|
1419
|
+
if (u !== SKIP) {
|
|
1420
|
+
wrote = true;
|
|
1421
|
+
wbNode[top] = parents[i];
|
|
1422
|
+
wbTarget[top] = u;
|
|
1423
|
+
top++;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
// A stateful lens can change its VIEW through the complement alone, moving no
|
|
1427
|
+
// source (a "stash"; `!wrote` ⇒ no children pushed). The forward cone never
|
|
1428
|
+
// fires, so invalidate this node's cache and propagate to its observers here.
|
|
1429
|
+
if (!wrote && sc !== undefined) {
|
|
1430
|
+
cur.flags |= F.Dirty;
|
|
1431
|
+
const subs = cur.subs;
|
|
1432
|
+
if (subs !== undefined)
|
|
1433
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
1434
|
+
}
|
|
1435
|
+
continue;
|
|
1216
1436
|
}
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1437
|
+
// 1→1 lens (single index-0 parent-edge).
|
|
1438
|
+
if (COUNTS)
|
|
1439
|
+
counts.put++;
|
|
1440
|
+
wbNode[top] = pe.parent;
|
|
1441
|
+
wbTarget[top] = b.put(tgt);
|
|
1442
|
+
top++;
|
|
1443
|
+
}
|
|
1444
|
+
// Post-order re-stamp: now the sources are written (versions bumped), record
|
|
1445
|
+
// each on-path stateful lens's parent-version sum, so its next forward read
|
|
1446
|
+
// sees an unchanged sum and skips `step`. Integers only — no `fwd`, no commit.
|
|
1447
|
+
for (let i = 0; i < sTop; i++) {
|
|
1448
|
+
const sc = wbStateful[i]._bwd.stateful;
|
|
1449
|
+
let ver = 0;
|
|
1450
|
+
for (let e = wbStateful[i].parentEdges; e !== undefined; e = e.nextParent) {
|
|
1451
|
+
ver += e.parent.version;
|
|
1227
1452
|
}
|
|
1228
|
-
|
|
1453
|
+
sc.stamp = ver;
|
|
1454
|
+
wbStateful[i] = undefined;
|
|
1229
1455
|
}
|
|
1230
|
-
// 1→1 lens.
|
|
1231
|
-
writeBack(parent, b.put(target));
|
|
1232
1456
|
}
|
|
1233
|
-
/** Fold
|
|
1234
|
-
*
|
|
1235
|
-
* from `resolveCone` once every contributor has cascaded in — fan-in is the one
|
|
1236
|
-
* non-dual ingredient. Runs untracked (`backResolve` already cleared
|
|
1237
|
-
* `activeSub`). */
|
|
1457
|
+
/** Fold a merge's contributions once (policy; default last-writer-wins) and write
|
|
1458
|
+
* the result up to its parent. Called post-order from `resolveCone`. */
|
|
1238
1459
|
function foldMerge(parent, mn) {
|
|
1460
|
+
if (COUNTS)
|
|
1461
|
+
counts.fold++;
|
|
1239
1462
|
const vals = mn.contributions;
|
|
1240
1463
|
const fold = mn.foldFn;
|
|
1241
1464
|
let folded;
|
|
@@ -1248,25 +1471,31 @@ function foldMerge(parent, mn) {
|
|
|
1248
1471
|
vals.length = 0; // reuse the merge-owned buffer in place (fold must not retain it)
|
|
1249
1472
|
writeBack(parent, folded);
|
|
1250
1473
|
}
|
|
1474
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1475
|
+
// Public API — factories (cell / derive / lens) over the builders above.
|
|
1476
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1251
1477
|
/** Writable source; passes an existing `Writable` through (idempotent). */
|
|
1252
1478
|
export function cell(initial, opts) {
|
|
1253
1479
|
if (initial instanceof Cell)
|
|
1254
1480
|
return initial;
|
|
1255
1481
|
return new Cell(initial, opts);
|
|
1256
1482
|
}
|
|
1257
|
-
// Bare (untyped) factories
|
|
1258
|
-
// from the closures (the polymorphic-`this` statics are for typed
|
|
1259
|
-
// subclasses like `Vec.lens`).
|
|
1483
|
+
// Bare (untyped) factories: plain `Cell`, inferring `R` from the closures.
|
|
1260
1484
|
const CELL_CTOR = Cell;
|
|
1261
1485
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
1262
1486
|
export function derive(...args) {
|
|
1263
|
-
return
|
|
1487
|
+
return buildDerive(CELL_CTOR, args);
|
|
1264
1488
|
}
|
|
1265
1489
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
1266
1490
|
export function lens(...args) {
|
|
1267
|
-
return
|
|
1491
|
+
return buildLens(CELL_CTOR, args);
|
|
1268
1492
|
}
|
|
1269
|
-
//
|
|
1493
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1494
|
+
// Effects & schedulers — the Effect watcher (internal) and the public
|
|
1495
|
+
// effect / batch / network / flush surface built on it.
|
|
1496
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1497
|
+
// Effect — one watcher class for both auto-tracked effects and explicit-topology
|
|
1498
|
+
// networks: alien-signals' effect plus the `EM` mode toggles `network()` needs.
|
|
1270
1499
|
class Effect {
|
|
1271
1500
|
flags = F.Watching | F.RecursedCheck;
|
|
1272
1501
|
subs = undefined;
|
|
@@ -1275,26 +1504,31 @@ class Effect {
|
|
|
1275
1504
|
depsTail = undefined;
|
|
1276
1505
|
fn;
|
|
1277
1506
|
cleanup = undefined;
|
|
1278
|
-
|
|
1507
|
+
/** Watcher-behavior bits (`EM`); `EM.None` for a plain effect. */
|
|
1508
|
+
mode;
|
|
1509
|
+
constructor(fn, mode = EM.None) {
|
|
1279
1510
|
this.fn = fn;
|
|
1280
|
-
|
|
1281
|
-
activeSub = this;
|
|
1282
|
-
try {
|
|
1283
|
-
++runDepth;
|
|
1284
|
-
const ret = fn();
|
|
1285
|
-
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1286
|
-
}
|
|
1287
|
-
finally {
|
|
1288
|
-
--runDepth;
|
|
1289
|
-
activeSub = prev;
|
|
1290
|
-
this.flags &= ~F.RecursedCheck;
|
|
1291
|
-
}
|
|
1511
|
+
this.mode = mode;
|
|
1292
1512
|
}
|
|
1293
1513
|
_update() {
|
|
1294
1514
|
this.flags = F.Mutable;
|
|
1295
1515
|
return true;
|
|
1296
1516
|
}
|
|
1297
1517
|
_notify() {
|
|
1518
|
+
const mode = this.mode;
|
|
1519
|
+
if (mode & EM.Manual) {
|
|
1520
|
+
this.flags |= F.Watching; // re-arm but don't queue; only `flush()` advances
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (mode & EM.Sync) {
|
|
1524
|
+
// Eager watcher (network): append + force a synchronous flush.
|
|
1525
|
+
queued[queuedLength++] = this;
|
|
1526
|
+
syncFlush = true;
|
|
1527
|
+
this.flags &= ~F.Watching;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
// Plain effect: batch-insert this effect and any subscribed to it, in
|
|
1531
|
+
// dependency order (alien-signals).
|
|
1298
1532
|
let e = this;
|
|
1299
1533
|
let insertIndex = queuedLength;
|
|
1300
1534
|
const firstInsertedIndex = insertIndex;
|
|
@@ -1324,8 +1558,8 @@ class Effect {
|
|
|
1324
1558
|
this._runCleanup();
|
|
1325
1559
|
}
|
|
1326
1560
|
_run() {
|
|
1327
|
-
// Resolve back-writes this
|
|
1328
|
-
//
|
|
1561
|
+
// Resolve back-writes this node reads directly (incl. view-only stashes);
|
|
1562
|
+
// `checkDirty` resolves any back-`Pending` source reached deeper.
|
|
1329
1563
|
if (this.deps !== undefined)
|
|
1330
1564
|
resolveBackDeps(this);
|
|
1331
1565
|
const flags = this.flags;
|
|
@@ -1335,27 +1569,39 @@ class Effect {
|
|
|
1335
1569
|
if (!this.flags)
|
|
1336
1570
|
return;
|
|
1337
1571
|
}
|
|
1338
|
-
this.
|
|
1339
|
-
this.flags = F.Watching | F.RecursedCheck;
|
|
1340
|
-
const prev = activeSub;
|
|
1341
|
-
activeSub = this;
|
|
1342
|
-
try {
|
|
1343
|
-
++cycle;
|
|
1344
|
-
++runDepth;
|
|
1345
|
-
const ret = this.fn();
|
|
1346
|
-
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1347
|
-
}
|
|
1348
|
-
finally {
|
|
1349
|
-
--runDepth;
|
|
1350
|
-
activeSub = prev;
|
|
1351
|
-
this.flags &= ~F.RecursedCheck;
|
|
1352
|
-
purgeDeps(this);
|
|
1353
|
-
}
|
|
1572
|
+
this._invoke();
|
|
1354
1573
|
}
|
|
1355
1574
|
else if (this.deps !== undefined) {
|
|
1356
1575
|
this.flags = F.Watching;
|
|
1357
1576
|
}
|
|
1358
1577
|
}
|
|
1578
|
+
/** Run the body — the single path for first fire, scheduled re-run, and manual
|
|
1579
|
+
* `flush()`. Auto-tracks deps unless `NoTrack`; self-excludes writes under `Exclude`. */
|
|
1580
|
+
_invoke() {
|
|
1581
|
+
const noTrack = this.mode & EM.NoTrack;
|
|
1582
|
+
if (!noTrack)
|
|
1583
|
+
this.depsTail = undefined;
|
|
1584
|
+
this.flags = F.Watching | F.RecursedCheck;
|
|
1585
|
+
const prevSub = activeSub;
|
|
1586
|
+
const prevExc = activeExcluded;
|
|
1587
|
+
activeSub = noTrack ? undefined : this;
|
|
1588
|
+
if (this.mode & EM.Exclude)
|
|
1589
|
+
activeExcluded = this;
|
|
1590
|
+
try {
|
|
1591
|
+
++cycle;
|
|
1592
|
+
++runDepth;
|
|
1593
|
+
const ret = this.fn();
|
|
1594
|
+
this.cleanup = typeof ret === "function" ? ret : undefined;
|
|
1595
|
+
}
|
|
1596
|
+
finally {
|
|
1597
|
+
--runDepth;
|
|
1598
|
+
activeSub = prevSub;
|
|
1599
|
+
activeExcluded = prevExc;
|
|
1600
|
+
this.flags &= ~F.RecursedCheck;
|
|
1601
|
+
if (!noTrack)
|
|
1602
|
+
purgeDeps(this);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1359
1605
|
_runCleanup() {
|
|
1360
1606
|
const c = this.cleanup;
|
|
1361
1607
|
this.cleanup = undefined;
|
|
@@ -1371,28 +1617,45 @@ class Effect {
|
|
|
1371
1617
|
}
|
|
1372
1618
|
export function effect(fn) {
|
|
1373
1619
|
const e = new Effect(fn);
|
|
1620
|
+
e._invoke();
|
|
1374
1621
|
return () => e._unwatched();
|
|
1375
1622
|
}
|
|
1376
|
-
/** Run effects woken by a write. Backward work is pulled lazily per read
|
|
1377
|
-
*
|
|
1378
|
-
* so flush owns NO backward bookkeeping — just the effect queue. */
|
|
1623
|
+
/** Run effects woken by a write. Backward work is pulled lazily per read, so
|
|
1624
|
+
* flush owns no backward bookkeeping — just the effect queue. */
|
|
1379
1625
|
function flush() {
|
|
1380
1626
|
if (flushing)
|
|
1381
1627
|
return;
|
|
1382
1628
|
flushing = true;
|
|
1629
|
+
// Error locality: one effect throwing must not strand its siblings. Drain the
|
|
1630
|
+
// whole queue, catching each body; surface the first error after the queue is
|
|
1631
|
+
// empty (later errors are dropped — the engine stays consistent, the user still
|
|
1632
|
+
// sees a failure). A throwing body isn't re-queued (its `F.Watching` is already
|
|
1633
|
+
// cleared); it re-arms on the next wake.
|
|
1634
|
+
let err;
|
|
1635
|
+
let threw = false;
|
|
1383
1636
|
try {
|
|
1384
1637
|
while (notifyIndex < queuedLength) {
|
|
1385
1638
|
const e = queued[notifyIndex];
|
|
1386
1639
|
queued[notifyIndex++] = undefined;
|
|
1387
|
-
|
|
1640
|
+
try {
|
|
1641
|
+
e._run();
|
|
1642
|
+
}
|
|
1643
|
+
catch (ex) {
|
|
1644
|
+
if (!threw) {
|
|
1645
|
+
err = ex;
|
|
1646
|
+
threw = true;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1388
1649
|
}
|
|
1389
1650
|
}
|
|
1390
1651
|
finally {
|
|
1391
1652
|
notifyIndex = 0;
|
|
1392
1653
|
queuedLength = 0;
|
|
1393
|
-
|
|
1654
|
+
syncFlush = false;
|
|
1394
1655
|
flushing = false;
|
|
1395
1656
|
}
|
|
1657
|
+
if (threw)
|
|
1658
|
+
throw err;
|
|
1396
1659
|
}
|
|
1397
1660
|
/** Queue an effect flush for the end of the current microtask turn (idempotent).
|
|
1398
1661
|
* A write wakes effects asynchronously; many writes in one turn coalesce. */
|
|
@@ -1406,26 +1669,24 @@ function schedule() {
|
|
|
1406
1669
|
});
|
|
1407
1670
|
}
|
|
1408
1671
|
/** Resolve the queue after a write: no-op inside a `batch`/flush (the barrier
|
|
1409
|
-
* owns flushing), else synchronously if a
|
|
1410
|
-
* deferred to the microtask (coalesced effects). */
|
|
1672
|
+
* owns flushing), else synchronously if a `Sync` watcher is waiting (eager
|
|
1673
|
+
* solve) or deferred to the microtask (coalesced effects). */
|
|
1411
1674
|
function autoFlush() {
|
|
1412
1675
|
if (batchDepth !== 0 || flushing)
|
|
1413
1676
|
return;
|
|
1414
|
-
if (
|
|
1677
|
+
if (syncFlush)
|
|
1415
1678
|
flush();
|
|
1416
1679
|
else
|
|
1417
1680
|
schedule();
|
|
1418
1681
|
}
|
|
1419
|
-
/** Run all pending effects
|
|
1420
|
-
*
|
|
1421
|
-
* to the microtask queue. Reads never need it — they pull current values. */
|
|
1682
|
+
/** Run all pending effects now, synchronously — the escape hatch for code that
|
|
1683
|
+
* must observe effect side-effects before yielding. Reads never need it. */
|
|
1422
1684
|
export function settle() {
|
|
1423
1685
|
flush();
|
|
1424
1686
|
}
|
|
1425
|
-
/** Group writes and flush effects
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
* woken effects before the call returns (and don't want a `settle()`). */
|
|
1687
|
+
/** Group writes and flush effects synchronously at the end of `fn`. Effects
|
|
1688
|
+
* coalesce on the microtask turn anyway; reach for `batch` only to run the woken
|
|
1689
|
+
* effects before the call returns. */
|
|
1429
1690
|
export function batch(fn) {
|
|
1430
1691
|
++batchDepth;
|
|
1431
1692
|
try {
|
|
@@ -1446,193 +1707,114 @@ export function untracked(fn) {
|
|
|
1446
1707
|
activeSub = prev;
|
|
1447
1708
|
}
|
|
1448
1709
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
disposed = false;
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
this.body = body;
|
|
1466
|
-
this.manual = manual;
|
|
1467
|
-
}
|
|
1468
|
-
/** Two-phase init so the body sees its own handle on the first fire. */
|
|
1469
|
-
_initWithHandle(handle, initialDeps) {
|
|
1470
|
-
this._handle = handle;
|
|
1471
|
-
this._linkBatch(initialDeps);
|
|
1472
|
-
this._runBody(EMPTY_DIRTY);
|
|
1473
|
-
}
|
|
1474
|
-
_update() {
|
|
1475
|
-
this.flags = F.Mutable;
|
|
1476
|
-
return true;
|
|
1477
|
-
}
|
|
1478
|
-
_notify() {
|
|
1479
|
-
if (this.manual) {
|
|
1480
|
-
this.pending = true;
|
|
1481
|
-
this.flags |= F.Watching;
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
queued[queuedLength++] = this;
|
|
1485
|
-
networkQueued = true; // eager: a queued network forces a synchronous flush
|
|
1486
|
-
this.flags &= ~F.Watching;
|
|
1487
|
-
}
|
|
1488
|
-
_unwatched() {
|
|
1489
|
-
this.disposed = true;
|
|
1490
|
-
this.flags = F.None;
|
|
1491
|
-
disposeAllDepsInReverse(this);
|
|
1492
|
-
const sub = this.subs;
|
|
1493
|
-
if (sub !== undefined)
|
|
1494
|
-
unlink(sub);
|
|
1495
|
-
this.lastValues.clear();
|
|
1496
|
-
}
|
|
1497
|
-
_run() {
|
|
1498
|
-
if (this.disposed)
|
|
1499
|
-
return;
|
|
1500
|
-
if (this.deps !== undefined)
|
|
1501
|
-
resolveBackDeps(this);
|
|
1502
|
-
const flags = this.flags;
|
|
1503
|
-
if (flags & F.Dirty || (flags & F.Pending && checkDirty(this.deps, this))) {
|
|
1504
|
-
this._runBody(this._computeDirty());
|
|
1505
|
-
}
|
|
1506
|
-
else if (this.deps !== undefined) {
|
|
1507
|
-
this.flags = F.Watching;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
_computeDirty() {
|
|
1710
|
+
/** Build a reactive sub-DAG. The body fires when any subscribed dep changes
|
|
1711
|
+
* (`dirty` = the changed subset), self-excludes its own writes, and (auto mode)
|
|
1712
|
+
* resolves synchronously. `manual: true` defers firing so only `flush()` advances;
|
|
1713
|
+
* `flush()` from inside the body throws. Network-specific state (last-values,
|
|
1714
|
+
* handle) lives in this closure, so the shared `Effect` carries none of it. */
|
|
1715
|
+
export function network(
|
|
1716
|
+
// biome-ignore lint/suspicious/noExplicitAny: deps come in many flavours
|
|
1717
|
+
deps, body, opts) {
|
|
1718
|
+
const lastValues = new Map();
|
|
1719
|
+
const depsSet = new Set();
|
|
1720
|
+
let ownCycle = 0;
|
|
1721
|
+
let disposed = false;
|
|
1722
|
+
// Forward-declared so the closures below can reach the node; assigned before
|
|
1723
|
+
// any runs (the first `_invoke` happens after construction).
|
|
1724
|
+
let node;
|
|
1725
|
+
const computeDirty = () => {
|
|
1511
1726
|
let dirty;
|
|
1512
|
-
for (const [
|
|
1513
|
-
if (
|
|
1514
|
-
|
|
1515
|
-
dirty = new Set();
|
|
1516
|
-
dirty.add(cell);
|
|
1517
|
-
}
|
|
1727
|
+
for (const [c, last] of lastValues) {
|
|
1728
|
+
if (c.peek() !== last)
|
|
1729
|
+
(dirty ??= new Set()).add(c);
|
|
1518
1730
|
}
|
|
1519
1731
|
return dirty ?? EMPTY_DIRTY;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
}
|
|
1533
|
-
finally {
|
|
1534
|
-
if (!--batchDepth)
|
|
1535
|
-
flush();
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
finally {
|
|
1539
|
-
--runDepth;
|
|
1540
|
-
activeNetwork = prevNetwork;
|
|
1541
|
-
this.flags &= ~F.RecursedCheck;
|
|
1542
|
-
this.lastValues.clear();
|
|
1543
|
-
let l = this.deps;
|
|
1544
|
-
while (l !== undefined) {
|
|
1545
|
-
const cell = l.dep;
|
|
1546
|
-
this.lastValues.set(cell, cell.peek());
|
|
1547
|
-
l = l.nextDep;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
this.pending = false;
|
|
1551
|
-
}
|
|
1552
|
-
flush() {
|
|
1553
|
-
if (this.disposed)
|
|
1554
|
-
return;
|
|
1555
|
-
if (this.flags & F.RecursedCheck) {
|
|
1556
|
-
throw new Error("network: flush() called from inside body — would recurse infinitely. " +
|
|
1557
|
-
"Return from the body and let the next dep change drive the next fire.");
|
|
1732
|
+
};
|
|
1733
|
+
const linkDeps = (cells) => {
|
|
1734
|
+
let tail = node.deps;
|
|
1735
|
+
if (tail !== undefined)
|
|
1736
|
+
while (tail.nextDep !== undefined)
|
|
1737
|
+
tail = tail.nextDep;
|
|
1738
|
+
node.depsTail = tail;
|
|
1739
|
+
for (const s of cells) {
|
|
1740
|
+
if (depsSet.has(s))
|
|
1741
|
+
continue;
|
|
1742
|
+
depsSet.add(s);
|
|
1743
|
+
link(s, node, ++ownCycle);
|
|
1558
1744
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
subscribe(cells) {
|
|
1562
|
-
if (this.disposed)
|
|
1563
|
-
return;
|
|
1564
|
-
this._linkBatch(cells);
|
|
1565
|
-
}
|
|
1566
|
-
unsubscribe(cells) {
|
|
1567
|
-
if (this.disposed)
|
|
1568
|
-
return;
|
|
1569
|
-
const set = this._depsSet;
|
|
1745
|
+
};
|
|
1746
|
+
const unlinkDeps = (cells) => {
|
|
1570
1747
|
for (const s of cells) {
|
|
1571
|
-
if (!
|
|
1748
|
+
if (!depsSet.has(s))
|
|
1572
1749
|
continue;
|
|
1573
|
-
|
|
1574
|
-
let l =
|
|
1575
|
-
while (l !== undefined) {
|
|
1750
|
+
depsSet.delete(s);
|
|
1751
|
+
for (let l = node.deps; l !== undefined; l = l.nextDep) {
|
|
1576
1752
|
if (l.dep === s) {
|
|
1577
|
-
unlink(l,
|
|
1753
|
+
unlink(l, node);
|
|
1578
1754
|
break;
|
|
1579
1755
|
}
|
|
1580
|
-
l = l.nextDep;
|
|
1581
1756
|
}
|
|
1582
1757
|
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1758
|
+
};
|
|
1759
|
+
const handle = {
|
|
1760
|
+
dispose: () => {
|
|
1761
|
+
if (disposed)
|
|
1762
|
+
return;
|
|
1763
|
+
disposed = true;
|
|
1764
|
+
node._unwatched();
|
|
1765
|
+
lastValues.clear();
|
|
1766
|
+
},
|
|
1767
|
+
flush: () => {
|
|
1768
|
+
if (disposed)
|
|
1769
|
+
return;
|
|
1770
|
+
// RecursedCheck doubles as the "body running" guard.
|
|
1771
|
+
if (node.flags & F.RecursedCheck) {
|
|
1772
|
+
throw new Error("network: flush() called from inside body — would recurse infinitely.");
|
|
1773
|
+
}
|
|
1774
|
+
batch(() => node._invoke());
|
|
1775
|
+
},
|
|
1776
|
+
subscribe: (...cells) => {
|
|
1777
|
+
if (!disposed)
|
|
1778
|
+
linkDeps(cells);
|
|
1779
|
+
},
|
|
1780
|
+
unsubscribe: (...cells) => {
|
|
1781
|
+
if (!disposed)
|
|
1782
|
+
unlinkDeps(cells);
|
|
1783
|
+
},
|
|
1784
|
+
};
|
|
1785
|
+
// The Effect body: hand the changed subset to the user body, then re-snapshot
|
|
1786
|
+
// the deps for the next fire.
|
|
1787
|
+
const run = () => {
|
|
1788
|
+
const dirty = computeDirty();
|
|
1789
|
+
try {
|
|
1790
|
+
body(dirty, handle);
|
|
1590
1791
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1792
|
+
finally {
|
|
1793
|
+
lastValues.clear();
|
|
1794
|
+
for (let l = node.deps; l !== undefined; l = l.nextDep) {
|
|
1795
|
+
const c = l.dep;
|
|
1796
|
+
lastValues.set(c, c.peek());
|
|
1797
|
+
}
|
|
1597
1798
|
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
/** Build a reactive sub-DAG node with explicit topology. The body fires
|
|
1601
|
-
* when any subscribed dep changes (`dirty` = the changed subset), runs
|
|
1602
|
-
* inside `batch()`, and self-excludes its own writes. Topology is the
|
|
1603
|
-
* deps array + later subscribe/unsubscribe (body reads add no deps).
|
|
1604
|
-
* `flush()` from inside the body throws; `manual: true` defers
|
|
1605
|
-
* auto-firing so only `flush()` advances. */
|
|
1606
|
-
export function network(
|
|
1607
|
-
// biome-ignore lint/suspicious/noExplicitAny: deps come in many flavours
|
|
1608
|
-
deps, body, opts) {
|
|
1609
|
-
const node = new _NetworkNode(body, opts?.manual ?? false);
|
|
1610
|
-
const handle = {
|
|
1611
|
-
dispose: () => node._unwatched(),
|
|
1612
|
-
flush: () => node.flush(),
|
|
1613
|
-
subscribe: (...cells) => node.subscribe(cells),
|
|
1614
|
-
unsubscribe: (...cells) => node.unsubscribe(cells),
|
|
1615
1799
|
};
|
|
1616
|
-
node
|
|
1800
|
+
node = new Effect(run, EM.NoTrack | EM.Exclude | (opts?.manual ? EM.Manual : EM.Sync));
|
|
1801
|
+
linkDeps(deps);
|
|
1802
|
+
batch(() => node._invoke()); // first fire (lastValues empty ⇒ EMPTY_DIRTY)
|
|
1617
1803
|
return handle;
|
|
1618
1804
|
}
|
|
1619
1805
|
// ── value-class authoring helpers ──────────────────────────────────
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
1622
|
-
// The choice between them IS the local declaration of writability at each
|
|
1623
|
-
// getter (mirroring `: this` invertible method returns). For arbitrary
|
|
1806
|
+
// `fieldLens`/`cachedDerive` are the two getter forms a value class declares;
|
|
1807
|
+
// the choice between them is the local declaration of writability. For arbitrary
|
|
1624
1808
|
// cached views, use `lazy()` directly.
|
|
1625
|
-
/** Bidirectional field lens onto `parent.value[key]
|
|
1626
|
-
*
|
|
1627
|
-
* conditional: `Writable<Cls>` on a writable parent, bare `Cls` on RO.
|
|
1809
|
+
/** Bidirectional field lens onto `parent.value[key]` (write spread-replaces),
|
|
1810
|
+
* cached per (instance, key). `Writable<Cls>` on a writable parent, bare `Cls` on RO.
|
|
1628
1811
|
*
|
|
1629
1812
|
* get x() { return fieldLens(this, "x", Num); } */
|
|
1630
1813
|
export function fieldLens(parent, key, Cls) {
|
|
1631
1814
|
return lazy(parent, key, () => fieldOf(parent, key, Cls));
|
|
1632
1815
|
}
|
|
1633
1816
|
/** Read-only derived view via `Cls.derive(parent, fn)`, memoized per
|
|
1634
|
-
* (instance, key)
|
|
1635
|
-
* getter form, not a new kind of cell.
|
|
1817
|
+
* (instance, key).
|
|
1636
1818
|
*
|
|
1637
1819
|
* get magnitude() {
|
|
1638
1820
|
* return cachedDerive(this, "magnitude", Num, v => Math.hypot(v.x, v.y));
|
|
@@ -1642,12 +1824,8 @@ export function cachedDerive(parent, key, Cls, fn) {
|
|
|
1642
1824
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.derive
|
|
1643
1825
|
return lazy(parent, key, () => Cls.derive(parent, fn));
|
|
1644
1826
|
}
|
|
1645
|
-
/** Every cell `s` transitively depends on, including itself
|
|
1646
|
-
*
|
|
1647
|
-
* peeking each Computed to populate deps; the `seen` set breaks cycles.
|
|
1648
|
-
* Used by `Propagators` to expand declared reads into their transitive
|
|
1649
|
-
* parent set. Inspection is safe: it only reads engine state and peeks
|
|
1650
|
-
* `.value` (idempotent for lazy Computeds). */
|
|
1827
|
+
/** Every cell `s` transitively depends on, including itself (BFS, peeking each
|
|
1828
|
+
* computed to populate deps; `seen` breaks cycles). */
|
|
1651
1829
|
export function transitiveDeps(s) {
|
|
1652
1830
|
const seen = new Set();
|
|
1653
1831
|
const queue = [s];
|
|
@@ -1656,7 +1834,6 @@ export function transitiveDeps(s) {
|
|
|
1656
1834
|
if (seen.has(cur))
|
|
1657
1835
|
continue;
|
|
1658
1836
|
seen.add(cur);
|
|
1659
|
-
// Cast to reach engine fields the typed Cell<T> shape doesn't surface.
|
|
1660
1837
|
const c = cur;
|
|
1661
1838
|
if (c.getter !== undefined) {
|
|
1662
1839
|
void cur.value;
|