bireactive 0.3.3 → 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/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 run on the transpose of the lens graph, carried by one `LensLink`
5
- // structure (the backward dual of forward's `Link`): `parentEdges` down,
6
- // `childEdges` up, created eagerly at lens construction. One traversal each
7
- // direction:
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, so the
19
- // two never share a bit. A view write marks the back-path `BF.Pending` and wakes
20
- // each source's forward cone; nothing runs until a read pulls (source-centric: a
21
- // source resolves ALL its writers together and commits once). Reads pull only at
22
- // clean entry points (getter top, source `_update`/`_writeSource`, effect
23
- // `_run`), never mid-compute. Fan-in is the one non-dual piece: a `merge`
24
- // accumulates N contributors and folds once, post-order, inside `resolveCone`.
25
- //
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
- /** Forward primal a source-reading `bwd` linearizes at, without a cascading
145
- * recompute: live/last-settled value for a source or realized derived, else
146
- * realize once via `.value` (PutGet holds for any source state). */
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`, appending it to `child`'s
153
- * `parentEdges` (down) eagerly at construction, in tuple order (so
154
- * `parentEdges` is index-ordered). The up-list (`parent.childEdges`) is spliced
155
- * lazily on first back-mark (`linkChild`), so child order is arm-order. */
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` (the up-traversal list),
173
- * once, on first back-mark — so a parent's child order is arm-order and
174
- * co-writer resolution is last-write-wins. Idempotent via `linked`. */
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), and mark it
190
- * re-linkable the backward dual of `unlink` dropping a subscriber from `subs`.
191
- * Called when a view is unwatched, to release the parent→child retaining edge (a
192
- * later arm re-`linkChild`s). The child's own down-list (`parentEdges`) stays:
193
- * it's intrinsic to the view and dies with it. */
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 exactly: a sole read-only-derived parent dead-ends
212
- * (block); a split routes around a read-only parent; otherwise the block is
213
- * inherited from any non-read-only parent already flagged. Topology is immutable
214
- * and parents are built first, so each node's bit is its parents' bits + one scan. */
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 so a source/computed
483
- // stays lean. Only a writable derived cell (lens / multi-out / merge / stateful /
484
- // pin) carries one; writable iff `_bwd !== undefined`. The backward shape is read
485
- // off field presence rather than a tag: `merge` set fan-in fold; `stateful` set ⇒
486
- // complement-carrying; no `parentEdges` pin sink; `scatter` ⇒ tuple `put`. The
487
- // one bit that isn't recoverable from topology is scalar-vs-tuple `put` (a 1-parent
488
- // split still takes a tuple), hence `scatter`.
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 (a
491
- * source-reading lens reads its parents at walk time), `put(target, sources, c)`
492
- * for stateful. `undefined` for a merge (folds) or pin (absorbs). */
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
- /** Complement state; present a complement-carrying (stateful) lens. */
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. If this lands mid-pull, the
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
- const changed = !this._equals(old, next);
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
- // Backward dual of `unlink` clearing us from each parent's `subs`: release
706
- // the parent→child retaining edge (the `childEdges` up-list) so a disposed
707
- // view stops being pinned by a long-lived source. Our own down-list
708
- // (`parentEdges`) stays — a later arm re-links via `markDown`. Skip a still
709
- // back-marked view (a pending write needs its edge); rare and bounded.
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
- // Construction helpers build via `new this()` so a subclass static
735
- // (`Vec.lens(...)`) yields a `Vec` with its constructor-set equality.
736
- /** Endomorphic lens. A 2-arg `bwd(view, current)` consults the current
737
- * source; a 1-arg `bwd(view)` reconstructs it from the view alone. */
738
- lens(fwd, bwd) {
739
- return buildLens(this.constructor, [this, fwd, bwd]);
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`. Identical hot closure whether
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
- // One writable-lens constructor for both shapes. The `Array.isArray` branch is paid
851
- // once at construction and installs the matching specialized hot closures — scalar
852
- // scalar `getter`/`put` for 1→1, the buffer-loop getter + tuple `put` (`scatter`)
853
- // for N→M — so neither hot path changes. A 2-arg call is the
854
- // complement-carrying form and routes to `buildStateful`. `bwd` is always present
855
- // (a read-only N view is a `derive`, built via `buildDerive`).
856
- // biome-ignore lint/suspicious/noExplicitAny: dispatch over the untyped call forms
857
- function buildLens(Cls, args) {
858
- const parent0 = args[0];
859
- if (args.length === 2) {
860
- return Array.isArray(parent0)
861
- ? buildStateful(Cls, parent0, args[1])
862
- : buildStateful1(Cls, parent0, args[1]);
863
- }
864
- let parent = parent0;
865
- let fwd = args[1];
866
- let bwd = args[2];
867
- // Object-keyed parents → rewrite to the positional array form (key order
868
- // fixed once; omitted backward keys become SKIP). The tuple fast path below
869
- // is untouched.
870
- if (parent0 !== null &&
871
- typeof parent0 === "object" &&
872
- !Array.isArray(parent0) &&
873
- !(parent0 instanceof Cell)) {
874
- const keys = Object.keys(parent0);
875
- const rec = parent0;
876
- const fwdObj = fwd;
877
- const bwdObj = bwd;
878
- const toObj = (vals) => {
879
- const o = {};
880
- for (let i = 0; i < keys.length; i++)
881
- o[keys[i]] = vals[i];
882
- return o;
883
- };
884
- parent = keys.map(k => rec[k]);
885
- fwd = (vals) => fwdObj(toObj(vals));
886
- bwd = ((t, vals) => {
887
- const o = bwdObj(t, toObj(vals));
888
- return keys.map(k => (k in o ? o[k] : SKIP));
889
- });
890
- }
891
- const readsSource = bwd.length >= 2;
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
- const n = parents.length;
898
- cell.getter = arrayGetter(parents, fwd);
899
- b.scatter = true;
900
- for (let i = 0; i < n; i++)
901
- linkLens(cell, parents[i], i);
902
- if (readsSource) {
903
- // Own reused buffer (not the getter's) to avoid aliasing; `bwd` consumes it
904
- // synchronously and must not retain it.
905
- const argbuf = new Array(n);
906
- const bwdN = bwd;
907
- b.put = (target) => {
908
- for (let i = 0; i < n; i++)
909
- argbuf[i] = backPrimal(parents[i]);
910
- return bwdN(target, argbuf);
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
- else {
914
- const bwd0 = bwd;
915
- b.put = (target) => bwd0(target);
916
- }
880
+ return bindOne(cls, parent, { get, put });
917
881
  }
918
- else {
919
- const p = parent;
920
- cell.getter = (() => fwd(p.value));
921
- // Source-reading lenses linearize at the parent's primal (`backPrimal`), so the
922
- // engine always calls the 1-arg form and never recomputes the parent's cone.
923
- b.put = readsSource ? (t) => bwd(t, backPrimal(p)) : bwd;
924
- linkLens(cell, p, 0);
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
- setWriteBlocked(cell);
927
- return cell;
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
- // Default `step` is the memoryless refresh (`init`); the engine runs it only on
942
- // an outside change, so the `external ? init(s) : c` idiom needs no user `step`.
943
- const init = spec.init;
944
- const step = (spec.step ?? init);
945
- const sc = (b.stateful = new StatefulCore(spec.init(seed), step));
946
- // Sentinel: version sums are ≥ 0, so the first read (or back-write) always
947
- // steps once, folding the initial sources into the seed complement. The
948
- // `init`/`step` split is seed-then-fold: `init` need not see the sources.
949
- sc.stamp = -1;
950
- const fwd = spec.fwd;
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 ver = 0;
957
- for (let i = 0; i < n; i++) {
913
+ for (let i = 0; i < n; i++)
958
914
  vals[i] = parents[i].value;
959
- ver += parents[i].version;
960
- }
961
- // Step only when sources moved since the last sync (lazy own-vs-external).
962
- if (ver !== sc.stamp) {
963
- if (COUNTS)
964
- counts.step++;
965
- sc.complement = sc.step(vals, sc.complement);
966
- sc.stamp = ver;
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
- // `step`/`fwd`/`bwd` — the version stamp is just the parent's `version`. Same
975
- // provenance and laziness as the N-source `buildStateful`, minus the array work.
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 init = spec.init;
984
- const step = (spec.step ?? init);
985
- const sc = (b.stateful = new StatefulCore(init(parent.peek()), step));
986
- sc.stamp = -1; // sentinel: first use folds the source in (see `buildStateful`)
987
- const fwd = spec.fwd;
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)`) — each lands in
1007
- // `buildDerived` with the matching getter. (Writable `lens(...)` is `buildLens`;
1008
- // statics pass the typed subclass, free functions plain `Cell`, so neither drifts.)
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
- // Installed on the prototype (not a class accessor): V8 JITs it better and keeps
1020
- // the field-only class shape for a stable hidden class.
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 past per-source equality). Stateful
1071
- // excluded (`scatter` but `stateful` set) — peeking would step its complement.
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
- * Ascend `childEdges` (only `BACK_MARKED` children) to the armed views,
1159
- * `writeBack`ing each. Source-centric a source reflects all its writers, so a
1160
- * call on it resolves every co-writer together and commits once.
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 over the back-cone, via an explicit frame stack of
1163
- * {node, next-child cursor}. On entering a
1164
- * node (pre): clear a merge's contributions, then `writeBack` if it holds an armed
1165
- * target. After its children drain (post): clear `BF.Pending`, then fold a merge.
1166
- * Children are walked in forward `childEdges` order (so a co-writer's last write
1167
- * wins) and a per-call `bEpoch` dedups diamonds — the merge fold lands at its true
1168
- * post-order position, interleaved with sibling writes, not deferred.
1169
- * Idempotent, so phase-2 of `backResolve` can call it unconditionally. */
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 this source's `BF.Pending`, then re-assert iff a lens-child is STILL
1312
- // armed (an overlapping co-writer) else that write is lost, and leaving it
1313
- // set unconditionally would strand `BF.Pending` on every fan-in source.
1314
- // Scan from the TAIL: `resolveCone` drives children head→tail, so the last
1315
- // still-armed co-writer sits near the tail — found in O(1) until the final
1316
- // one, turning a fan-in's re-assert from O(N²) into O(N). (Order is
1317
- // irrelevant; this is a find-any.)
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
- const res = b.put(tgt, x, sc.complement);
1355
- sc.complement = res.complement;
1356
- wbStateful[sTop++] = cur;
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 ver = 0;
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
- const res = b.put(tgt, vals, sc.complement);
1399
- const upd = res.updates;
1400
- // Commit `bwd`'s complement directly; it must be consistent with `updates`
1401
- // (no reliance on a post-write `step`). The stamp is re-set post-order (after
1402
- // the sources are written and their versions bumped) so the next forward read
1403
- // sees an unchanged sum and skips `step` — own-write provenance.
1404
- sc.complement = res.complement;
1405
- wbStateful[sTop++] = cur;
1406
- out = upd;
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
- wbNode[top] = pe.parent;
1441
- wbTarget[top] = b.put(tgt);
1442
- top++;
1443
- }
1444
- // Post-order re-stamp: now the sources are written (versions bumped), record
1445
- // each on-path stateful lens's parent-version sum, so its next forward read
1446
- // sees an unchanged sum and skips `step`. Integers only — no `fwd`, no commit.
1447
- for (let i = 0; i < sTop; i++) {
1448
- const sc = wbStateful[i]._bwd.stateful;
1449
- let ver = 0;
1450
- for (let e = wbStateful[i].parentEdges; e !== undefined; e = e.nextParent) {
1451
- ver += e.parent.version;
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; surface the first error after the queue is
1631
- // empty (later errors are dropped the engine stays consistent, the user still
1632
- // sees a failure). A throwing body isn't re-queued (its `F.Watching` is already
1633
- // cleared); it re-arms on the next wake.
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 {