bireactive 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation/anim.js +4 -0
- package/dist/coll.d.ts +7 -7
- package/dist/coll.js +3 -1
- package/dist/core/cell.d.ts +89 -66
- package/dist/core/cell.js +642 -401
- package/dist/core/index.d.ts +4 -14
- package/dist/core/index.js +4 -14
- package/dist/core/lenses/aggregates.d.ts +1 -1
- package/dist/core/lenses/aggregates.js +4 -3
- package/dist/core/lenses/closed-form-policies.js +6 -6
- package/dist/core/lenses/decompositions.js +3 -3
- package/dist/core/lenses/domain-aggregates.js +5 -5
- package/dist/core/lenses/geometry.d.ts +1 -1
- package/dist/core/lenses/geometry.js +6 -7
- package/dist/core/lenses/memory.d.ts +2 -2
- package/dist/core/lenses/memory.js +3 -3
- package/dist/core/lenses/typed-factor.js +4 -3
- package/dist/core/traits.d.ts +1 -0
- package/dist/core/values/box.js +7 -7
- package/dist/core/values/color.js +5 -5
- package/dist/core/values/field.d.ts +70 -0
- package/dist/core/values/field.js +230 -0
- package/dist/core/values/gpu.d.ts +4 -2
- package/dist/core/values/gpu.js +11 -4
- package/dist/core/values/matrix.js +7 -7
- package/dist/core/values/num.d.ts +1 -1
- package/dist/core/values/num.js +1 -1
- package/dist/core/values/pose.js +4 -4
- package/dist/core/values/range.js +6 -6
- package/dist/core/values/template.d.ts +1 -1
- package/dist/core/values/template.js +2 -1
- package/dist/core/values/transform.js +7 -7
- package/dist/core/values/tri.js +3 -3
- package/dist/core/values/vec.js +8 -12
- package/dist/ext/timeline.js +2 -2
- package/dist/formats/cst.d.ts +127 -0
- package/dist/formats/cst.js +280 -0
- package/dist/formats/edn.d.ts +2 -0
- package/dist/formats/edn.js +301 -0
- package/dist/formats/index.d.ts +6 -0
- package/dist/formats/index.js +8 -0
- package/dist/formats/json.d.ts +2 -0
- package/dist/formats/json.js +332 -0
- package/dist/formats/lens.d.ts +8 -0
- package/dist/formats/lens.js +54 -0
- package/dist/formats/toml.d.ts +2 -0
- package/dist/formats/toml.js +526 -0
- package/dist/formats/yaml.d.ts +2 -0
- package/dist/formats/yaml.js +661 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/learn/data.d.ts +49 -0
- package/dist/learn/data.js +181 -0
- package/dist/learn/index.d.ts +3 -0
- package/dist/learn/index.js +6 -0
- package/dist/learn/lens-net.d.ts +63 -0
- package/dist/learn/lens-net.js +219 -0
- package/dist/learn/mlp.d.ts +77 -0
- package/dist/learn/mlp.js +292 -0
- package/dist/propagators/csp.d.ts +13 -0
- package/dist/propagators/csp.js +52 -0
- package/dist/propagators/flex.d.ts +31 -0
- package/dist/propagators/flex.js +189 -0
- package/dist/propagators/graph.d.ts +73 -0
- package/dist/propagators/graph.js +543 -0
- package/dist/propagators/index.d.ts +8 -6
- package/dist/propagators/index.js +15 -6
- package/dist/propagators/lattice.d.ts +45 -0
- package/dist/propagators/lattice.js +113 -0
- package/dist/propagators/layout.d.ts +1 -27
- package/dist/propagators/layout.js +6 -175
- package/dist/propagators/numeric.d.ts +17 -0
- package/dist/propagators/numeric.js +93 -0
- package/dist/propagators/solver.d.ts +51 -0
- package/dist/propagators/solver.js +175 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/lens.d.ts +121 -0
- package/dist/schema/lens.js +429 -0
- package/dist/shapes/annular-sector.js +4 -4
- package/dist/shapes/button.js +1 -1
- package/dist/shapes/circle.js +1 -1
- package/dist/shapes/handle.js +2 -2
- package/dist/shapes/label.js +1 -1
- package/dist/shapes/layout.js +2 -2
- package/dist/shapes/rect.js +7 -7
- package/dist/shapes/shape.js +8 -8
- package/dist/tex/tex.js +9 -2
- package/dist/web/diagram.js +2 -2
- package/package.json +9 -19
- package/dist/propagators/network.d.ts +0 -52
- package/dist/propagators/network.js +0 -185
- package/dist/propagators/propagator.d.ts +0 -12
- package/dist/propagators/propagator.js +0 -16
- package/dist/propagators/range.d.ts +0 -45
- package/dist/propagators/range.js +0 -147
- package/dist/propagators/relations.d.ts +0 -60
- package/dist/propagators/relations.js +0 -343
package/dist/core/cell.js
CHANGED
|
@@ -2,26 +2,62 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Forward propagation is alien-signals verbatim (link/propagate/
|
|
4
4
|
// checkDirty/shallowPropagate, Dirty/Pending/Recursed flags, lazy pull).
|
|
5
|
-
// Backward is not a second engine: a write "compiles" a view-edit into
|
|
6
|
-
// source-edits by walking up `_bwdParent`, applying each lens's `put` to
|
|
7
|
-
// compute what the source(s) must become, committing via the SAME
|
|
8
|
-
// forward write path. So views are never sticky (a view is always
|
|
9
|
-
// `get(source)`; lossy lenses snap), no-op deltas short-circuit for free
|
|
10
|
-
// via equality, and backward cost ≤ forward cost.
|
|
11
5
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// (N→M, e.g. mean/diff). Info the source can't hold lives in a
|
|
20
|
-
// stateful-lens complement, not a bespoke engine kind.
|
|
6
|
+
// Backward is the SAME shape as forward — a lazy push-pull — not a second
|
|
7
|
+
// engine, and it carries NO dynamic side tables: state lives on flags plus two
|
|
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:
|
|
21
13
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
14
|
+
// role forward (source → view) backward (view → source)
|
|
15
|
+
// ---- ------------------------ ------------------------
|
|
16
|
+
// down edge subs (who reads me) _bwd.parent (my parents)
|
|
17
|
+
// up edge deps (my deps) _lensSubs (my lens-children)
|
|
18
|
+
// push (mark) propagate (down `subs`) markDown (down `_bwd.parent`)
|
|
19
|
+
// pull (resolve) checkDirty (up `deps`) resolveCone (up `_lensSubs`)
|
|
20
|
+
// commit/compute _update / getter writeBack
|
|
21
|
+
// "dirty" flag F.Dirty (source staged) BF.Dirty (view holds target)
|
|
22
|
+
// "pending" flag F.Pending (on the cone) BF.Pending (on the back-path)
|
|
23
|
+
//
|
|
24
|
+
// Forward flags live on `flags`, backward on a SEPARATE `bflags` word — the two
|
|
25
|
+
// engines never share a bit, so a forward reset can't disturb a back-write.
|
|
26
|
+
//
|
|
27
|
+
// Forward: a source write PUSHES dirtiness down the cone (`Pending`, cheap
|
|
28
|
+
// flagging) and the value is PULLED up on read (`checkDirty` ascends `deps` to
|
|
29
|
+
// the `Dirty` source). Backward is the exact dual. A write to a view PUSHES
|
|
30
|
+
// (`markDown`): stash the `target` on the view itself (`pendingValue` +
|
|
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.
|
|
25
61
|
//
|
|
26
62
|
// Mode table — a cell's role is fully determined by which fields are set:
|
|
27
63
|
// source getter undefined (truth in currentValue)
|
|
@@ -31,15 +67,12 @@
|
|
|
31
67
|
// merge getter + _bwd{ merge } (N→1 backward fold)
|
|
32
68
|
// stateful getter + _bwd{ put, parent, stateful } (complement-carrying)
|
|
33
69
|
// A cell is writable iff `_bwd !== undefined` (the backward sidecar; see
|
|
34
|
-
// `BwdSpec`). `pendingValue` is
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// `_queueIdx`) and merge folds defer until all contributors land; the
|
|
41
|
-
// flush loop alternates bwd-drain / effect-drain to a fixpoint.
|
|
42
|
-
// Flag bits (alien-signals v2).
|
|
70
|
+
// `BwdSpec`). `pendingValue` is a source's staged forward write only; a getter
|
|
71
|
+
// cell never uses it.
|
|
72
|
+
// Forward flag bits (alien-signals v2), on `flags`. Backward state lives in a
|
|
73
|
+
// SEPARATE word (`bflags`, `BF` below) so the two engines never share a bit: a
|
|
74
|
+
// forward recompute that resets `flags` can't disturb a pending back-write, and
|
|
75
|
+
// vice versa — the duals are fully decoupled (no preservation hacks).
|
|
43
76
|
const F = {
|
|
44
77
|
None: 0,
|
|
45
78
|
Mutable: 1,
|
|
@@ -48,9 +81,74 @@ const F = {
|
|
|
48
81
|
Recursed: 8,
|
|
49
82
|
Dirty: 16,
|
|
50
83
|
Pending: 32,
|
|
51
|
-
/** Backward-only: cell has a pending backward contribution queued. */
|
|
52
|
-
BwdQueued: 64,
|
|
53
84
|
};
|
|
85
|
+
// Backward flag bits, on a Cell's own `bflags` word (the dual of `flags`).
|
|
86
|
+
const BF = {
|
|
87
|
+
None: 0,
|
|
88
|
+
/** Backward dual of `F.Dirty`: this VIEW holds an unresolved back-write target
|
|
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. */
|
|
91
|
+
Dirty: 1,
|
|
92
|
+
/** Backward dual of `F.Pending`: this node lies on the back-path from a
|
|
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. */
|
|
97
|
+
Pending: 2,
|
|
98
|
+
};
|
|
99
|
+
// Named mask (legibility): a cell's whole backward state in one test.
|
|
100
|
+
/** Armed root OR on a back-path — i.e. a read must `backResolve` first. */
|
|
101
|
+
const BACK_MARKED = BF.Dirty | BF.Pending;
|
|
102
|
+
/** Multi-out / stateful back-write sentinel: "leave this parent untouched."
|
|
103
|
+
* A `bwd` returning per-parent updates yields `SKIP` for a parent it declines
|
|
104
|
+
* to write; every other slot value is written verbatim — INCLUDING `undefined`,
|
|
105
|
+
* which is a first-class cell value, not a hole. A SHORT array skips the trailing
|
|
106
|
+
* parents (so writing only the leading few needs no `SKIP` padding); `[]` skips
|
|
107
|
+
* all. (Single-out 1→1 `put` has no skip notion: it always writes its one parent,
|
|
108
|
+
* so it stays `undefined`-safe by construction.) */
|
|
109
|
+
export const SKIP = Symbol("bireactive.SKIP");
|
|
110
|
+
// Mode predicates — the single place a cell's role is read off its fields (see
|
|
111
|
+
// the mode table by `BwdSpec`). V8 inlines these; the backward walk reads them
|
|
112
|
+
// instead of duplicating `getter`/`_bwd` field probes.
|
|
113
|
+
/** Source (truth leaf): no forward derivation. */
|
|
114
|
+
function isSource(c) {
|
|
115
|
+
return c.getter === undefined;
|
|
116
|
+
}
|
|
117
|
+
/** Writable: carries a backward sidecar (lens / multi-out / merge / stateful / pin). */
|
|
118
|
+
function isWritable(c) {
|
|
119
|
+
return c._bwd !== undefined;
|
|
120
|
+
}
|
|
121
|
+
/** Read-only derived: a `derive` with no backward path. A split routes around
|
|
122
|
+
* it; a sole parent has nowhere to land (the back-walk throws). */
|
|
123
|
+
function isReadOnlyDerived(c) {
|
|
124
|
+
return !isSource(c) && !isWritable(c);
|
|
125
|
+
}
|
|
126
|
+
/** Forward primal a source-reading `bwd` linearizes at, WITHOUT a cascading
|
|
127
|
+
* recompute (the lazy dual of reverse-mode AD reusing a stored linearization
|
|
128
|
+
* point). Source or realized derived → its live/last-settled value (PutGet
|
|
129
|
+
* holds for any source state, so a stale primal still round-trips); unrealized
|
|
130
|
+
* derived (`Dirty`) → realize once via `.value`, seeding `currentValue`. */
|
|
131
|
+
function backPrimal(c) {
|
|
132
|
+
if (c.getter === undefined || c.flags & F.Dirty)
|
|
133
|
+
return c.value;
|
|
134
|
+
return c.currentValue;
|
|
135
|
+
}
|
|
136
|
+
/** Register `node` on each backward parent's `_lensSubs` (the edge `resolveCone`
|
|
137
|
+
* ascends), once, lazily on the first back-write — so a lens only ever read
|
|
138
|
+
* forward never allocates it. Idempotent via `_linkedBack`; persists for life. */
|
|
139
|
+
function linkBack(node) {
|
|
140
|
+
if (node._linkedBack)
|
|
141
|
+
return;
|
|
142
|
+
node._linkedBack = true;
|
|
143
|
+
const parent = node._bwd.parent; // set for every mode except `pin` (parentless)
|
|
144
|
+
if (parent === undefined)
|
|
145
|
+
return;
|
|
146
|
+
if (Array.isArray(parent))
|
|
147
|
+
for (let i = 0; i < parent.length; i++)
|
|
148
|
+
(parent[i]._lensSubs ??= []).push(node);
|
|
149
|
+
else
|
|
150
|
+
(parent._lensSubs ??= []).push(node);
|
|
151
|
+
}
|
|
54
152
|
let cycle = 0;
|
|
55
153
|
let runDepth = 0;
|
|
56
154
|
let batchDepth = 0;
|
|
@@ -58,14 +156,26 @@ let notifyIndex = 0;
|
|
|
58
156
|
let queuedLength = 0;
|
|
59
157
|
let activeSub;
|
|
60
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;
|
|
61
171
|
/** Network running its body, if any. Source writes self-exclude it so a
|
|
62
172
|
* network reading+writing a cell doesn't re-trigger itself. */
|
|
63
173
|
let activeNetwork;
|
|
64
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;
|
|
65
178
|
const EMPTY_DIRTY = new Set();
|
|
66
|
-
/** Backward worklist: lens cells with deferred writes, merge cells
|
|
67
|
-
* awaiting fold. Drained to a fixpoint with effects by flush. */
|
|
68
|
-
const bwdQueue = [];
|
|
69
179
|
// Fires on every SOURCE value-change (the one place truth mutates).
|
|
70
180
|
// Backward writes reach it via `_writeSource`, attributing lens edits to
|
|
71
181
|
// the source they resolve to.
|
|
@@ -207,7 +317,15 @@ function checkDirty(startLink, startSub) {
|
|
|
207
317
|
const flags = dep.flags;
|
|
208
318
|
if (sub.flags & F.Dirty)
|
|
209
319
|
dirty = true;
|
|
210
|
-
else if ((flags & (F.Mutable | F.Dirty)) === (F.Mutable | F.Dirty)
|
|
320
|
+
else if ((flags & (F.Mutable | F.Dirty)) === (F.Mutable | F.Dirty) ||
|
|
321
|
+
// A back-`Pending` SOURCE (leaf: no getter) looks unchanged until its
|
|
322
|
+
// back-write resolves; `_update` resolves it (pulls its registered views,
|
|
323
|
+
// runs the `put`s), then reports whether it moved — like a forward-`Dirty`
|
|
324
|
+
// source. Intermediate views are also back-`Pending`, but they have a
|
|
325
|
+
// getter and fall through to the `Pending` recurse below.
|
|
326
|
+
(flags & F.Mutable &&
|
|
327
|
+
dep.bflags & BF.Pending &&
|
|
328
|
+
isSource(dep))) {
|
|
211
329
|
const subs = dep.subs;
|
|
212
330
|
if (dep._update()) {
|
|
213
331
|
if (subs.nextSub !== undefined)
|
|
@@ -232,11 +350,18 @@ function checkDirty(startLink, startSub) {
|
|
|
232
350
|
while (checkDepth--) {
|
|
233
351
|
l = stack.value;
|
|
234
352
|
stack = stack.prev;
|
|
235
|
-
|
|
353
|
+
// `dirty` tracks change down THIS branch, but a node may have been marked
|
|
354
|
+
// `F.Dirty` independently — `shallowPropagate` fires when a lazy back-write
|
|
355
|
+
// commits its source mid-`checkDirty` (a hazard a forward-only engine never
|
|
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.
|
|
359
|
+
if (dirty || sub.flags & F.Dirty) {
|
|
236
360
|
const subs = sub.subs;
|
|
237
361
|
if (sub._update()) {
|
|
238
362
|
if (subs.nextSub !== undefined)
|
|
239
363
|
shallowPropagate(subs);
|
|
364
|
+
dirty = true;
|
|
240
365
|
sub = l.sub;
|
|
241
366
|
continue;
|
|
242
367
|
}
|
|
@@ -289,41 +414,15 @@ function disposeAllDepsInReverse(sub) {
|
|
|
289
414
|
l = prev;
|
|
290
415
|
}
|
|
291
416
|
}
|
|
292
|
-
export const DIRECT_SLOT = Symbol("merge:direct-slot");
|
|
293
417
|
class MergeNode {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
this.
|
|
302
|
-
this.hasIncrementalAcc = policy.remove !== undefined;
|
|
303
|
-
this.acc = policy.identity;
|
|
304
|
-
}
|
|
305
|
-
receive(slot, next) {
|
|
306
|
-
if (this.hasIncrementalAcc) {
|
|
307
|
-
const remove = this.policy.remove;
|
|
308
|
-
const prior = this.slots.get(slot);
|
|
309
|
-
if (prior === undefined)
|
|
310
|
-
this.acc = this.policy.combine(this.acc, next);
|
|
311
|
-
else
|
|
312
|
-
this.acc = this.policy.combine(remove(this.acc, prior), next);
|
|
313
|
-
}
|
|
314
|
-
this.slots.set(slot, next);
|
|
315
|
-
}
|
|
316
|
-
fold() {
|
|
317
|
-
if (this.hasIncrementalAcc)
|
|
318
|
-
return this.acc;
|
|
319
|
-
let acc = this.policy.identity;
|
|
320
|
-
for (const v of this.slots.values())
|
|
321
|
-
acc = this.policy.combine(acc, v);
|
|
322
|
-
return acc;
|
|
323
|
-
}
|
|
324
|
-
reset() {
|
|
325
|
-
this.slots.clear();
|
|
326
|
-
this.acc = this.policy.identity;
|
|
418
|
+
foldFn;
|
|
419
|
+
/** Contributions gathered as this merge's cone resolves; folded and cleared
|
|
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. */
|
|
423
|
+
contributions = [];
|
|
424
|
+
constructor(fold) {
|
|
425
|
+
this.foldFn = fold;
|
|
327
426
|
}
|
|
328
427
|
}
|
|
329
428
|
// BwdSpec — the backward sidecar.
|
|
@@ -340,23 +439,20 @@ class MergeNode {
|
|
|
340
439
|
// stays distinctly typed: `merge` (the N→1 fold node) and `stateful` (the
|
|
341
440
|
// complement machinery of a complement-carrying lens). Both are rare, so
|
|
342
441
|
// a plain 1→1 / multi-out lens leaves them `undefined` and pays only
|
|
343
|
-
// `parent` + `put
|
|
442
|
+
// `parent` + `put`.
|
|
344
443
|
class BwdSpec {
|
|
345
444
|
/** Backward target(s): one `Cell` (1→1 / merge) or `Cell[]` (multi-out). */
|
|
346
445
|
parent = undefined;
|
|
347
|
-
/** Lens `put` — backward derivation (dual of `getter`).
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
446
|
+
/** Lens `put` — backward derivation (dual of `getter`). A 1→1 / multi-out
|
|
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)`. */
|
|
351
450
|
// biome-ignore lint/suspicious/noExplicitAny: put fn is opaque shape
|
|
352
451
|
put = undefined;
|
|
353
452
|
/** Backward aggregation node; presence IS the merge-mode discriminant. */
|
|
354
453
|
merge = undefined;
|
|
355
454
|
/** Complement machinery; presence IS the stateful-mode discriminant. */
|
|
356
455
|
stateful = undefined;
|
|
357
|
-
/** Index in `bwdQueue` of this cell's latest push; the drain skips stale
|
|
358
|
-
* entries so each cell propagates backward once per flush, last-write. */
|
|
359
|
-
queueIdx = -1;
|
|
360
456
|
}
|
|
361
457
|
/** Runtime state of a stateful (complement-carrying) lens — the rare
|
|
362
458
|
* backward mode, kept off `BwdSpec` so plain lenses don't carry its slots.
|
|
@@ -365,22 +461,21 @@ class BwdSpec {
|
|
|
365
461
|
class StatefulCore {
|
|
366
462
|
/** Engine-owned memory the view discards. */
|
|
367
463
|
complement;
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
/** Advance the complement: `step(sources, complement, external)`. */
|
|
464
|
+
/** Advance the complement: `step(sources, complement, external)`. (The
|
|
465
|
+
* forward projection `fwd` is captured directly in the getter closure — it
|
|
466
|
+
* is only ever read there — so it costs no slot here.) */
|
|
372
467
|
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
373
468
|
step;
|
|
374
|
-
/**
|
|
375
|
-
* until the first back-write.
|
|
376
|
-
|
|
469
|
+
/** Sources this lens last committed back (the own-vs-external test compares
|
|
470
|
+
* live sources against these); `undefined` until the first back-write. A
|
|
471
|
+
* back-write reads the live sources into a fresh array, builds the committed
|
|
472
|
+
* candidate in place, and keeps it as `last` for the next own-vs-external
|
|
473
|
+
* comparison. */
|
|
474
|
+
last = undefined;
|
|
377
475
|
constructor(complement,
|
|
378
|
-
// biome-ignore lint/suspicious/noExplicitAny: opaque fwd shape
|
|
379
|
-
fwd,
|
|
380
476
|
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
381
477
|
step) {
|
|
382
478
|
this.complement = complement;
|
|
383
|
-
this.fwd = fwd;
|
|
384
479
|
this.step = step;
|
|
385
480
|
}
|
|
386
481
|
}
|
|
@@ -414,34 +509,40 @@ export const isLens = (v) => v instanceof Cell && v.getter !== undefined && v._b
|
|
|
414
509
|
/** Read-only mode: derived with no backward path. */
|
|
415
510
|
export const isReadonly = (v) => v instanceof Cell && v.getter !== undefined && v._bwd === undefined;
|
|
416
511
|
export class Cell {
|
|
417
|
-
|
|
512
|
+
/** @internal */
|
|
513
|
+
flags;
|
|
514
|
+
/** @internal */
|
|
418
515
|
subs;
|
|
516
|
+
/** @internal */
|
|
419
517
|
subsTail;
|
|
518
|
+
/** @internal */
|
|
420
519
|
deps;
|
|
520
|
+
/** @internal */
|
|
421
521
|
depsTail;
|
|
422
|
-
/** Forward derivation (computed/lens/merge). `undefined` ⇒ source. */
|
|
522
|
+
/** @internal Forward derivation (computed/lens/merge). `undefined` ⇒ source. */
|
|
423
523
|
getter;
|
|
424
|
-
/** Per-instance equality
|
|
425
|
-
* construction) so hot paths call it without an `undefined` branch. */
|
|
524
|
+
/** @internal Per-instance equality; always defined (defaults to `Object.is`). */
|
|
426
525
|
_equals;
|
|
427
|
-
/** First-subscriber / last-subscriber lifecycle hooks. */
|
|
526
|
+
/** @internal First-subscriber / last-subscriber lifecycle hooks. */
|
|
428
527
|
_watched;
|
|
528
|
+
/** @internal */
|
|
429
529
|
_unwatchedHook;
|
|
430
|
-
/** Source:
|
|
431
|
-
* Getter cell: `currentValue` = last derived cache, `pendingValue`
|
|
432
|
-
* reused as the deferred backward target (see `set value`). The two
|
|
433
|
-
* roles never coexist, so two fields suffice for four. */
|
|
530
|
+
/** @internal Source: committed value + staged write. */
|
|
434
531
|
currentValue;
|
|
532
|
+
/** @internal */
|
|
435
533
|
pendingValue;
|
|
436
|
-
/** Backward sidecar
|
|
437
|
-
* `undefined` for a read-only cell (source or computed). Allocated only
|
|
438
|
-
* for writable derived cells, keeping the common node lean. Writability
|
|
439
|
-
* is exactly `_bwd !== undefined`. See `BwdSpec`. */
|
|
534
|
+
/** @internal Backward sidecar; `undefined` iff read-only. Writability is `_bwd !== undefined`. */
|
|
440
535
|
_bwd;
|
|
536
|
+
/** @internal Backward dual of `subs`: direct lens-children for back-write cone traversal. */
|
|
537
|
+
_lensSubs;
|
|
538
|
+
/** @internal Backward flag word (`BF`), dual of forward `flags`. */
|
|
539
|
+
bflags;
|
|
540
|
+
/** @internal Guards against `linkBack` re-registering a duplicate in `_lensSubs`. */
|
|
541
|
+
_linkedBack;
|
|
542
|
+
// Every slot is assigned exactly once below, in declaration order, for a stable
|
|
543
|
+
// V8 hidden class across all variants (source / computed / lens / merge).
|
|
441
544
|
constructor(initial, opts) {
|
|
442
|
-
this.
|
|
443
|
-
this.pendingValue = initial;
|
|
444
|
-
// Pre-init every optional slot for a stable V8 hidden class across variants.
|
|
545
|
+
this.flags = F.Mutable;
|
|
445
546
|
this.subs = undefined;
|
|
446
547
|
this.subsTail = undefined;
|
|
447
548
|
this.deps = undefined;
|
|
@@ -450,7 +551,12 @@ export class Cell {
|
|
|
450
551
|
this._equals = Object.is;
|
|
451
552
|
this._watched = undefined;
|
|
452
553
|
this._unwatchedHook = undefined;
|
|
554
|
+
this.currentValue = initial;
|
|
555
|
+
this.pendingValue = initial;
|
|
453
556
|
this._bwd = undefined;
|
|
557
|
+
this._lensSubs = undefined;
|
|
558
|
+
this.bflags = BF.None;
|
|
559
|
+
this._linkedBack = false;
|
|
454
560
|
if (opts !== undefined) {
|
|
455
561
|
if (opts.equals !== undefined)
|
|
456
562
|
this._equals = opts.equals;
|
|
@@ -460,14 +566,12 @@ export class Cell {
|
|
|
460
566
|
this._unwatchedHook = opts.unwatched;
|
|
461
567
|
}
|
|
462
568
|
}
|
|
463
|
-
|
|
464
|
-
this.flags |= F.BwdQueued;
|
|
465
|
-
this._bwd.queueIdx = bwdQueue.length;
|
|
466
|
-
bwdQueue.push(this);
|
|
467
|
-
}
|
|
468
|
-
/** Source write (alien's signal setter). Self-excludes the active
|
|
469
|
-
* network so a body writing its own dep doesn't re-trigger itself. */
|
|
569
|
+
/** @internal Single write-commit point; self-excludes the active network. */
|
|
470
570
|
_writeSource(next) {
|
|
571
|
+
// A forward write to a source with an unresolved back-write demand resolves
|
|
572
|
+
// it FIRST, so the later forward write wins (LWW).
|
|
573
|
+
if (this.bflags & BF.Pending && !draining)
|
|
574
|
+
backResolve(this);
|
|
471
575
|
const prev = this.pendingValue;
|
|
472
576
|
this.pendingValue = next;
|
|
473
577
|
if (!this._equals(prev, next)) {
|
|
@@ -475,15 +579,15 @@ export class Cell {
|
|
|
475
579
|
if (writeHook !== undefined)
|
|
476
580
|
writeHook(this);
|
|
477
581
|
const subs = this.subs;
|
|
478
|
-
if (subs !== undefined)
|
|
582
|
+
if (subs !== undefined) {
|
|
479
583
|
propagate(subs, runDepth > 0, activeNetwork);
|
|
480
|
-
|
|
481
|
-
|
|
584
|
+
autoFlush();
|
|
585
|
+
}
|
|
482
586
|
}
|
|
483
587
|
}
|
|
588
|
+
/** @internal */
|
|
484
589
|
_update() {
|
|
485
590
|
if (this.getter !== undefined) {
|
|
486
|
-
// Computed/lens/merge: re-run the forward derivation.
|
|
487
591
|
this.depsTail = undefined;
|
|
488
592
|
this.flags = F.Mutable | F.RecursedCheck;
|
|
489
593
|
const prev = activeSub;
|
|
@@ -502,12 +606,20 @@ export class Cell {
|
|
|
502
606
|
purgeDeps(this);
|
|
503
607
|
}
|
|
504
608
|
}
|
|
609
|
+
// A back-`Pending` source first resolves its armed back-write (`backResolve`
|
|
610
|
+
// pulls the views registered on it, runs the `put`s, stages it via
|
|
611
|
+
// `_writeSource`) — so its `pendingValue` reflects the back-write before we
|
|
612
|
+
// commit it.
|
|
613
|
+
if (this.bflags & BF.Pending && !draining)
|
|
614
|
+
backResolve(this);
|
|
505
615
|
this.flags = F.Mutable;
|
|
506
616
|
const prevV = this.currentValue;
|
|
507
617
|
this.currentValue = this.pendingValue;
|
|
508
618
|
return !this._equals(prevV, this.currentValue);
|
|
509
619
|
}
|
|
620
|
+
/** @internal */
|
|
510
621
|
_notify() { }
|
|
622
|
+
/** @internal */
|
|
511
623
|
_unwatched() {
|
|
512
624
|
if (this.getter !== undefined && this.depsTail !== undefined) {
|
|
513
625
|
this.flags = F.Mutable | F.Dirty;
|
|
@@ -527,10 +639,6 @@ export class Cell {
|
|
|
527
639
|
activeSub = prev;
|
|
528
640
|
}
|
|
529
641
|
}
|
|
530
|
-
/** Guard: silent coercion to string/number is almost always a bug. */
|
|
531
|
-
[Symbol.toPrimitive](hint) {
|
|
532
|
-
throw new TypeError(`Cell cannot be coerced to ${hint} — use \`.value\``);
|
|
533
|
-
}
|
|
534
642
|
// Construction helpers build via `new this()` so a subclass static
|
|
535
643
|
// (`Vec.lens(...)`) yields a `Vec` with its constructor-set equality.
|
|
536
644
|
// Every lens has a structural backward target (`_bwd.parent`), which is
|
|
@@ -545,10 +653,11 @@ export class Cell {
|
|
|
545
653
|
derive(fn) {
|
|
546
654
|
return buildDerived(this.constructor, () => fn(this.value));
|
|
547
655
|
}
|
|
548
|
-
/** Backward-
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
|
|
656
|
+
/** Backward fan-in node. Forward, the identity view of its parent;
|
|
657
|
+
* backward, the convergence point where N contributors (upstream lenses
|
|
658
|
+
* and direct writes) fold into one value for the parent. `fold` is handed
|
|
659
|
+
* every live push at once; omitted, it is last-writer-wins. */
|
|
660
|
+
merge(fold) {
|
|
552
661
|
if (this.getter !== undefined && this._bwd === undefined) {
|
|
553
662
|
throw new TypeError("merge: receiver is read-only");
|
|
554
663
|
}
|
|
@@ -558,7 +667,7 @@ export class Cell {
|
|
|
558
667
|
cell.getter = () => parent.value;
|
|
559
668
|
const b = (cell._bwd = new BwdSpec());
|
|
560
669
|
b.parent = parent;
|
|
561
|
-
b.merge = new MergeNode(
|
|
670
|
+
b.merge = new MergeNode(fold);
|
|
562
671
|
return cell;
|
|
563
672
|
}
|
|
564
673
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch
|
|
@@ -575,10 +684,10 @@ export class Cell {
|
|
|
575
684
|
static is(v) {
|
|
576
685
|
return v instanceof this;
|
|
577
686
|
}
|
|
578
|
-
/**
|
|
687
|
+
/** Coerce `Val<Inner<Cls>>` → `Cls`: instance → identity, RO cell →
|
|
579
688
|
* tracked `derive`, literal → fresh seed. */
|
|
580
689
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
581
|
-
static
|
|
690
|
+
static coerce(v) {
|
|
582
691
|
if (v instanceof this)
|
|
583
692
|
return v;
|
|
584
693
|
if (v instanceof Cell) {
|
|
@@ -594,27 +703,25 @@ export class Cell {
|
|
|
594
703
|
const cell = new this();
|
|
595
704
|
cell.flags = F.Mutable | F.Dirty;
|
|
596
705
|
cell.getter = () => v;
|
|
597
|
-
|
|
598
|
-
|
|
706
|
+
// Parentless `_bwd`: `writeBack` absorbs at `parent === undefined` before any
|
|
707
|
+
// `put`, so the sink needs no closure — writability is just `_bwd !== undefined`.
|
|
708
|
+
cell._bwd = new BwdSpec();
|
|
599
709
|
return cell;
|
|
600
710
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
return buildDerived(ctor, () => get(parent.value));
|
|
614
|
-
}
|
|
615
|
-
// Spread-replace reads the current source ⇒ source-reading (lens) form.
|
|
616
|
-
return buildLens1(ctor, parent, get, (v, s) => ({ ...s, [key]: v }), true);
|
|
711
|
+
}
|
|
712
|
+
/** Typed field lens onto `parent.value[key]`. RO parent → RO derive;
|
|
713
|
+
* writable parent → bidirectional lens with spread-replace `put`. */
|
|
714
|
+
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
715
|
+
export function fieldOf(
|
|
716
|
+
// biome-ignore lint/suspicious/noExplicitAny: parent is contravariant on put
|
|
717
|
+
parent, key, Cls) {
|
|
718
|
+
const ctor = Cls;
|
|
719
|
+
const get = (s) => s[key];
|
|
720
|
+
const ro = parent.getter !== undefined && parent._bwd === undefined;
|
|
721
|
+
if (ro) {
|
|
722
|
+
return buildDerived(ctor, () => get(parent.value));
|
|
617
723
|
}
|
|
724
|
+
return buildLens1(ctor, parent, get, (v, s) => ({ ...s, [key]: v }), true);
|
|
618
725
|
}
|
|
619
726
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
620
727
|
function buildDerived(Cls, getter) {
|
|
@@ -629,9 +736,11 @@ function buildLens1(Cls, parent, fwd, bwd, readsSource) {
|
|
|
629
736
|
cell.flags = F.Mutable | F.Dirty;
|
|
630
737
|
cell.getter = (() => fwd(parent.value));
|
|
631
738
|
const b = (cell._bwd = new BwdSpec());
|
|
632
|
-
// Source-reading lenses
|
|
633
|
-
//
|
|
634
|
-
|
|
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;
|
|
635
744
|
b.parent = parent;
|
|
636
745
|
return cell;
|
|
637
746
|
}
|
|
@@ -650,13 +759,21 @@ function buildLensN(Cls, parents, fwd, bwd, readsSource) {
|
|
|
650
759
|
return cell; // read-only derive-N
|
|
651
760
|
const b = (cell._bwd = new BwdSpec());
|
|
652
761
|
b.parent = parents;
|
|
653
|
-
|
|
654
|
-
|
|
762
|
+
if (readsSource) {
|
|
763
|
+
// Own reused buffer, NOT the getter's `vals`: the walk reads each parent's
|
|
764
|
+
// primal (`backPrimal`), so a separate array avoids aliasing the getter's.
|
|
765
|
+
// `bwd` consumes it synchronously and must not retain it (same as `fwd`);
|
|
766
|
+
// re-entry through the same lens is impossible (the lens graph is a DAG).
|
|
767
|
+
const args = new Array(n);
|
|
768
|
+
b.put = (target) => {
|
|
655
769
|
for (let i = 0; i < n; i++)
|
|
656
|
-
|
|
657
|
-
return bwd(target,
|
|
658
|
-
}
|
|
659
|
-
|
|
770
|
+
args[i] = backPrimal(parents[i]);
|
|
771
|
+
return bwd(target, args);
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
b.put = (target) => bwd(target);
|
|
776
|
+
}
|
|
660
777
|
return cell;
|
|
661
778
|
}
|
|
662
779
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
@@ -671,7 +788,8 @@ spec) {
|
|
|
671
788
|
const seed = new Array(n);
|
|
672
789
|
for (let i = 0; i < n; i++)
|
|
673
790
|
seed[i] = parents[i].peek();
|
|
674
|
-
const sc = (b.stateful = new StatefulCore(spec.init(seed), spec.
|
|
791
|
+
const sc = (b.stateful = new StatefulCore(spec.init(seed), spec.step));
|
|
792
|
+
const fwd = spec.fwd;
|
|
675
793
|
b.put = spec.bwd;
|
|
676
794
|
b.parent = parents;
|
|
677
795
|
cell.getter = (() => {
|
|
@@ -680,7 +798,7 @@ spec) {
|
|
|
680
798
|
// External unless the live sources still equal this lens's own last
|
|
681
799
|
// back-write.
|
|
682
800
|
let external = true;
|
|
683
|
-
const lb = sc.
|
|
801
|
+
const lb = sc.last;
|
|
684
802
|
if (lb !== undefined) {
|
|
685
803
|
external = false;
|
|
686
804
|
for (let i = 0; i < n; i++) {
|
|
@@ -691,7 +809,7 @@ spec) {
|
|
|
691
809
|
}
|
|
692
810
|
}
|
|
693
811
|
sc.complement = sc.step(vals, sc.complement, external);
|
|
694
|
-
return
|
|
812
|
+
return fwd(vals, sc.complement);
|
|
695
813
|
});
|
|
696
814
|
return cell;
|
|
697
815
|
}
|
|
@@ -707,16 +825,17 @@ spec) {
|
|
|
707
825
|
const cell = new Cls();
|
|
708
826
|
cell.flags = F.Mutable | F.Dirty;
|
|
709
827
|
const b = (cell._bwd = new BwdSpec());
|
|
710
|
-
const sc = (b.stateful = new StatefulCore(spec.init([parent.peek()]), spec.
|
|
828
|
+
const sc = (b.stateful = new StatefulCore(spec.init([parent.peek()]), spec.step));
|
|
829
|
+
const fwd = spec.fwd;
|
|
711
830
|
b.put = spec.bwd;
|
|
712
831
|
b.parent = [parent];
|
|
713
832
|
const vals = [undefined];
|
|
714
833
|
cell.getter = (() => {
|
|
715
834
|
const v = (vals[0] = parent.value);
|
|
716
|
-
const lb = sc.
|
|
835
|
+
const lb = sc.last;
|
|
717
836
|
const external = lb === undefined || lb[0] !== v;
|
|
718
837
|
sc.complement = sc.step(vals, sc.complement, external);
|
|
719
|
-
return
|
|
838
|
+
return fwd(vals, sc.complement);
|
|
720
839
|
});
|
|
721
840
|
return cell;
|
|
722
841
|
}
|
|
@@ -746,9 +865,17 @@ function dispatchLens(ctor, args) {
|
|
|
746
865
|
return buildLensN(ctor, parent, a, b, readsSource);
|
|
747
866
|
return buildLens1(ctor, parent, a, b, readsSource);
|
|
748
867
|
}
|
|
749
|
-
//
|
|
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.
|
|
750
870
|
Object.defineProperty(Cell.prototype, "value", {
|
|
751
871
|
get() {
|
|
872
|
+
// Reading is the PULL. A back-marked cell resolves at this clean entry,
|
|
873
|
+
// BEFORE its own compute, so a source-reading `put` never re-enters a
|
|
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).
|
|
877
|
+
if (this.bflags & BACK_MARKED && !draining)
|
|
878
|
+
backResolve(this);
|
|
752
879
|
const flags = this.flags;
|
|
753
880
|
if (this.getter !== undefined) {
|
|
754
881
|
if (flags & F.RecursedCheck) {
|
|
@@ -763,26 +890,11 @@ Object.defineProperty(Cell.prototype, "value", {
|
|
|
763
890
|
shallowPropagate(subs);
|
|
764
891
|
}
|
|
765
892
|
}
|
|
766
|
-
else if (!flags) {
|
|
767
|
-
// First read: lazy init.
|
|
768
|
-
this.flags = F.Mutable | F.RecursedCheck;
|
|
769
|
-
const prev = activeSub;
|
|
770
|
-
activeSub = this;
|
|
771
|
-
let threw = true;
|
|
772
|
-
try {
|
|
773
|
-
this.currentValue = this.getter();
|
|
774
|
-
threw = false;
|
|
775
|
-
}
|
|
776
|
-
finally {
|
|
777
|
-
activeSub = prev;
|
|
778
|
-
this.flags = threw ? F.Mutable | F.Dirty : this.flags & ~F.RecursedCheck;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
893
|
if (activeSub !== undefined)
|
|
782
894
|
link(this, activeSub, cycle);
|
|
783
895
|
return this.currentValue;
|
|
784
896
|
}
|
|
785
|
-
//
|
|
897
|
+
// Source path.
|
|
786
898
|
if (flags & F.Dirty) {
|
|
787
899
|
this.flags = F.Mutable;
|
|
788
900
|
const prevV = this.currentValue;
|
|
@@ -802,228 +914,339 @@ Object.defineProperty(Cell.prototype, "value", {
|
|
|
802
914
|
this._writeSource(next);
|
|
803
915
|
return;
|
|
804
916
|
}
|
|
805
|
-
// Backward write. Deferred while batching/flushing so repeated writes
|
|
806
|
-
// coalesce (last-write-wins) and merge folds wait for all
|
|
807
|
-
// contributors; eager + synchronous otherwise. Exception: inside a
|
|
808
|
-
// network body writes are eager so the body's fixpoint loop observes
|
|
809
|
-
// its own edits via `peek()` between steps.
|
|
810
917
|
const b = this._bwd;
|
|
811
918
|
if (b === undefined) {
|
|
812
919
|
throw new TypeError("Cannot write to a computed");
|
|
813
920
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
else
|
|
820
|
-
bwdUntracked(this, undefined, false);
|
|
921
|
+
// GetPut for a multi-parent split: its `put` may move sources even when the
|
|
922
|
+
// view is unchanged (a lossy redistribution that `_writeSource`'s per-source
|
|
923
|
+
// equality can't catch), so absorb a write that maps to the current view
|
|
924
|
+
// here. (Stateful excluded — peeking would step its complement.)
|
|
925
|
+
if (Array.isArray(b.parent) && b.stateful === undefined && this._equals(next, this.peek())) {
|
|
821
926
|
return;
|
|
822
927
|
}
|
|
823
|
-
|
|
824
|
-
// Multi-parent / stateful: defer to flush so a split coalesces, a
|
|
825
|
-
// merge folds after all contributors land, and a complement steps
|
|
826
|
-
// once. Reuse `pendingValue` (unused by a getter's forward path) as
|
|
827
|
-
// the deferred backward target; drained by flush. Entry no-op vs the
|
|
828
|
-
// current view (GetPut) skips the walk.
|
|
829
|
-
if (this._equals(next, this.peek()))
|
|
830
|
-
return;
|
|
831
|
-
this.pendingValue = next;
|
|
832
|
-
this._enqueueBwd();
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
// Single-parent lens: run the walk now. Inside a batch `_writeSource`
|
|
836
|
-
// stages the source (Dirty + pending) and defers only the flush, so
|
|
837
|
-
// the view reads back consistently and a later write supersedes via
|
|
838
|
-
// the source's pending value — last-write-wins, no queue, no lost
|
|
839
|
-
// revert. We must NOT peek the view here when batching: that would
|
|
840
|
-
// commit the source's pending value and break net-zero revert
|
|
841
|
-
// coalescing — `propagateBwd`'s `settled` no-op stop prunes a true
|
|
842
|
-
// no-op without committing. Outside a batch, the O(1) GetPut check is
|
|
843
|
-
// safe (no source is staged) and worth keeping.
|
|
844
|
-
if (!deferred && this._equals(next, this.peek()))
|
|
845
|
-
return;
|
|
846
|
-
bwdUntracked(this, next, deferred);
|
|
928
|
+
arm(this, next);
|
|
847
929
|
},
|
|
848
930
|
enumerable: false,
|
|
849
931
|
configurable: false,
|
|
850
932
|
});
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
// establish forward deps on whatever `activeSub` is writing (e.g. an
|
|
861
|
-
// effect that writes a lens). All backward entry points route through here.
|
|
862
|
-
function bwdUntracked(cell, target, deferred) {
|
|
863
|
-
const prev = activeSub;
|
|
864
|
-
activeSub = undefined;
|
|
865
|
-
try {
|
|
866
|
-
propagateBwd(cell, target, deferred);
|
|
867
|
-
}
|
|
868
|
-
finally {
|
|
869
|
-
activeSub = prev;
|
|
933
|
+
/** Backward push: arm a back-write of `target` on view `node` (the dual of a
|
|
934
|
+
* source `set`). A re-write of a still-armed view (an unobserved drag) keeps
|
|
935
|
+
* only the last target — the path down to the sources is already marked, so
|
|
936
|
+
* skip the walk and coalesce. The trailing `schedule` wakes the effects the
|
|
937
|
+
* push woke (on the microtask turn); each effect's read pulls its own cone. */
|
|
938
|
+
function arm(node, target) {
|
|
939
|
+
if (!(node.bflags & BF.Dirty)) {
|
|
940
|
+
markDown(node); // flag path + wake cones, FIRST (a throw arms nothing)
|
|
941
|
+
node.bflags |= BF.Dirty;
|
|
870
942
|
}
|
|
943
|
+
node.pendingValue = target; // the view holds its own demand (getters ignore this field)
|
|
944
|
+
autoFlush();
|
|
871
945
|
}
|
|
872
|
-
/**
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
946
|
+
/** MARK (push), dual of forward `propagate`: descend `start`'s static backward
|
|
947
|
+
* path down `_bwd.parent` to its sources, flag each node `BF.Pending`, register
|
|
948
|
+
* the reverse edge (`linkBack`), and wake every source's forward cone. Runs no
|
|
949
|
+
* `put` — an over-approximation `resolveCone` later resolves precisely.
|
|
950
|
+
*
|
|
951
|
+
* `BF.Pending` is its own dedup: an already-marked node has its whole (static)
|
|
952
|
+
* subtree marked, so descent stops there (diamonds cost one visit). A merge
|
|
953
|
+
* relays to its parent; a sole read-only-derived parent has nowhere to land →
|
|
954
|
+
* throw. The 1→1 spine allocates nothing (`stack` is built only on a branch). */
|
|
955
|
+
function markDown(start) {
|
|
956
|
+
let node = start;
|
|
957
|
+
let stack;
|
|
958
|
+
for (;;) {
|
|
959
|
+
let next;
|
|
960
|
+
if (isSource(node)) {
|
|
961
|
+
// Leaf (dual of a `Dirty` source): wake its cone ONCE.
|
|
962
|
+
if (!(node.bflags & BF.Pending)) {
|
|
963
|
+
node.bflags |= BF.Pending;
|
|
964
|
+
const subs = node.subs;
|
|
965
|
+
if (subs !== undefined)
|
|
966
|
+
propagate(subs, runDepth > 0, activeNetwork);
|
|
967
|
+
}
|
|
894
968
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
969
|
+
else if (node === start || !(node.bflags & BF.Pending)) {
|
|
970
|
+
// On the back-path (dual of a `Pending` intermediate). An already-marked
|
|
971
|
+
// intermediate (≠ start) has its subtree marked — stop (diamond dedup).
|
|
972
|
+
if (node !== start)
|
|
973
|
+
node.bflags |= BF.Pending;
|
|
974
|
+
linkBack(node); // register the reverse edge lazily, on first back-write
|
|
975
|
+
// `b.parent` is the back-target for EVERY mode (a merge's fold target is
|
|
976
|
+
// just its single, always-writable parent), so one descent covers all.
|
|
977
|
+
const parent = node._bwd.parent;
|
|
978
|
+
if (parent !== undefined) {
|
|
979
|
+
if (Array.isArray(parent)) {
|
|
980
|
+
const multi = parent.length > 1;
|
|
981
|
+
for (let i = 0; i < parent.length; i++) {
|
|
982
|
+
const p = parent[i];
|
|
983
|
+
if (isReadOnlyDerived(p)) {
|
|
984
|
+
// A split routes around it; a sole parent can't.
|
|
985
|
+
if (!multi)
|
|
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
|
+
}
|
|
1000
|
+
}
|
|
900
1001
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
// into `put` at build time (see `buildLens1`); `pin` ignores `v`.
|
|
904
|
-
push = cb.put(v);
|
|
1002
|
+
if (next !== undefined) {
|
|
1003
|
+
node = next;
|
|
905
1004
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
return;
|
|
909
|
-
// Concrete no-op stop: if the parent already holds `push`, committing
|
|
910
|
-
// changes nothing upstream, so the walk stops. Sound for ANY topology
|
|
911
|
-
// (no speculation). A lossy lens hides an off-grid edit by returning
|
|
912
|
-
// the current source from `put`. Merge parents fold instead. `settled`
|
|
913
|
-
// reads a batched source's pending value WITHOUT committing it, so a
|
|
914
|
-
// net-zero revert leaves the source unchanged and downstream un-fired.
|
|
915
|
-
const pb = parent._bwd;
|
|
916
|
-
const parentMerge = pb !== undefined ? pb.merge : undefined;
|
|
917
|
-
if (parentMerge === undefined && parent._equals(push, settled(parent)))
|
|
918
|
-
return;
|
|
919
|
-
if (parentMerge !== undefined) {
|
|
920
|
-
parentMerge.receive(cell, push);
|
|
921
|
-
if (deferred) {
|
|
922
|
-
if (!(parent.flags & F.BwdQueued))
|
|
923
|
-
parent._enqueueBwd();
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
cell = parent;
|
|
927
|
-
continue;
|
|
1005
|
+
else if (stack !== undefined && stack.length > 0) {
|
|
1006
|
+
node = stack.pop();
|
|
928
1007
|
}
|
|
929
|
-
|
|
930
|
-
// Source: commit + forward-propagate (the forward write).
|
|
931
|
-
parent._writeSource(push);
|
|
1008
|
+
else {
|
|
932
1009
|
return;
|
|
933
1010
|
}
|
|
934
|
-
// Parent is a lens: keep walking, carrying its new view value.
|
|
935
|
-
cell = parent;
|
|
936
|
-
v = push;
|
|
937
1011
|
}
|
|
938
1012
|
}
|
|
939
|
-
/**
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
if (
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (u === undefined) {
|
|
968
|
-
cand[i] = vals[i];
|
|
969
|
-
}
|
|
970
|
-
else {
|
|
971
|
-
cand[i] = u;
|
|
972
|
-
anyWrite = true;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
sc.complement = sc.step(cand, res.complement, false);
|
|
976
|
-
if (!anyWrite) {
|
|
977
|
-
// Complement-only change (no source moves): mark dirty for a correct next read.
|
|
978
|
-
cell.flags = F.Mutable | F.Dirty;
|
|
979
|
-
return;
|
|
1013
|
+
/** RESOLVE (pull), dual of forward `checkDirty`: resolve ONE node's whole
|
|
1014
|
+
* back-cone. Ascend the static reverse adjacency `_lensSubs` (only `BACK_MARKED`
|
|
1015
|
+
* children) to the armed views above, `writeBack`ing each. Source-CENTRIC — a
|
|
1016
|
+
* source must reflect ALL its writers (they compose into one committed value),
|
|
1017
|
+
* so a call on the source resolves every co-writer together and commits once.
|
|
1018
|
+
* `BF.Dirty` is the dedup.
|
|
1019
|
+
*
|
|
1020
|
+
* Recursive so a MERGE folds POST-ORDER (the one non-dual ingredient): resolve
|
|
1021
|
+
* its contributors first (each cascades a `put` into `contributions`), then fold
|
|
1022
|
+
* once on the way back up and write the parent, as a getter reads deps then
|
|
1023
|
+
* computes. */
|
|
1024
|
+
function resolveCone(node) {
|
|
1025
|
+
const b = node._bwd;
|
|
1026
|
+
const merge = b !== undefined ? b.merge : undefined;
|
|
1027
|
+
if (merge !== undefined)
|
|
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).
|
|
1031
|
+
if (node.bflags & BF.Dirty) {
|
|
1032
|
+
node.bflags &= ~BF.Dirty;
|
|
1033
|
+
writeBack(node, node.pendingValue);
|
|
1034
|
+
}
|
|
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);
|
|
980
1041
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
+
}
|
|
1047
|
+
/** PULL entry for a back-marked `start`. Source-centric: a source read resolves
|
|
1048
|
+
* its own cone; a VIEW read first DESCENDS its marked back-path (`_bwd.parent`
|
|
1049
|
+
* through `BF.Pending`) to the sources, then resolves each source's whole cone
|
|
1050
|
+
* (so co-writers compose and the source commits once). The `draining` guard
|
|
1051
|
+
* stops a `put`'s source read from re-entering. */
|
|
1052
|
+
function backResolve(start) {
|
|
1053
|
+
draining = true;
|
|
1054
|
+
++batchDepth;
|
|
1055
|
+
const prev = activeSub;
|
|
1056
|
+
activeSub = undefined;
|
|
1057
|
+
try {
|
|
1058
|
+
if (isSource(start)) {
|
|
1059
|
+
resolveCone(start);
|
|
985
1060
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1061
|
+
else {
|
|
1062
|
+
// Descend the marked back-path to every source, resolving each source's
|
|
1063
|
+
// cone. A view with no source to land on (a `pin` sink) resolves itself.
|
|
1064
|
+
let node = start;
|
|
1065
|
+
let stack;
|
|
1066
|
+
let reached = false;
|
|
1067
|
+
for (;;) {
|
|
1068
|
+
let next;
|
|
1069
|
+
const b = node._bwd;
|
|
1070
|
+
const parent = b !== undefined ? b.parent : undefined; // merge's parent IS b.parent
|
|
1071
|
+
if (parent !== undefined) {
|
|
1072
|
+
if (Array.isArray(parent)) {
|
|
1073
|
+
for (let i = 0; i < parent.length; i++) {
|
|
1074
|
+
const p = parent[i];
|
|
1075
|
+
if (isSource(p)) {
|
|
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
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (next !== undefined)
|
|
1098
|
+
node = next;
|
|
1099
|
+
else if (stack !== undefined && stack.length > 0)
|
|
1100
|
+
node = stack.pop();
|
|
1101
|
+
else
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
if (!reached && start.bflags & BF.Dirty) {
|
|
1105
|
+
start.bflags &= ~BF.Dirty;
|
|
1106
|
+
writeBack(start, start.pendingValue);
|
|
1107
|
+
}
|
|
989
1108
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1109
|
+
}
|
|
1110
|
+
finally {
|
|
1111
|
+
activeSub = prev;
|
|
1112
|
+
--batchDepth;
|
|
1113
|
+
draining = false;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/** Resolve any back-write a woken reactive node reads DIRECTLY. `checkDirty`
|
|
1117
|
+
* alone catches back-writes that move a source (it ascends to the source),
|
|
1118
|
+
* but a stateful "stash" moves only the VIEW (the complement echoes a value
|
|
1119
|
+
* back, no source changes) — invisible to a source-based check. Resolving the
|
|
1120
|
+
* node's back-marked deps here makes that view-change visible before the
|
|
1121
|
+
* dirtiness check. Granular: only this node's own deps. */
|
|
1122
|
+
function resolveBackDeps(node) {
|
|
1123
|
+
for (let l = node.deps; l !== undefined; l = l.nextDep) {
|
|
1124
|
+
const d = l.dep;
|
|
1125
|
+
if (d.bflags & BACK_MARKED && !draining)
|
|
1126
|
+
backResolve(d);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/** Backward commit/compute (dual of forward `_update`): drive a back-write of
|
|
1130
|
+
* `target` toward the sources, applying each lens's `put`, clearing `BF.Pending`
|
|
1131
|
+
* on descent, and staging each source as it's reached (so a later sibling reading
|
|
1132
|
+
* that SAME source composes rather than clobbers). A `SKIP` per-parent slot
|
|
1133
|
+
* prunes a branch; every other slot is written verbatim, `undefined` included. A
|
|
1134
|
+
* `merge` accumulates (folded post-order by `resolveCone`); a read-only parent
|
|
1135
|
+
* throws (already caught at MARK time). */
|
|
1136
|
+
function writeBack(node, target) {
|
|
1137
|
+
if (isSource(node)) {
|
|
1138
|
+
node._writeSource(target); // source — staged now, visible to later siblings
|
|
1139
|
+
// Clear this source's back-`Pending`, then re-assert it iff a lens-child is
|
|
1140
|
+
// STILL armed (an overlapping co-writer through this same source) — a later
|
|
1141
|
+
// read must still resolve them, else that write is lost. (Forward writes
|
|
1142
|
+
// live on `flags`, back-state on `bflags`, so the source-stage above no
|
|
1143
|
+
// longer touches `Pending`; we own clearing it here.) `_lensSubs` holds only
|
|
1144
|
+
// ever-back-written children, so this is a short flag scan, not a registry walk.
|
|
1145
|
+
node.bflags &= ~BF.Pending;
|
|
1146
|
+
const subs = node._lensSubs;
|
|
1147
|
+
if (subs !== undefined) {
|
|
1148
|
+
for (let i = 0; i < subs.length; i++)
|
|
1149
|
+
if (subs[i].bflags & BACK_MARKED) {
|
|
1150
|
+
node.bflags |= BF.Pending;
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
993
1153
|
}
|
|
994
1154
|
return;
|
|
995
1155
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (
|
|
1001
|
-
|
|
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
|
|
1002
1162
|
return;
|
|
1003
1163
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
}
|
|
1188
|
+
sc.complement = sc.step(vals, sc.complement, external);
|
|
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;
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
out = b.put(target);
|
|
1204
|
+
}
|
|
1205
|
+
// A short `out` skips the trailing parents (the dual of a forward getter
|
|
1206
|
+
// reading fewer deps); `SKIP` skips a specific slot; a real `undefined` is
|
|
1207
|
+
// written verbatim.
|
|
1208
|
+
let wrote = false;
|
|
1209
|
+
const m = out.length < n ? out.length : n;
|
|
1210
|
+
for (let i = 0; i < m; i++) {
|
|
1211
|
+
const u = out[i];
|
|
1212
|
+
if (u !== SKIP) {
|
|
1213
|
+
wrote = true;
|
|
1214
|
+
writeBack(parent[i], u);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
// A stateful lens can change its VIEW through the complement alone, moving
|
|
1218
|
+
// no source (a degenerate "stash" — e.g. a collapsed axis remembering the
|
|
1219
|
+
// written angle, or a broken parse holding the typed text). No source write
|
|
1220
|
+
// means the forward cone never fires, so invalidate the node's own cache and
|
|
1221
|
+
// propagate to its observers here.
|
|
1222
|
+
if (!wrote && sc !== undefined) {
|
|
1223
|
+
node.flags |= F.Dirty;
|
|
1224
|
+
const subs = node.subs;
|
|
1225
|
+
if (subs !== undefined)
|
|
1226
|
+
propagate(subs, runDepth > 0, activeNetwork);
|
|
1227
|
+
}
|
|
1228
|
+
return;
|
|
1011
1229
|
}
|
|
1230
|
+
// 1→1 lens.
|
|
1231
|
+
writeBack(parent, b.put(target));
|
|
1012
1232
|
}
|
|
1013
|
-
/**
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1233
|
+
/** Fold one merge's gathered contributions ONCE (its policy; default
|
|
1234
|
+
* last-writer-wins) and write the result up to its parent. Called post-order
|
|
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`). */
|
|
1238
|
+
function foldMerge(parent, mn) {
|
|
1239
|
+
const vals = mn.contributions;
|
|
1240
|
+
const fold = mn.foldFn;
|
|
1241
|
+
let folded;
|
|
1242
|
+
if (fold !== undefined)
|
|
1243
|
+
folded = fold(vals);
|
|
1244
|
+
else if (vals.length > 0)
|
|
1245
|
+
folded = vals[vals.length - 1];
|
|
1246
|
+
else
|
|
1247
|
+
return; // last-writer-wins with no contributor: leave the parent
|
|
1248
|
+
vals.length = 0; // reuse the merge-owned buffer in place (fold must not retain it)
|
|
1249
|
+
writeBack(parent, folded);
|
|
1027
1250
|
}
|
|
1028
1251
|
/** Writable source; passes an existing `Writable` through (idempotent). */
|
|
1029
1252
|
export function cell(initial, opts) {
|
|
@@ -1101,6 +1324,10 @@ class Effect {
|
|
|
1101
1324
|
this._runCleanup();
|
|
1102
1325
|
}
|
|
1103
1326
|
_run() {
|
|
1327
|
+
// Resolve back-writes this effect reads directly (incl. view-only stashes),
|
|
1328
|
+
// then let `checkDirty` resolve any back-`Pending` source reached deeper.
|
|
1329
|
+
if (this.deps !== undefined)
|
|
1330
|
+
resolveBackDeps(this);
|
|
1104
1331
|
const flags = this.flags;
|
|
1105
1332
|
if (flags & F.Dirty || (flags & F.Pending && checkDirty(this.deps, this))) {
|
|
1106
1333
|
if (this.cleanup) {
|
|
@@ -1146,47 +1373,59 @@ export function effect(fn) {
|
|
|
1146
1373
|
const e = new Effect(fn);
|
|
1147
1374
|
return () => e._unwatched();
|
|
1148
1375
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1376
|
+
/** Run effects woken by a write. Backward work is pulled lazily per read
|
|
1377
|
+
* (`resolveBackDeps` + `checkDirty` → `backResolve`, which also folds merges),
|
|
1378
|
+
* so flush owns NO backward bookkeeping — just the effect queue. */
|
|
1152
1379
|
function flush() {
|
|
1153
1380
|
if (flushing)
|
|
1154
1381
|
return;
|
|
1155
1382
|
flushing = true;
|
|
1156
|
-
let bwdIndex = 0;
|
|
1157
1383
|
try {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const cb = cell._bwd;
|
|
1163
|
-
if (cb.queueIdx !== bwdIndex || !(cell.flags & F.BwdQueued)) {
|
|
1164
|
-
bwdIndex++;
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
bwdIndex++;
|
|
1168
|
-
cell.flags &= ~F.BwdQueued;
|
|
1169
|
-
if (cb.merge !== undefined) {
|
|
1170
|
-
bwdUntracked(cell, undefined, true);
|
|
1171
|
-
}
|
|
1172
|
-
else {
|
|
1173
|
-
bwdUntracked(cell, cell.pendingValue, true);
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
while (notifyIndex < queuedLength) {
|
|
1177
|
-
const e = queued[notifyIndex];
|
|
1178
|
-
queued[notifyIndex++] = undefined;
|
|
1179
|
-
e._run();
|
|
1180
|
-
}
|
|
1384
|
+
while (notifyIndex < queuedLength) {
|
|
1385
|
+
const e = queued[notifyIndex];
|
|
1386
|
+
queued[notifyIndex++] = undefined;
|
|
1387
|
+
e._run();
|
|
1181
1388
|
}
|
|
1182
1389
|
}
|
|
1183
1390
|
finally {
|
|
1184
|
-
bwdQueue.length = 0;
|
|
1185
1391
|
notifyIndex = 0;
|
|
1186
1392
|
queuedLength = 0;
|
|
1393
|
+
networkQueued = false;
|
|
1187
1394
|
flushing = false;
|
|
1188
1395
|
}
|
|
1189
1396
|
}
|
|
1397
|
+
/** Queue an effect flush for the end of the current microtask turn (idempotent).
|
|
1398
|
+
* A write wakes effects asynchronously; many writes in one turn coalesce. */
|
|
1399
|
+
function schedule() {
|
|
1400
|
+
if (scheduled)
|
|
1401
|
+
return;
|
|
1402
|
+
scheduled = true;
|
|
1403
|
+
queueMicrotask(() => {
|
|
1404
|
+
scheduled = false;
|
|
1405
|
+
flush();
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
/** Resolve the queue after a write: no-op inside a `batch`/flush (the barrier
|
|
1409
|
+
* owns flushing), else synchronously if a network is waiting (eager solve) or
|
|
1410
|
+
* deferred to the microtask (coalesced effects). */
|
|
1411
|
+
function autoFlush() {
|
|
1412
|
+
if (batchDepth !== 0 || flushing)
|
|
1413
|
+
return;
|
|
1414
|
+
if (networkQueued)
|
|
1415
|
+
flush();
|
|
1416
|
+
else
|
|
1417
|
+
schedule();
|
|
1418
|
+
}
|
|
1419
|
+
/** Run all pending effects NOW, synchronously. The escape hatch for code (tests,
|
|
1420
|
+
* imperative call sites) that must observe effect side-effects before yielding
|
|
1421
|
+
* to the microtask queue. Reads never need it — they pull current values. */
|
|
1422
|
+
export function settle() {
|
|
1423
|
+
flush();
|
|
1424
|
+
}
|
|
1425
|
+
/** Group writes and flush effects SYNCHRONOUSLY at the end of `fn` — a sync
|
|
1426
|
+
* barrier. Effects coalesce on the microtask turn anyway (see `schedule`), so
|
|
1427
|
+
* `batch` is no longer needed for that; reach for it only when you must run the
|
|
1428
|
+
* woken effects before the call returns (and don't want a `settle()`). */
|
|
1190
1429
|
export function batch(fn) {
|
|
1191
1430
|
++batchDepth;
|
|
1192
1431
|
try {
|
|
@@ -1243,6 +1482,7 @@ class _NetworkNode {
|
|
|
1243
1482
|
return;
|
|
1244
1483
|
}
|
|
1245
1484
|
queued[queuedLength++] = this;
|
|
1485
|
+
networkQueued = true; // eager: a queued network forces a synchronous flush
|
|
1246
1486
|
this.flags &= ~F.Watching;
|
|
1247
1487
|
}
|
|
1248
1488
|
_unwatched() {
|
|
@@ -1257,6 +1497,8 @@ class _NetworkNode {
|
|
|
1257
1497
|
_run() {
|
|
1258
1498
|
if (this.disposed)
|
|
1259
1499
|
return;
|
|
1500
|
+
if (this.deps !== undefined)
|
|
1501
|
+
resolveBackDeps(this);
|
|
1260
1502
|
const flags = this.flags;
|
|
1261
1503
|
if (flags & F.Dirty || (flags & F.Pending && checkDirty(this.deps, this))) {
|
|
1262
1504
|
this._runBody(this._computeDirty());
|
|
@@ -1279,7 +1521,7 @@ class _NetworkNode {
|
|
|
1279
1521
|
_runBody(dirty) {
|
|
1280
1522
|
// RecursedCheck doubles as the "body running" guard (see flush()).
|
|
1281
1523
|
this.flags = F.Watching | F.RecursedCheck;
|
|
1282
|
-
const
|
|
1524
|
+
const prevNetwork = activeNetwork;
|
|
1283
1525
|
activeNetwork = this;
|
|
1284
1526
|
try {
|
|
1285
1527
|
++cycle;
|
|
@@ -1295,7 +1537,7 @@ class _NetworkNode {
|
|
|
1295
1537
|
}
|
|
1296
1538
|
finally {
|
|
1297
1539
|
--runDepth;
|
|
1298
|
-
activeNetwork =
|
|
1540
|
+
activeNetwork = prevNetwork;
|
|
1299
1541
|
this.flags &= ~F.RecursedCheck;
|
|
1300
1542
|
this.lastValues.clear();
|
|
1301
1543
|
let l = this.deps;
|
|
@@ -1374,10 +1616,9 @@ deps, body, opts) {
|
|
|
1374
1616
|
node._initWithHandle(handle, deps);
|
|
1375
1617
|
return handle;
|
|
1376
1618
|
}
|
|
1377
|
-
// MISC stuff used by a few places, to revisit...
|
|
1378
1619
|
// ── value-class authoring helpers ──────────────────────────────────
|
|
1379
1620
|
//
|
|
1380
|
-
// `
|
|
1621
|
+
// `fieldLens`/`cachedDerive` are the two getter forms a value class declares.
|
|
1381
1622
|
// The choice between them IS the local declaration of writability at each
|
|
1382
1623
|
// getter (mirroring `: this` invertible method returns). For arbitrary
|
|
1383
1624
|
// cached views, use `lazy()` directly.
|
|
@@ -1385,9 +1626,9 @@ deps, body, opts) {
|
|
|
1385
1626
|
* replaces the composite. Cached per (instance, key). Return type is
|
|
1386
1627
|
* conditional: `Writable<Cls>` on a writable parent, bare `Cls` on RO.
|
|
1387
1628
|
*
|
|
1388
|
-
* get x() { return
|
|
1389
|
-
export function
|
|
1390
|
-
return lazy(parent, key, () =>
|
|
1629
|
+
* get x() { return fieldLens(this, "x", Num); } */
|
|
1630
|
+
export function fieldLens(parent, key, Cls) {
|
|
1631
|
+
return lazy(parent, key, () => fieldOf(parent, key, Cls));
|
|
1391
1632
|
}
|
|
1392
1633
|
/** Read-only derived view via `Cls.derive(parent, fn)`, memoized per
|
|
1393
1634
|
* (instance, key); always bare `Cls` (RO). The cache is the point — the
|