bireactive 0.3.4 → 0.3.5
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/automerge/apply-patches.d.ts +4 -0
- package/dist/automerge/apply-patches.js +62 -0
- package/dist/automerge/doc-cell.js +8 -2
- package/dist/automerge/reconcile.d.ts +9 -8
- package/dist/automerge/reconcile.js +38 -25
- package/dist/core/_counts.d.ts +0 -2
- package/dist/core/_counts.js +0 -1
- package/dist/core/cell.d.ts +44 -87
- package/dist/core/cell.js +195 -301
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/lenses/memory.js +25 -21
- package/dist/core/lenses/point-cloud.js +66 -47
- package/dist/core/lenses/snap.js +8 -7
- package/dist/core/lenses/text.js +8 -6
- package/dist/core/optic.d.ts +0 -5
- package/dist/core/optic.js +16 -28
- package/dist/core/values/audio.js +6 -5
- package/dist/core/values/canvas.js +20 -12
- package/dist/core/values/field.d.ts +2 -0
- package/dist/core/values/field.js +25 -0
- package/dist/core/values/reg.d.ts +2 -3
- package/dist/core/values/reg.js +3 -4
- package/dist/core/values/str.js +6 -6
- package/dist/formats/lens.js +21 -13
- package/dist/jsx-runtime.js +88 -22
- package/dist/learn/lens-net.js +3 -5
- package/dist/schema/lens.d.ts +4 -1
- package/dist/schema/lens.js +14 -7
- package/package.json +1 -1
package/dist/core/cell.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// cell.ts — symmetric bidirectional reactive engine.
|
|
2
2
|
//
|
|
3
3
|
// Forward propagation is alien-signals verbatim. Backward is the same lazy
|
|
4
|
-
// push-pull
|
|
5
|
-
//
|
|
6
|
-
// `
|
|
7
|
-
//
|
|
4
|
+
// push-pull on the transpose of the lens graph, carried by `LensLink` (the dual
|
|
5
|
+
// of `Link`): `parentEdges` down, `childEdges` up. A view write marks the
|
|
6
|
+
// back-path `BF.Pending` and wakes each source's cone; nothing runs until a read
|
|
7
|
+
// pulls. Reads pull only at clean entry points (getter top, source
|
|
8
|
+
// `_update`/`_writeSource`, effect `_run`), never mid-compute.
|
|
8
9
|
//
|
|
9
10
|
// role forward (source → view) backward (view → source)
|
|
10
11
|
// down edge subs (who reads me) parentEdges (my parents)
|
|
@@ -15,16 +16,9 @@
|
|
|
15
16
|
// "dirty" flag F.Dirty (source staged) BF.Dirty (view holds target)
|
|
16
17
|
// "pending" flag F.Pending (on the cone) BF.Pending (on the back-path)
|
|
17
18
|
//
|
|
18
|
-
// Forward flags live on `flags`, backward on a separate `bflags` word
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// source resolves ALL its writers together and commits once). Reads pull only at
|
|
22
|
-
// clean entry points (getter top, source `_update`/`_writeSource`, effect
|
|
23
|
-
// `_run`), never mid-compute. Fan-in is the one non-dual piece: a `merge`
|
|
24
|
-
// accumulates N contributors and folds once, post-order, inside `resolveCone`.
|
|
25
|
-
//
|
|
26
|
-
// Mode table — `getter`/`_bwd` fix forward/writable; the backward shape is read off
|
|
27
|
-
// `_bwd` field presence (`merge` / `stateful` / `parentEdges` / `scatter`):
|
|
19
|
+
// Forward flags live on `flags`, backward on a separate `bflags` word. Fan-in is
|
|
20
|
+
// the one non-dual piece: a `merge` folds N contributors once, post-order, in
|
|
21
|
+
// `resolveCone`. A cell's mode is read off `getter`/`_bwd` field presence:
|
|
28
22
|
// source getter undefined (truth in currentValue)
|
|
29
23
|
// derived getter, no _bwd (read-only derived)
|
|
30
24
|
// lens 1→1 getter + _bwd{ put } (scalar put)
|
|
@@ -74,10 +68,6 @@ let backCycle = 0;
|
|
|
74
68
|
* `writeBack` — so one shared stack suffices (no per-call allocation). */
|
|
75
69
|
const wbNode = [];
|
|
76
70
|
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
71
|
/** `resolveCone`'s pooled post-order frame stack: the node and its next-child
|
|
82
72
|
* cursor. Non-reentrant (no nested `resolveCone`), so shared pools suffice. */
|
|
83
73
|
const rcNode = [];
|
|
@@ -141,18 +131,15 @@ function isWritable(c) {
|
|
|
141
131
|
function isReadOnlyDerived(c) {
|
|
142
132
|
return !isSource(c) && !isWritable(c);
|
|
143
133
|
}
|
|
144
|
-
/**
|
|
145
|
-
*
|
|
146
|
-
* realize once via `.value` (PutGet holds for any source state). */
|
|
134
|
+
/** Value a source-reading `put` linearizes at, with no cascading recompute:
|
|
135
|
+
* the live/last-settled value, realizing once via `.value` if never computed. */
|
|
147
136
|
function backPrimal(c) {
|
|
148
137
|
if (c.getter === undefined || c.flags & F.Dirty)
|
|
149
138
|
return c.value;
|
|
150
139
|
return c.currentValue;
|
|
151
140
|
}
|
|
152
|
-
/** Create a lens-edge `child →[index] parent`,
|
|
153
|
-
*
|
|
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. */
|
|
141
|
+
/** Create a lens-edge `child →[index] parent`, appended to `child.parentEdges`
|
|
142
|
+
* in tuple order. */
|
|
156
143
|
function linkLens(child, parent, index) {
|
|
157
144
|
const e = {
|
|
158
145
|
index,
|
|
@@ -169,9 +156,7 @@ function linkLens(child, parent, index) {
|
|
|
169
156
|
child.parentEdges = e;
|
|
170
157
|
child.parentEdgesTail = e;
|
|
171
158
|
}
|
|
172
|
-
/** Splice a lens-edge into its parent's `childEdges`
|
|
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`. */
|
|
159
|
+
/** Splice a lens-edge into its parent's `childEdges` up-list, once. Idempotent via `linked`. */
|
|
175
160
|
function linkChild(e) {
|
|
176
161
|
if (e.linked)
|
|
177
162
|
return;
|
|
@@ -186,11 +171,8 @@ function linkChild(e) {
|
|
|
186
171
|
parent.childEdges = e;
|
|
187
172
|
parent.childEdgesTail = e;
|
|
188
173
|
}
|
|
189
|
-
/** Remove a lens-edge from its parent's `childEdges` up-list in O(1)
|
|
190
|
-
* re-linkable
|
|
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. */
|
|
174
|
+
/** Remove a lens-edge from its parent's `childEdges` up-list in O(1) and mark it
|
|
175
|
+
* re-linkable. Called on unwatch to release the parent→child retaining edge. */
|
|
194
176
|
function unlinkChild(e) {
|
|
195
177
|
if (COUNTS)
|
|
196
178
|
counts.unlinkChild++;
|
|
@@ -208,10 +190,9 @@ function unlinkChild(e) {
|
|
|
208
190
|
e.nextChild = undefined;
|
|
209
191
|
}
|
|
210
192
|
/** Precompute `BF.WriteBlocked` once, after a writable's `parentEdges` are linked.
|
|
211
|
-
* Mirrors `markDown`'s descent
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* and parents are built first, so each node's bit is its parents' bits + one scan. */
|
|
193
|
+
* Mirrors `markDown`'s descent: a sole read-only-derived parent dead-ends (block);
|
|
194
|
+
* a split routes around a read-only parent; otherwise inherit the block from any
|
|
195
|
+
* non-read-only parent already flagged. */
|
|
215
196
|
function setWriteBlocked(cell) {
|
|
216
197
|
const pe = cell.parentEdges;
|
|
217
198
|
if (pe === undefined)
|
|
@@ -479,50 +460,26 @@ class MergeNode {
|
|
|
479
460
|
this.foldFn = fold;
|
|
480
461
|
}
|
|
481
462
|
}
|
|
482
|
-
// BwdSpec — the backward sidecar, off a single `_bwd` pointer
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
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`.
|
|
463
|
+
// BwdSpec — the backward sidecar, off a single `_bwd` pointer (writable iff
|
|
464
|
+
// `_bwd !== undefined`). The backward shape is read off field presence, not a tag:
|
|
465
|
+
// `merge` ⇒ fan-in fold; `stateful` ⇒ complement-carrying; no `parentEdges` ⇒ pin
|
|
466
|
+
// sink; `scatter` ⇒ tuple `put` (the one bit not recoverable from topology, since a
|
|
467
|
+
// 1-parent split still takes a tuple).
|
|
489
468
|
class BwdSpec {
|
|
490
|
-
/** Lens `put` (dual of `getter`): `put(target)` for 1→1 / multi-out
|
|
491
|
-
*
|
|
492
|
-
* for stateful. `undefined` for a merge (folds) or pin (absorbs). */
|
|
469
|
+
/** Lens `put` (dual of `getter`): `put(target)` for 1→1 / multi-out,
|
|
470
|
+
* `put(target, sources, c)` for stateful. `undefined` for a merge or pin. */
|
|
493
471
|
// biome-ignore lint/suspicious/noExplicitAny: put fn is opaque shape
|
|
494
472
|
put = undefined;
|
|
495
473
|
/** Fold payload; present ⇒ a fan-in merge. */
|
|
496
474
|
merge = undefined;
|
|
497
|
-
/**
|
|
475
|
+
/** The mutable complement object a stateful optic's `get`/`put` thread (and may
|
|
476
|
+
* mutate in place); present ⇒ a complement-carrying (stateful) lens. Seeded once
|
|
477
|
+
* per bind from `optic.complement`, never reassigned. */
|
|
498
478
|
stateful = undefined;
|
|
499
479
|
/** `put` yields a per-parent tuple (split / stateful) vs a scalar (1→1). The
|
|
500
480
|
* only discriminant not derivable from topology (a 1-parent split is a tuple). */
|
|
501
481
|
scatter = false;
|
|
502
482
|
}
|
|
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. */
|
|
506
|
-
class StatefulCore {
|
|
507
|
-
/** Engine-owned memory the view discards. */
|
|
508
|
-
complement;
|
|
509
|
-
/** Advance the complement: `step(sources, complement)`. Run only when the
|
|
510
|
-
* sources actually moved (the engine gates it; see the stateful header). */
|
|
511
|
-
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
512
|
-
step;
|
|
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;
|
|
519
|
-
constructor(complement,
|
|
520
|
-
// biome-ignore lint/suspicious/noExplicitAny: opaque step shape
|
|
521
|
-
step) {
|
|
522
|
-
this.complement = complement;
|
|
523
|
-
this.step = step;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
483
|
// ─────────────────────────────────────────────────────────────────────────
|
|
527
484
|
// Public API — sentinels, read/write shapes, and value-coercion helpers.
|
|
528
485
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -602,10 +559,6 @@ export class Cell {
|
|
|
602
559
|
/** @internal Visit epoch for `backResolve`'s collect phase (dedups diamonds
|
|
603
560
|
* without a Set; compared against the global `backCycle`). */
|
|
604
561
|
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
562
|
/** Optional debug label (`cell(0, { name })`); used by errors and graph dumps. */
|
|
610
563
|
name;
|
|
611
564
|
// Every slot assigned once, in declaration order, for a stable V8 hidden class.
|
|
@@ -628,7 +581,6 @@ export class Cell {
|
|
|
628
581
|
this.childEdgesTail = undefined;
|
|
629
582
|
this.bflags = BF.None;
|
|
630
583
|
this.bEpoch = 0;
|
|
631
|
-
this.version = 0;
|
|
632
584
|
this.name = undefined;
|
|
633
585
|
if (opts !== undefined) {
|
|
634
586
|
if (opts.equals !== undefined)
|
|
@@ -649,15 +601,13 @@ export class Cell {
|
|
|
649
601
|
const prev = this.pendingValue;
|
|
650
602
|
this.pendingValue = next;
|
|
651
603
|
if (!this._equals(prev, next)) {
|
|
652
|
-
this.version++; // stamp the change for stateful-lens provenance (sum-of-versions)
|
|
653
604
|
this.flags = F.Mutable | F.Dirty;
|
|
654
605
|
if (writeHook !== undefined)
|
|
655
606
|
writeHook(this);
|
|
656
607
|
const subs = this.subs;
|
|
657
608
|
if (subs !== undefined) {
|
|
658
609
|
// Convert the cone's arm-time `Pending` into `Dirty` so a second observer
|
|
659
|
-
// (not just the first reader) sees the change
|
|
660
|
-
// freshly-`Dirty` nodes are honored by `checkDirty`'s unwind.
|
|
610
|
+
// (not just the first reader) sees the change; honored mid-pull by `checkDirty`.
|
|
661
611
|
propagate(subs, runDepth > 0, activeExcluded);
|
|
662
612
|
autoFlush();
|
|
663
613
|
}
|
|
@@ -678,10 +628,7 @@ export class Cell {
|
|
|
678
628
|
const old = this.currentValue;
|
|
679
629
|
const next = (this.currentValue = this.getter());
|
|
680
630
|
threw = false;
|
|
681
|
-
|
|
682
|
-
if (changed)
|
|
683
|
-
this.version++; // derived commit: stamp for stateful provenance
|
|
684
|
-
return changed;
|
|
631
|
+
return !this._equals(old, next);
|
|
685
632
|
}
|
|
686
633
|
finally {
|
|
687
634
|
activeSub = prev;
|
|
@@ -702,11 +649,9 @@ export class Cell {
|
|
|
702
649
|
_notify() { }
|
|
703
650
|
/** @internal */
|
|
704
651
|
_unwatched() {
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
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.
|
|
652
|
+
// Release each parent→child retaining edge (the `childEdges` up-list) so a
|
|
653
|
+
// disposed view isn't pinned by a long-lived source; a later arm re-links via
|
|
654
|
+
// `markDown`. Skip a still back-marked view — its pending write needs the edge.
|
|
710
655
|
if (!(this.bflags & BACK_MARKED)) {
|
|
711
656
|
for (let e = this.parentEdges; e !== undefined; e = e.nextParent) {
|
|
712
657
|
if (e.linked)
|
|
@@ -731,29 +676,18 @@ export class Cell {
|
|
|
731
676
|
activeSub = prev;
|
|
732
677
|
}
|
|
733
678
|
}
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
return buildLens(
|
|
679
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch over fwd/bwd vs optic chain
|
|
680
|
+
lens(...args) {
|
|
681
|
+
if (typeof args[0] === "function") {
|
|
682
|
+
return buildLens(this.constructor, [this, ...args]);
|
|
683
|
+
}
|
|
684
|
+
return buildLens(CELL_CTOR, [this, ...args]);
|
|
740
685
|
}
|
|
741
686
|
/** Read-only same-type view: the RO dual of the endo `.lens`. For a cross-type view use the typed static
|
|
742
687
|
* `Target.derive(src, fn)`. */
|
|
743
688
|
derive(fn) {
|
|
744
689
|
return buildDerived(this.constructor, () => fn(this.value));
|
|
745
690
|
}
|
|
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
691
|
/** Backward fan-in: forwards its parent's value unchanged; on write, folds N
|
|
758
692
|
* contributors into one value. `fold` defaults to last-writer-wins. */
|
|
759
693
|
merge(fold) {
|
|
@@ -836,8 +770,7 @@ function buildDerived(Cls, getter) {
|
|
|
836
770
|
return cell;
|
|
837
771
|
}
|
|
838
772
|
// Shared N-input read getter: refill a construction-owned buffer from the parents
|
|
839
|
-
// each read (no per-read alloc), then apply `fwd`.
|
|
840
|
-
// the node is a read-only derive-N or a writable split lens.
|
|
773
|
+
// each read (no per-read alloc), then apply `fwd`.
|
|
841
774
|
function arrayGetter(parents, fwd) {
|
|
842
775
|
const n = parents.length;
|
|
843
776
|
const vals = new Array(n);
|
|
@@ -847,89 +780,119 @@ function arrayGetter(parents, fwd) {
|
|
|
847
780
|
return fwd(vals);
|
|
848
781
|
};
|
|
849
782
|
}
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
// biome-ignore lint/suspicious/noExplicitAny: dispatch over the untyped call forms
|
|
857
|
-
function buildLens(Cls, args) {
|
|
858
|
-
const parent0 = args[0];
|
|
859
|
-
if (args.length === 2) {
|
|
860
|
-
return Array.isArray(parent0)
|
|
861
|
-
? buildStateful(Cls, parent0, args[1])
|
|
862
|
-
: buildStateful1(Cls, parent0, args[1]);
|
|
863
|
-
}
|
|
864
|
-
let parent = parent0;
|
|
865
|
-
let fwd = args[1];
|
|
866
|
-
let bwd = args[2];
|
|
867
|
-
// Object-keyed parents → rewrite to the positional array form (key order
|
|
868
|
-
// fixed once; omitted backward keys become SKIP). The tuple fast path below
|
|
869
|
-
// is untouched.
|
|
870
|
-
if (parent0 !== null &&
|
|
871
|
-
typeof parent0 === "object" &&
|
|
872
|
-
!Array.isArray(parent0) &&
|
|
873
|
-
!(parent0 instanceof Cell)) {
|
|
874
|
-
const keys = Object.keys(parent0);
|
|
875
|
-
const rec = parent0;
|
|
876
|
-
const fwdObj = fwd;
|
|
877
|
-
const bwdObj = bwd;
|
|
878
|
-
const toObj = (vals) => {
|
|
879
|
-
const o = {};
|
|
880
|
-
for (let i = 0; i < keys.length; i++)
|
|
881
|
-
o[keys[i]] = vals[i];
|
|
882
|
-
return o;
|
|
883
|
-
};
|
|
884
|
-
parent = keys.map(k => rec[k]);
|
|
885
|
-
fwd = (vals) => fwdObj(toObj(vals));
|
|
886
|
-
bwd = ((t, vals) => {
|
|
887
|
-
const o = bwdObj(t, toObj(vals));
|
|
888
|
-
return keys.map(k => (k in o ? o[k] : SKIP));
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
const readsSource = bwd.length >= 2;
|
|
783
|
+
// Bind a pure (complement-free) optic to one source. Source-reading vs iso is the
|
|
784
|
+
// `put` arity. The 1→1 getter/put stays scalar.
|
|
785
|
+
function bindPureScalar(Cls, p, optic) {
|
|
786
|
+
const get = optic.get;
|
|
787
|
+
const put = optic.put;
|
|
788
|
+
const readsSource = put.length >= 2;
|
|
892
789
|
const cell = new Cls();
|
|
893
790
|
cell.flags = F.Mutable | F.Dirty;
|
|
894
791
|
const b = (cell._bwd = new BwdSpec());
|
|
792
|
+
cell.getter = (() => get(p.value));
|
|
793
|
+
// Source-reading lenses linearize at the parent's primal (`backPrimal`), so the
|
|
794
|
+
// engine always calls the 1-arg form and never recomputes the parent's cone.
|
|
795
|
+
b.put = readsSource ? (t) => put(t, backPrimal(p)) : put;
|
|
796
|
+
linkLens(cell, p, 0);
|
|
797
|
+
setWriteBlocked(cell);
|
|
798
|
+
return cell;
|
|
799
|
+
}
|
|
800
|
+
function bindPureTuple(Cls, parents, optic) {
|
|
801
|
+
const get = optic.get;
|
|
802
|
+
const put = optic.put;
|
|
803
|
+
const readsSource = put.length >= 2;
|
|
804
|
+
const n = parents.length;
|
|
805
|
+
const cell = new Cls();
|
|
806
|
+
cell.flags = F.Mutable | F.Dirty;
|
|
807
|
+
const b = (cell._bwd = new BwdSpec());
|
|
808
|
+
cell.getter = arrayGetter(parents, get);
|
|
809
|
+
b.scatter = true;
|
|
810
|
+
for (let i = 0; i < n; i++)
|
|
811
|
+
linkLens(cell, parents[i], i);
|
|
812
|
+
if (readsSource) {
|
|
813
|
+
// Own reused buffer (not the getter's) to avoid aliasing; `put` consumes it
|
|
814
|
+
// synchronously and must not retain it.
|
|
815
|
+
const argbuf = new Array(n);
|
|
816
|
+
const putN = put;
|
|
817
|
+
b.put = (target) => {
|
|
818
|
+
for (let i = 0; i < n; i++)
|
|
819
|
+
argbuf[i] = backPrimal(parents[i]);
|
|
820
|
+
return putN(target, argbuf);
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const put0 = put;
|
|
825
|
+
b.put = (target) => put0(target);
|
|
826
|
+
}
|
|
827
|
+
setWriteBlocked(cell);
|
|
828
|
+
return cell;
|
|
829
|
+
}
|
|
830
|
+
// Bind one optic to a source (cell or array). The four shapes (scalar/tuple ×
|
|
831
|
+
// pure/stateful) fall out of `Array.isArray(parent)` and the `complement` discriminant.
|
|
832
|
+
function bindOne(Cls, parent, optic) {
|
|
895
833
|
if (Array.isArray(parent)) {
|
|
896
834
|
const parents = parent;
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
835
|
+
return optic.complement !== undefined
|
|
836
|
+
? buildStateful(Cls, parents, optic)
|
|
837
|
+
: bindPureTuple(Cls, parents, optic);
|
|
838
|
+
}
|
|
839
|
+
const p = parent;
|
|
840
|
+
return optic.complement !== undefined
|
|
841
|
+
? buildStateful1(Cls, p, optic)
|
|
842
|
+
: bindPureScalar(Cls, p, optic);
|
|
843
|
+
}
|
|
844
|
+
// One writable-lens constructor over all call forms. `(parent, fwd, bwd)` is sugar
|
|
845
|
+
// for an inline pure optic; `(parent, ...optics)` folds an optic chain by repeated
|
|
846
|
+
// binding (each optic bound to the prior result; only the last stage takes `Cls`).
|
|
847
|
+
// Composition is just re-binding, so `cell.ts` needs no optic.ts import.
|
|
848
|
+
// biome-ignore lint/suspicious/noExplicitAny: dispatch over the untyped call forms
|
|
849
|
+
function buildLens(Cls, args) {
|
|
850
|
+
const parent0 = args[0];
|
|
851
|
+
const cls = Cls;
|
|
852
|
+
if (typeof args[1] === "function") {
|
|
853
|
+
// `(parent, fwd, bwd)` → an inline pure optic, then bind.
|
|
854
|
+
let parent = parent0;
|
|
855
|
+
let get = args[1];
|
|
856
|
+
let put = args[2];
|
|
857
|
+
// Object-keyed parents → rewrite to the positional array form (key order fixed
|
|
858
|
+
// once; omitted backward keys become SKIP).
|
|
859
|
+
if (parent0 !== null &&
|
|
860
|
+
typeof parent0 === "object" &&
|
|
861
|
+
!Array.isArray(parent0) &&
|
|
862
|
+
!(parent0 instanceof Cell)) {
|
|
863
|
+
const keys = Object.keys(parent0);
|
|
864
|
+
const rec = parent0;
|
|
865
|
+
const getObj = get;
|
|
866
|
+
const putObj = put;
|
|
867
|
+
const toObj = (vals) => {
|
|
868
|
+
const o = {};
|
|
869
|
+
for (let i = 0; i < keys.length; i++)
|
|
870
|
+
o[keys[i]] = vals[i];
|
|
871
|
+
return o;
|
|
911
872
|
};
|
|
873
|
+
parent = keys.map(k => rec[k]);
|
|
874
|
+
get = ((vals) => getObj(toObj(vals)));
|
|
875
|
+
put = ((t, vals) => {
|
|
876
|
+
const o = putObj(t, toObj(vals));
|
|
877
|
+
return keys.map(k => (k in o ? o[k] : SKIP));
|
|
878
|
+
});
|
|
912
879
|
}
|
|
913
|
-
|
|
914
|
-
const bwd0 = bwd;
|
|
915
|
-
b.put = (target) => bwd0(target);
|
|
916
|
-
}
|
|
880
|
+
return bindOne(cls, parent, { get, put });
|
|
917
881
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
b.put = readsSource ? (t) => bwd(t, backPrimal(p)) : bwd;
|
|
924
|
-
linkLens(cell, p, 0);
|
|
882
|
+
// Optic value(s): fold the chain by repeated binding.
|
|
883
|
+
let acc = parent0;
|
|
884
|
+
const last = args.length - 1;
|
|
885
|
+
for (let i = 1; i <= last; i++) {
|
|
886
|
+
acc = bindOne(i === last ? cls : CELL_CTOR, acc, args[i]);
|
|
925
887
|
}
|
|
926
|
-
|
|
927
|
-
|
|
888
|
+
return acc;
|
|
889
|
+
}
|
|
890
|
+
// Seed the complement object from the current sources (fresh per bind).
|
|
891
|
+
function seedComplement(optic, seed) {
|
|
892
|
+
return optic.complement(seed);
|
|
928
893
|
}
|
|
929
894
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
930
|
-
function buildStateful(Cls, parents,
|
|
931
|
-
// biome-ignore lint/suspicious/noExplicitAny: opaque spec
|
|
932
|
-
spec) {
|
|
895
|
+
function buildStateful(Cls, parents, optic) {
|
|
933
896
|
const n = parents.length;
|
|
934
897
|
const vals = new Array(n);
|
|
935
898
|
const cell = new Cls();
|
|
@@ -938,74 +901,40 @@ spec) {
|
|
|
938
901
|
const seed = new Array(n);
|
|
939
902
|
for (let i = 0; i < n; i++)
|
|
940
903
|
seed[i] = parents[i].peek();
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const step = (spec.step ?? init);
|
|
945
|
-
const sc = (b.stateful = new StatefulCore(spec.init(seed), step));
|
|
946
|
-
// Sentinel: version sums are ≥ 0, so the first read (or back-write) always
|
|
947
|
-
// steps once, folding the initial sources into the seed complement. The
|
|
948
|
-
// `init`/`step` split is seed-then-fold: `init` need not see the sources.
|
|
949
|
-
sc.stamp = -1;
|
|
950
|
-
const fwd = spec.fwd;
|
|
951
|
-
b.put = spec.bwd;
|
|
904
|
+
const c = (b.stateful = seedComplement(optic, seed));
|
|
905
|
+
const get = optic.get;
|
|
906
|
+
b.put = optic.put;
|
|
952
907
|
b.scatter = true;
|
|
953
908
|
for (let i = 0; i < n; i++)
|
|
954
909
|
linkLens(cell, parents[i], i);
|
|
910
|
+
// Forward-only refresh: `get` reads the sources and (idempotently) updates the
|
|
911
|
+
// complement; the cache runs it once per source-version change.
|
|
955
912
|
cell.getter = (() => {
|
|
956
|
-
let
|
|
957
|
-
for (let i = 0; i < n; i++) {
|
|
913
|
+
for (let i = 0; i < n; i++)
|
|
958
914
|
vals[i] = parents[i].value;
|
|
959
|
-
|
|
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;
|
|
967
|
-
}
|
|
968
|
-
return fwd(vals, sc.complement);
|
|
915
|
+
return get(vals, c);
|
|
969
916
|
});
|
|
970
917
|
setWriteBlocked(cell);
|
|
971
918
|
return cell;
|
|
972
919
|
}
|
|
973
920
|
// Single-source stateful fast-path: one parent, so no `vals` buffer and a scalar
|
|
974
|
-
// `
|
|
975
|
-
// provenance and laziness as the N-source `buildStateful`, minus the array work.
|
|
921
|
+
// `get`/`put` — minus the array work of the N-source `buildStateful`.
|
|
976
922
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
977
|
-
function buildStateful1(Cls, parent,
|
|
978
|
-
// biome-ignore lint/suspicious/noExplicitAny: opaque spec
|
|
979
|
-
spec) {
|
|
923
|
+
function buildStateful1(Cls, parent, optic) {
|
|
980
924
|
const cell = new Cls();
|
|
981
925
|
cell.flags = F.Mutable | F.Dirty;
|
|
982
926
|
const b = (cell._bwd = new BwdSpec());
|
|
983
|
-
const
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
sc.stamp = -1; // sentinel: first use folds the source in (see `buildStateful`)
|
|
987
|
-
const fwd = spec.fwd;
|
|
988
|
-
b.put = spec.bwd;
|
|
927
|
+
const c = (b.stateful = seedComplement(optic, parent.peek()));
|
|
928
|
+
const get = optic.get;
|
|
929
|
+
b.put = optic.put;
|
|
989
930
|
// `scatter` stays false: writeBack routes this through the scalar stateful branch.
|
|
990
931
|
linkLens(cell, parent, 0);
|
|
991
|
-
cell.getter = (() =>
|
|
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);
|
|
1001
|
-
});
|
|
932
|
+
cell.getter = (() => get(parent.value, c));
|
|
1002
933
|
setWriteBlocked(cell);
|
|
1003
934
|
return cell;
|
|
1004
935
|
}
|
|
1005
936
|
// 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)`)
|
|
1007
|
-
// `buildDerived` with the matching getter. (Writable `lens(...)` is `buildLens`;
|
|
1008
|
-
// statics pass the typed subclass, free functions plain `Cell`, so neither drifts.)
|
|
937
|
+
// read (`derive(p, fn)`), or an N-parent read (`derive(ps, fn)`).
|
|
1009
938
|
// biome-ignore lint/suspicious/noExplicitAny: dispatch over the untyped call forms
|
|
1010
939
|
function buildDerive(Cls, args) {
|
|
1011
940
|
if (args.length === 1)
|
|
@@ -1016,8 +945,7 @@ function buildDerive(Cls, args) {
|
|
|
1016
945
|
return buildDerived(Cls, arrayGetter(parent, fn));
|
|
1017
946
|
return buildDerived(Cls, () => fn(parent.value));
|
|
1018
947
|
}
|
|
1019
|
-
//
|
|
1020
|
-
// the field-only class shape for a stable hidden class.
|
|
948
|
+
// Prototype accessor (not a class accessor): V8 JITs it better, keeps a stable hidden class.
|
|
1021
949
|
Object.defineProperty(Cell.prototype, "value", {
|
|
1022
950
|
get() {
|
|
1023
951
|
// Reading is the PULL: a back-marked cell resolves here, before its own
|
|
@@ -1067,8 +995,8 @@ Object.defineProperty(Cell.prototype, "value", {
|
|
|
1067
995
|
throw new TypeError("Cannot write to a computed");
|
|
1068
996
|
}
|
|
1069
997
|
// GetPut for a multi-parent split: absorb a write equal to the current view
|
|
1070
|
-
// (its `put` could redistribute sources
|
|
1071
|
-
//
|
|
998
|
+
// (its `put` could redistribute sources). Stateful excluded — peeking would run
|
|
999
|
+
// `get` and mutate its complement.
|
|
1072
1000
|
if (b.scatter && b.stateful === undefined && this._equals(next, this.peek())) {
|
|
1073
1001
|
return;
|
|
1074
1002
|
}
|
|
@@ -1154,19 +1082,15 @@ function markDown(start) {
|
|
|
1154
1082
|
}
|
|
1155
1083
|
}
|
|
1156
1084
|
}
|
|
1157
|
-
/** RESOLVE (pull), dual of `checkDirty`: resolve one node's whole back-cone
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* call on it resolves every co-writer together and commits once.
|
|
1085
|
+
/** RESOLVE (pull), dual of `checkDirty`: resolve one node's whole back-cone by
|
|
1086
|
+
* ascending `childEdges` to the armed views and `writeBack`ing each. Source-centric
|
|
1087
|
+
* — a call on a source resolves every co-writer together and commits once.
|
|
1161
1088
|
*
|
|
1162
|
-
* Iterative post-order
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
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. */
|
|
1089
|
+
* Iterative post-order via an explicit {node, next-child cursor} frame stack:
|
|
1090
|
+
* pre-order clears a merge's buffer and drives an armed target (`enterCone`);
|
|
1091
|
+
* post-order clears `BF.Pending` and folds a merge at its true position. Children
|
|
1092
|
+
* walk in `childEdges` order (co-writer last-write-wins); `bEpoch` dedups diamonds.
|
|
1093
|
+
* Idempotent, so `backResolve`'s phase 2 can call it unconditionally. */
|
|
1170
1094
|
function resolveCone(root) {
|
|
1171
1095
|
const epoch = ++backCycle;
|
|
1172
1096
|
root.bEpoch = epoch;
|
|
@@ -1300,7 +1224,6 @@ function writeBack(node, target) {
|
|
|
1300
1224
|
wbNode[0] = node;
|
|
1301
1225
|
wbTarget[0] = target;
|
|
1302
1226
|
let top = 1;
|
|
1303
|
-
let sTop = 0;
|
|
1304
1227
|
while (top > 0) {
|
|
1305
1228
|
if (COUNTS)
|
|
1306
1229
|
counts.writeBackVisit++;
|
|
@@ -1308,13 +1231,10 @@ function writeBack(node, target) {
|
|
|
1308
1231
|
const tgt = wbTarget[top];
|
|
1309
1232
|
if (isSource(cur)) {
|
|
1310
1233
|
cur._writeSource(tgt); // staged now, visible to later siblings
|
|
1311
|
-
// Clear
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
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.)
|
|
1234
|
+
// Clear `BF.Pending`, then re-assert iff a lens-child is still armed (an
|
|
1235
|
+
// overlapping co-writer); else leaving it set would strand `BF.Pending` on
|
|
1236
|
+
// every fan-in source. Scan from the tail (where `resolveCone` leaves the last
|
|
1237
|
+
// still-armed co-writer), turning a fan-in re-assert from O(N²) into O(N).
|
|
1318
1238
|
cur.bflags &= ~BF.Pending;
|
|
1319
1239
|
for (let e = cur.childEdgesTail; e !== undefined; e = e.prevChild) {
|
|
1320
1240
|
if (COUNTS)
|
|
@@ -1343,18 +1263,11 @@ function writeBack(node, target) {
|
|
|
1343
1263
|
// Single-source stateful fast-path (scalar `bwd`); one index-0 parent edge.
|
|
1344
1264
|
const p = pe.parent;
|
|
1345
1265
|
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
1266
|
if (COUNTS)
|
|
1353
1267
|
counts.put++;
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
const u = res.update;
|
|
1268
|
+
// `put` sees the last-read complement (+ fresh source) and may mutate it;
|
|
1269
|
+
// it returns just the scalar update (or SKIP).
|
|
1270
|
+
const u = b.put(tgt, x, sc);
|
|
1358
1271
|
if (u !== SKIP) {
|
|
1359
1272
|
wbNode[top] = p;
|
|
1360
1273
|
wbTarget[top] = u;
|
|
@@ -1380,30 +1293,13 @@ function writeBack(node, target) {
|
|
|
1380
1293
|
let out;
|
|
1381
1294
|
if (sc !== undefined) {
|
|
1382
1295
|
const vals = new Array(n);
|
|
1383
|
-
let
|
|
1384
|
-
for (let i = 0; i < n; i++) {
|
|
1296
|
+
for (let i = 0; i < n; i++)
|
|
1385
1297
|
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
1298
|
if (COUNTS)
|
|
1397
1299
|
counts.put++;
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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;
|
|
1300
|
+
// `put` sees the last-read complement (+ fresh sources) and may mutate it;
|
|
1301
|
+
// it reconstructs any current-source facts it needs from `vals`.
|
|
1302
|
+
out = b.put(tgt, vals, sc);
|
|
1407
1303
|
}
|
|
1408
1304
|
else {
|
|
1409
1305
|
if (COUNTS)
|
|
@@ -1434,24 +1330,24 @@ function writeBack(node, target) {
|
|
|
1434
1330
|
}
|
|
1435
1331
|
continue;
|
|
1436
1332
|
}
|
|
1437
|
-
// 1→1 lens (single index-0 parent-edge).
|
|
1333
|
+
// 1→1 lens (single index-0 parent-edge). A `SKIP` rejects the write: the
|
|
1334
|
+
// source is left, so invalidate this view and propagate (it recomputes to old).
|
|
1438
1335
|
if (COUNTS)
|
|
1439
1336
|
counts.put++;
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1337
|
+
{
|
|
1338
|
+
const u = b.put(tgt);
|
|
1339
|
+
if (u !== SKIP) {
|
|
1340
|
+
wbNode[top] = pe.parent;
|
|
1341
|
+
wbTarget[top] = u;
|
|
1342
|
+
top++;
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
cur.flags |= F.Dirty;
|
|
1346
|
+
const subs = cur.subs;
|
|
1347
|
+
if (subs !== undefined)
|
|
1348
|
+
propagate(subs, runDepth > 0, activeExcluded);
|
|
1349
|
+
}
|
|
1452
1350
|
}
|
|
1453
|
-
sc.stamp = ver;
|
|
1454
|
-
wbStateful[i] = undefined;
|
|
1455
1351
|
}
|
|
1456
1352
|
}
|
|
1457
1353
|
/** Fold a merge's contributions once (policy; default last-writer-wins) and write
|
|
@@ -1627,10 +1523,8 @@ function flush() {
|
|
|
1627
1523
|
return;
|
|
1628
1524
|
flushing = true;
|
|
1629
1525
|
// Error locality: one effect throwing must not strand its siblings. Drain the
|
|
1630
|
-
// whole queue, catching each body
|
|
1631
|
-
//
|
|
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.
|
|
1526
|
+
// whole queue, catching each body, and surface the first error after it empties
|
|
1527
|
+
// (later errors dropped). A throwing body isn't re-queued; it re-arms on next wake.
|
|
1634
1528
|
let err;
|
|
1635
1529
|
let threw = false;
|
|
1636
1530
|
try {
|