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.
@@ -0,0 +1,4 @@
1
+ import type { Patch } from "@automerge/automerge-repo";
2
+ /** Rebuild `prev` to match the post-change doc `after`, cloning only the spine to
3
+ * each patched value and sharing every untouched subtree by reference. */
4
+ export declare function applyPatches<T extends object>(prev: T, patches: Patch[], after: T): T;
@@ -0,0 +1,62 @@
1
+ // apply-patches.ts — doc → cell incremental invalidation.
2
+ //
3
+ // The naive bridge re-snapshots the whole doc on every change
4
+ // (`structuredClone(handle.doc())`), giving every sub-object a fresh identity, so
5
+ // every field lens recomputes even where nothing changed. `applyPatches` instead
6
+ // walks the Automerge `change` patches and rebuilds only the spine to each changed
7
+ // value: ancestors are shallow-cloned (new identity), the changed value is taken
8
+ // from the post-change doc, and every untouched sibling keeps its prior reference.
9
+ // Unchanged slices stay `Object.is`-equal, so their lenses never fire.
10
+ const shallow = (v) => (Array.isArray(v) ? v.slice() : { ...v });
11
+ function getIn(root, path) {
12
+ let cur = root;
13
+ for (const k of path)
14
+ cur = cur[k];
15
+ return cur;
16
+ }
17
+ /** Rebuild `prev` to match the post-change doc `after`, cloning only the spine to
18
+ * each patched value and sharing every untouched subtree by reference. */
19
+ export function applyPatches(prev, patches, after) {
20
+ if (patches.length === 0)
21
+ return prev;
22
+ const root = shallow(prev);
23
+ const owned = new Set([root]);
24
+ // Descend `path`, shallow-cloning each container the first time we enter it so a
25
+ // shared (prev) object is never mutated; returns the owned container at `path`.
26
+ const spine = (path) => {
27
+ let cur = root;
28
+ for (const k of path) {
29
+ let child = cur[k];
30
+ if (!owned.has(child)) {
31
+ child = shallow(child);
32
+ owned.add(child);
33
+ cur[k] = child;
34
+ }
35
+ cur = child;
36
+ }
37
+ return cur;
38
+ };
39
+ for (const p of patches) {
40
+ const path = p.path;
41
+ if (path.length === 0)
42
+ return structuredClone(after);
43
+ const last = path[path.length - 1];
44
+ // Object-key deletion: rebuild the container minus the key, surviving siblings shared.
45
+ if (p.action === "del" && typeof last === "string") {
46
+ delete spine(path.slice(0, -1))[last];
47
+ continue;
48
+ }
49
+ // Sequence ops (list splice/insert/del, text splice, range marks) point *into*
50
+ // their container; drop the trailing index to replace the whole list/string.
51
+ const seqOp = p.action === "del" ||
52
+ p.action === "insert" ||
53
+ p.action === "splice" ||
54
+ p.action === "mark" ||
55
+ p.action === "unmark";
56
+ const vp = seqOp && typeof last === "number" ? path.slice(0, -1) : path;
57
+ if (vp.length === 0)
58
+ return structuredClone(after);
59
+ spine(vp.slice(0, -1))[vp[vp.length - 1]] = structuredClone(getIn(after, vp));
60
+ }
61
+ return root;
62
+ }
@@ -1,11 +1,15 @@
1
1
  import type { DocHandle } from "@automerge/automerge-repo";
2
2
  import { type Cell, type Writable } from "../core/cell.js";
3
3
  import { type Store } from "../core/store.js";
4
- import { type By } from "./reconcile.js";
4
+ import { type By, type Replace } from "./reconcile.js";
5
5
  /** Bridge options shared by every `connect*` entry. */
6
6
  export interface DocOptions {
7
7
  /** Identity key for list elements, enabling keyed reconciliation (see `reconcile`). */
8
8
  by?: By;
9
+ /** Keys whose values are written wholesale (a `put`) rather than deep-merged —
10
+ * for opaque blobs a downstream bridge can only apply as whole-object puts
11
+ * (see `reconcile`'s `Replace`). */
12
+ replace?: Replace;
9
13
  }
10
14
  /** Lifecycle shared by every bridge: retarget the doc, detach both directions. */
11
15
  export interface DocLifecycle<T> {
@@ -17,6 +17,7 @@
17
17
  // CRDT is the shared core, every schema is just a projection.
18
18
  import { cell, effect } from "../core/cell.js";
19
19
  import { store } from "../core/store.js";
20
+ import { applyPatches } from "./apply-patches.js";
20
21
  import { reconcile } from "./reconcile.js";
21
22
  function deepEqual(a, b) {
22
23
  if (Object.is(a, b))
@@ -39,14 +40,19 @@ function deepEqual(a, b) {
39
40
  return true;
40
41
  }
41
42
  /** Wire an existing cell to a handle in both directions; returns an unbind. */
42
- function bind(c, handle, by) {
43
- const onChange = () => {
44
- c.value = structuredClone(handle.doc());
43
+ function bind(c, handle, by, replace) {
44
+ // Patch-driven invalidation: rebuild only the spine to each changed value so
45
+ // untouched slices keep their identity and their lenses don't recompute. Fall
46
+ // back to a full snapshot when the change replaced this handle's whole scope.
47
+ const onChange = (p) => {
48
+ c.value = p.scopeReplaced
49
+ ? structuredClone(handle.doc())
50
+ : applyPatches(c.peek(), p.patches, p.patchInfo.after);
45
51
  };
46
52
  handle.on("change", onChange);
47
53
  const stop = effect(() => {
48
54
  const next = c.value;
49
- handle.change((d) => reconcile(d, next, by));
55
+ handle.change((d) => reconcile(d, next, by, replace));
50
56
  });
51
57
  return () => {
52
58
  stop();
@@ -56,8 +62,9 @@ function bind(c, handle, by) {
56
62
  /** Core: a doc-backed cell plus lifecycle. The cell projections layer on top. */
57
63
  function connect(handle, opts) {
58
64
  const by = opts?.by;
65
+ const replace = opts?.replace;
59
66
  const c = cell(structuredClone(handle.doc()), { equals: deepEqual, name: "doc" });
60
- let unbind = bind(c, handle, by);
67
+ let unbind = bind(c, handle, by, replace);
61
68
  return {
62
69
  cell: c,
63
70
  retarget: next => {
@@ -65,7 +72,7 @@ function connect(handle, opts) {
65
72
  // Seed the cell from the new doc *before* re-binding, so the cell→doc
66
73
  // effect doesn't push the old value into the freshly targeted doc.
67
74
  c.value = structuredClone(next.doc());
68
- unbind = bind(c, next, by);
75
+ unbind = bind(c, next, by, replace);
69
76
  },
70
77
  dispose: () => unbind(),
71
78
  };
@@ -1,4 +1,4 @@
1
1
  export type { CellBridge, DocBridge, DocLifecycle, DocOptions, StoreBridge } from "./doc-cell.js";
2
2
  export { connectCell, connectDoc, connectStore } from "./doc-cell.js";
3
- export type { By } from "./reconcile.js";
3
+ export type { By, Replace } from "./reconcile.js";
4
4
  export { reconcile } from "./reconcile.js";
@@ -2,7 +2,17 @@ type Any = any;
2
2
  /** Stable identity key for a list element; return a primitive. `undefined` (or a
3
3
  * collision) on any element makes that list fall back to positional. */
4
4
  export type By = (element: unknown) => unknown;
5
+ /** Selects fields whose value is *replaced* wholesale (a scalar assignment → an
6
+ * Automerge `put`) instead of recursively merged. Use this for opaque JSON blobs
7
+ * a downstream bridge can only consume as whole-object puts — e.g. tldraw's
8
+ * `richText`, whose patch applier rejects nested text `splice`s and mis-reads
9
+ * nested `del`s. A predicate receives the value's full path from the doc root
10
+ * (e.g. `["store", "shape:1", "props", "richText"]`); an array or Set of names
11
+ * matches any path whose last segment is one of them (key-name sugar). The value
12
+ * is still only written when it differs, so unrelated commits don't churn it. */
13
+ export type Replace = ((path: (string | number)[]) => boolean) | readonly string[] | ReadonlySet<string>;
5
14
  /** Minimally mutate the Automerge node `target` (inside `handle.change`) to equal
6
- * the plain value `next`. Pass `by` for identity-keyed list reconciliation. */
7
- export declare function reconcile(target: Any, next: Any, by?: By): void;
15
+ * the plain value `next`. Pass `by` for identity-keyed list reconciliation, and
16
+ * `replace` to assign chosen keys wholesale instead of merging into them. */
17
+ export declare function reconcile(target: Any, next: Any, by?: By, replace?: Replace): void;
8
18
  export {};
@@ -16,28 +16,71 @@
16
16
  // common subsequence keeps shared elements in place and emits minimal keyed
17
17
  // splices/inserts for the rest, so reorders and mid-inserts merge cleanly.
18
18
  import { updateText } from "@automerge/automerge-repo";
19
+ /** Normalize the `Replace` sugar (array/Set of key names) into a path predicate. */
20
+ function toReplacePred(r) {
21
+ if (typeof r === "function")
22
+ return r;
23
+ const names = r instanceof Set ? r : new Set(r);
24
+ return path => names.has(String(path[path.length - 1]));
25
+ }
19
26
  const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
27
+ /** Structural equality, used to skip a wholesale `replace` write when the field
28
+ * is already deep-equal (so reconciling an unrelated change doesn't rewrite it).
29
+ * Note `a` may be a live Automerge proxy: its *lists* don't expose indices via
30
+ * `Object.keys`, so arrays are walked by length/index, not key enumeration. */
31
+ function deepEq(a, b) {
32
+ if (Object.is(a, b))
33
+ return true;
34
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object")
35
+ return false;
36
+ const aArr = Array.isArray(a);
37
+ if (aArr !== Array.isArray(b))
38
+ return false;
39
+ if (aArr) {
40
+ const av = a;
41
+ const bv = b;
42
+ if (av.length !== bv.length)
43
+ return false;
44
+ for (let i = 0; i < av.length; i++)
45
+ if (!deepEq(av[i], bv[i]))
46
+ return false;
47
+ return true;
48
+ }
49
+ const ak = Object.keys(a);
50
+ const bk = Object.keys(b);
51
+ if (ak.length !== bk.length)
52
+ return false;
53
+ for (const k of ak) {
54
+ if (!Object.hasOwn(b, k))
55
+ return false;
56
+ if (!deepEq(a[k], b[k]))
57
+ return false;
58
+ }
59
+ return true;
60
+ }
20
61
  /** Minimally mutate the Automerge node `target` (inside `handle.change`) to equal
21
- * the plain value `next`. Pass `by` for identity-keyed list reconciliation. */
22
- export function reconcile(target, next, by) {
62
+ * the plain value `next`. Pass `by` for identity-keyed list reconciliation, and
63
+ * `replace` to assign chosen keys wholesale instead of merging into them. */
64
+ export function reconcile(target, next, by, replace) {
65
+ const ctx = { by, replace: replace && toReplacePred(replace), path: [] };
23
66
  if (Array.isArray(next) && Array.isArray(target))
24
- reconcileList(target, next, by);
67
+ reconcileList(target, next, ctx);
25
68
  else
26
- reconcileObject(target, next, by);
69
+ reconcileObject(target, next, ctx);
27
70
  }
28
- function reconcileObject(target, next, by) {
71
+ function reconcileObject(target, next, ctx) {
29
72
  for (const k of Object.keys(target))
30
73
  if (!(k in next))
31
74
  delete target[k];
32
75
  for (const k of Object.keys(next))
33
- setKey(target, k, target[k], next[k], false, by);
76
+ setKey(target, k, target[k], next[k], false, ctx);
34
77
  }
35
- function reconcileList(target, next, by) {
36
- if (by !== undefined && reconcileKeyed(target, next, by))
78
+ function reconcileList(target, next, ctx) {
79
+ if (ctx.by !== undefined && reconcileKeyed(target, next, ctx))
37
80
  return;
38
81
  const shared = Math.min(target.length, next.length);
39
82
  for (let i = 0; i < shared; i++)
40
- setKey(target, i, target[i], next[i], true, by);
83
+ setKey(target, i, target[i], next[i], true, ctx);
41
84
  if (next.length < target.length)
42
85
  target.splice(next.length);
43
86
  else
@@ -46,7 +89,8 @@ function reconcileList(target, next, by) {
46
89
  }
47
90
  /** Keyed list reconcile via LCS. Returns false (→ positional fallback) when keys
48
91
  * aren't total + unique on either side. */
49
- function reconcileKeyed(target, next, by) {
92
+ function reconcileKeyed(target, next, ctx) {
93
+ const by = ctx.by;
50
94
  const tKeys = target.map(by);
51
95
  const nKeys = next.map(by);
52
96
  if (!totalUnique(tKeys) || !totalUnique(nKeys))
@@ -57,7 +101,7 @@ function reconcileKeyed(target, next, by) {
57
101
  if (keep.has(nKeys[n])) {
58
102
  while (i < target.length && !keep.has(by(target[i])))
59
103
  target.splice(i, 1);
60
- setKey(target, i, target[i], next[n], true, by); // same identity → merge edits
104
+ setKey(target, i, target[i], next[n], true, ctx); // same identity → merge edits
61
105
  i++;
62
106
  }
63
107
  else {
@@ -98,24 +142,39 @@ function lcs(a, b) {
98
142
  }
99
143
  return keep;
100
144
  }
101
- function setKey(parent, key, a, b, inList, by) {
102
- if (typeof b === "string" && typeof a === "string") {
103
- // Char-level merge for object text fields; list string elements just assign
104
- // (path-relative updateText targets a keyed field, not an array slot).
105
- if (a !== b) {
106
- if (inList)
145
+ function setKey(parent, key, a, b, inList, ctx) {
146
+ ctx.path.push(key);
147
+ try {
148
+ // Wholesale-replace paths bypass the merge entirely: assign the value so
149
+ // Automerge emits a single `put` of the (re)built subtree — no nested text
150
+ // `splice`s, no `del`s — which is all some bridges can apply. Guard on
151
+ // deep-equality so reconciling an unrelated change doesn't rewrite it.
152
+ if (ctx.replace?.(ctx.path)) {
153
+ if (!deepEq(a, b))
107
154
  parent[key] = b;
108
- else
109
- updateText(parent, [key], b);
155
+ return;
156
+ }
157
+ if (typeof b === "string" && typeof a === "string") {
158
+ // Char-level merge for object text fields; list string elements just assign
159
+ // (path-relative updateText targets a keyed field, not an array slot).
160
+ if (a !== b) {
161
+ if (inList)
162
+ parent[key] = b;
163
+ else
164
+ updateText(parent, [key], b);
165
+ }
166
+ }
167
+ else if (Array.isArray(b) && Array.isArray(a)) {
168
+ reconcileList(a, b, ctx);
169
+ }
170
+ else if (isPlainObject(b) && isPlainObject(a)) {
171
+ reconcileObject(a, b, ctx);
172
+ }
173
+ else if (a !== b) {
174
+ parent[key] = b;
110
175
  }
111
176
  }
112
- else if (Array.isArray(b) && Array.isArray(a)) {
113
- reconcileList(a, b, by);
114
- }
115
- else if (isPlainObject(b) && isPlainObject(a)) {
116
- reconcileObject(a, b, by);
117
- }
118
- else if (a !== b) {
119
- parent[key] = b;
177
+ finally {
178
+ ctx.path.pop();
120
179
  }
121
180
  }
@@ -29,8 +29,6 @@ export interface Counts {
29
29
  put: number;
30
30
  /** A merge `fold` was invoked. */
31
31
  fold: number;
32
- /** A stateful `step` was invoked (backward commit path). */
33
- step: number;
34
32
  }
35
33
  /** Live counter record. Mutated in place so importers hold a stable reference. */
36
34
  export declare const counts: Counts;
@@ -20,7 +20,6 @@ function fresh() {
20
20
  reassertScan: 0,
21
21
  put: 0,
22
22
  fold: 0,
23
- step: 0,
24
23
  };
25
24
  }
26
25
  /** Live counter record. Mutated in place so importers hold a stable reference. */
@@ -21,9 +21,8 @@ interface LensLink {
21
21
  index: number;
22
22
  parent: Cell<unknown>;
23
23
  child: Cell<unknown>;
24
- /** Spliced into `parent.childEdges` yet? The down-list (`parentEdges`) is
25
- * eager at construction; the up-list is lazy on first back-mark so the
26
- * parent's child order is arm-order (co-writer resolution is last-write-wins). */
24
+ /** Spliced into `parent.childEdges` yet? The up-list is lazy on first back-mark,
25
+ * so a parent's child order is arm-order (co-writer resolution is last-wins). */
27
26
  linked: boolean;
28
27
  nextParent: LensLink | undefined;
29
28
  prevChild: LensLink | undefined;
@@ -39,35 +38,19 @@ declare class MergeNode<T> {
39
38
  constructor(fold: MergeFold<T> | undefined);
40
39
  }
41
40
  declare class BwdSpec {
42
- /** Lens `put` (dual of `getter`): `put(target)` for 1→1 / multi-out (a
43
- * source-reading lens reads its parents at walk time), `put(target, sources, c)`
44
- * for stateful. `undefined` for a merge (folds) or pin (absorbs). */
41
+ /** Lens `put` (dual of `getter`): `put(target)` for 1→1 / multi-out,
42
+ * `put(target, sources, c)` for stateful. `undefined` for a merge or pin. */
45
43
  put: ((target: any, current?: any) => any) | undefined;
46
44
  /** Fold payload; present ⇒ a fan-in merge. */
47
45
  merge: MergeNode<unknown> | undefined;
48
- /** Complement state; present a complement-carrying (stateful) lens. */
49
- stateful: StatefulCore | undefined;
46
+ /** The mutable complement object a stateful optic's `get`/`put` thread (and may
47
+ * mutate in place); present ⇒ a complement-carrying (stateful) lens. Seeded once
48
+ * per bind from `optic.complement`, never reassigned. */
49
+ stateful: object | undefined;
50
50
  /** `put` yields a per-parent tuple (split / stateful) vs a scalar (1→1). The
51
51
  * only discriminant not derivable from topology (a 1-parent split is a tuple). */
52
52
  scatter: boolean;
53
53
  }
54
- /** Runtime state of a symmetric-lens complement, kept off `BwdSpec` so plain
55
- * lenses don't carry its slots. See the stateful-lens header for the theory
56
- * (symmetric/edit lenses) and the version-stamp provenance. */
57
- declare class StatefulCore {
58
- /** Engine-owned memory the view discards. */
59
- complement: unknown;
60
- /** Advance the complement: `step(sources, complement)`. Run only when the
61
- * sources actually moved (the engine gates it; see the stateful header). */
62
- step: (sources: any, complement: any) => any;
63
- /** Sum of the parents' `version`s as of the last sync. Sources moved iff the
64
- * live sum differs — the lazy own-vs-external provenance that replaces a value
65
- * witness. A read syncs it after stepping; a back-write re-stamps it post-order
66
- * (own writes don't re-step, so `bwd` must leave the complement consistent).
67
- * Seeded to `-1` (sums are ≥ 0) so the first use always folds the sources in. */
68
- stamp: number;
69
- constructor(complement: unknown, step: (sources: any, complement: any) => any);
70
- }
71
54
  /** Multi-out / stateful back-write sentinel: "leave this parent untouched."
72
55
  * Every non-`SKIP` slot is written verbatim, `undefined` included; a short array
73
56
  * skips the trailing parents. (1→1 `put` always writes its one parent.) */
@@ -129,17 +112,24 @@ export interface CellOptions<T = unknown> {
129
112
  /** Debug label; surfaces in cyclic-read errors and graph dumps (see debug.ts). */
130
113
  name?: string;
131
114
  }
132
- /** A lens as a first-class value, unbound from any source: `get` projects A→B,
133
- * `put` writes B back into an A. Apply with `cell.through(optic)`; build with
134
- * `optic` / `iso` / `atKey` / `compose` (optic.ts). `readsSource` is `false`
135
- * only for an `iso`, letting `through` bind a cheaper 1-arg backward. */
136
- export interface Optic<A, B> {
137
- readonly get: (a: A) => B;
138
- readonly put: (b: B, a: A) => A;
139
- readonly readsSource: boolean;
140
- /** Compose with a following optic (this first, then `next`). */
141
- through<C>(next: Optic<B, C>): Optic<A, C>;
115
+ /** Per-source back-write shape: a partial SKIP-able tuple for N parents (an
116
+ * array source), the whole new source (or `SKIP`) for one. */
117
+ export type Updates<S> = S extends readonly unknown[] ? BackUpdates<{
118
+ [K in keyof S]: S[K] | Skip;
119
+ }> : S | Skip;
120
+ /** A lens as a first-class value, unbound from any source. `get` projects the
121
+ * source(s) `S` to a view `V`; `put` writes the view back as per-source
122
+ * `Updates<S>`. A 1-arg `put` is an `iso` and ignores the source. `complement`
123
+ * present stateful (see the stateful-optic header). Apply with `cell.lens` /
124
+ * `lens`; build with `optic` / `iso` / `atKey`. */
125
+ export interface Optic<S, V, C extends object = never> {
126
+ get(s: S, c: C): V;
127
+ put(target: V, s: S, c: C): Updates<S>;
128
+ /** Present ⇒ stateful; seeds the per-bind complement. */
129
+ complement?: (s: S) => C;
142
130
  }
131
+ /** An optic bound to a source, with its complement type hidden. */
132
+ export type AppliedOptic<S, V> = Optic<S, V, any>;
143
133
  export declare class Cell<T = unknown> implements ReactiveNode {
144
134
  /** @internal */
145
135
  flags: number;
@@ -180,10 +170,6 @@ export declare class Cell<T = unknown> implements ReactiveNode {
180
170
  /** @internal Visit epoch for `backResolve`'s collect phase (dedups diamonds
181
171
  * without a Set; compared against the global `backCycle`). */
182
172
  bEpoch: number;
183
- /** @internal Monotone committed-change counter. A stateful lens sums its
184
- * parents' versions to detect "did my sources move since I last synced?" —
185
- * the lazy provenance that replaces a value witness (see the stateful header). */
186
- version: number;
187
173
  /** Optional debug label (`cell(0, { name })`); used by errors and graph dumps. */
188
174
  name: string | undefined;
189
175
  constructor(initial: T, opts?: CellOptions<T>);
@@ -197,18 +183,16 @@ export declare class Cell<T = unknown> implements ReactiveNode {
197
183
  /** @internal */
198
184
  _unwatched(): void;
199
185
  peek(): T;
200
- /** Endomorphic lens. A 2-arg `bwd(view, current)` consults the current
201
- * source; a 1-arg `bwd(view)` reconstructs it from the view alone. */
186
+ /** Endomorphic lens from a `fwd`/`bwd` pair (2-arg `bwd` reads the source, 1-arg
187
+ * reconstructs it), or apply optic value(s), chaining left-to-right. The function
188
+ * form is same-type (returns `this`); optics are cross-type. */
202
189
  lens(this: Cell<T>, fwd: (v: T) => T, bwd: (target: T, current: T) => T): this;
190
+ lens<V1>(this: Cell<T>, o: AppliedOptic<T, V1>): Writable<Cell<V1>>;
191
+ lens<V1, V2>(this: Cell<T>, o1: AppliedOptic<T, V1>, o2: AppliedOptic<V1, V2>): Writable<Cell<V2>>;
192
+ lens<V1, V2, V3>(this: Cell<T>, o1: AppliedOptic<T, V1>, o2: AppliedOptic<V1, V2>, o3: AppliedOptic<V2, V3>): Writable<Cell<V3>>;
203
193
  /** Read-only same-type view: the RO dual of the endo `.lens`. For a cross-type view use the typed static
204
194
  * `Target.derive(src, fn)`. */
205
195
  derive(this: Cell<T>, fn: (v: T) => T): this;
206
- /** Apply optic value(s) as a writable lens: `c.through(o)` ≡ `lens(c, o.get,
207
- * o.put)`; multiple optics compose left-to-right (`c.through(a, b)` = `a`
208
- * then `b`). Cross-type, unlike the endomorphic instance `.lens`. */
209
- through<B>(this: Cell<T>, o: Optic<T, B>): Writable<Cell<B>>;
210
- through<B, C>(this: Cell<T>, o1: Optic<T, B>, o2: Optic<B, C>): Writable<Cell<C>>;
211
- through<B, C, D>(this: Cell<T>, o1: Optic<T, B>, o2: Optic<B, C>, o3: Optic<C, D>): Writable<Cell<D>>;
212
196
  /** Backward fan-in: forwards its parent's value unchanged; on write, folds N
213
197
  * contributors into one value. `fold` defaults to last-writer-wins. */
214
198
  merge(this: Cell<T>, fold?: MergeFold<T>): Cell<T>;
@@ -218,14 +202,14 @@ export declare class Cell<T = unknown> implements ReactiveNode {
218
202
  static derive<C extends AnyCellCtor, P>(this: C, parent: Read<P>, fn: (v: P) => Inner<InstanceType<C>>): InstanceType<C>;
219
203
  static derive<C extends AnyCellCtor, P extends readonly Read<unknown>[]>(this: C, parents: P, fn: (vals: ReadValues<P>) => Inner<InstanceType<C>>): InstanceType<C>;
220
204
  static derive<C extends AnyCellCtor>(this: C, fn: () => Inner<InstanceType<C>>): InstanceType<C>;
221
- /** Writable lens. `Cls.lens(parent, fwd, bwd)` for one input,
222
- * `Cls.lens(parents, fwd, bwd)` for N; a 2-arg `bwd` reads the source,
223
- * a 1-arg `bwd` reconstructs it. `Cls.lens(parent(s), spec)` builds a
224
- * complement-carrying lens from `{ init, step, fwd, bwd }`. */
205
+ /** Writable lens, typed to this class. `Cls.lens(parent, fwd, bwd)` for one
206
+ * input, `Cls.lens(parents, fwd, bwd)` for N (a 2-arg `bwd` reads the source, a
207
+ * 1-arg `bwd` reconstructs it); `Cls.lens(parent(s), optic)` applies an optic
208
+ * value (pure or complement-carrying), chaining if several are given. */
225
209
  static lens<C extends AnyCellCtor, P>(this: C, parent: Read<P>, fwd: (v: P) => Inner<InstanceType<C>>, bwd: (target: Inner<InstanceType<C>>, v: P) => P): Writable<InstanceType<C>>;
226
210
  static lens<C extends AnyCellCtor, P extends readonly Read<unknown>[]>(this: C, parents: P, fwd: (vals: ReadValues<P>) => Inner<InstanceType<C>>, bwd: (target: Inner<InstanceType<C>>, vals: ReadValues<P>) => BackUpdates<ReadValuesOrSkip<P>>): Writable<InstanceType<C>>;
227
- static lens<C extends AnyCellCtor, P, Cm>(this: C, parent: Read<P>, spec: StatefulLensSpec1<P, Inner<InstanceType<C>>, Cm>): Writable<InstanceType<C>>;
228
- static lens<C extends AnyCellCtor, P extends readonly Read<unknown>[], Cm>(this: C, parents: P, spec: StatefulLensSpec<ReadValues<P>, Inner<InstanceType<C>>, Cm>): Writable<InstanceType<C>>;
211
+ static lens<C extends AnyCellCtor, P>(this: C, parent: Read<P>, optic: AppliedOptic<P, Inner<InstanceType<C>>>): Writable<InstanceType<C>>;
212
+ static lens<C extends AnyCellCtor, P extends readonly Read<unknown>[]>(this: C, parents: P, optic: AppliedOptic<ReadValues<P>, Inner<InstanceType<C>>>): Writable<InstanceType<C>>;
229
213
  /** Type predicate against this class: `Vec.is(x)` narrows `x` to `Vec`.
230
214
  * Inherited static; works for any subclass via polymorphic `this`. */
231
215
  static is<C extends AnyCellCtor>(this: C, v: unknown): v is InstanceType<C>;
@@ -239,35 +223,6 @@ export declare class Cell<T = unknown> implements ReactiveNode {
239
223
  /** Typed field lens onto `parent.value[key]`. RO parent → RO derive;
240
224
  * writable parent → bidirectional lens with spread-replace `put`. */
241
225
  export declare function fieldOf<C extends AnyCellCtor>(parent: Cell<any>, key: string | number | symbol, Cls: C): InstanceType<C>;
242
- export interface StatefulBwd<S extends readonly unknown[], C> {
243
- /** Per-parent updates: a value (written verbatim, `undefined` included) or
244
- * `SKIP` to leave that parent. A short array skips the trailing parents. */
245
- updates: BackUpdates<{
246
- [K in keyof S]: S[K] | Skip;
247
- }>;
248
- complement: C;
249
- }
250
- export interface StatefulLensSpec<S extends readonly unknown[], V, C> {
251
- init: (sources: S) => C;
252
- /** Advance the complement on an outside change. Optional — defaults to `init`
253
- * (the memoryless refresh); the engine runs it only when sources actually move. */
254
- step?: (sources: S, complement: C) => C;
255
- fwd: (sources: S, complement: C) => V;
256
- bwd: (target: V, sources: S, complement: C) => StatefulBwd<S, C>;
257
- }
258
- /** Single-source `bwd` result: a scalar `update` (or `SKIP`) plus the complement. */
259
- export interface StatefulBwd1<S, C> {
260
- update: S | Skip;
261
- complement: C;
262
- }
263
- /** Single-source stateful spec — the scalar fast-path of `StatefulLensSpec`: one
264
- * parent, so `init`/`step`/`fwd`/`bwd` take the source value directly, not a tuple. */
265
- export interface StatefulLensSpec1<S, V, C> {
266
- init: (source: S) => C;
267
- step?: (source: S, complement: C) => C;
268
- fwd: (source: S, complement: C) => V;
269
- bwd: (target: V, source: S, complement: C) => StatefulBwd1<S, C>;
270
- }
271
226
  /** Writable source; passes an existing `Writable` through (idempotent). */
272
227
  export declare function cell<T>(initial: T | Writable<Cell<T>>, opts?: CellOptions<T>): Writable<Cell<T>>;
273
228
  /** Untyped read-only view: `derive(parent, fn)`, `derive(parents, fn)`,
@@ -275,9 +230,9 @@ export declare function cell<T>(initial: T | Writable<Cell<T>>, opts?: CellOptio
275
230
  export declare function derive<P, R>(parent: Read<P>, fn: (v: P) => R): Cell<R>;
276
231
  export declare function derive<P extends readonly Read<unknown>[], R>(parents: P, fn: (vals: ReadValues<P>) => R): Cell<R>;
277
232
  export declare function derive<R>(fn: () => R): Cell<R>;
278
- /** Untyped lens, inferring `R` from the closures. A 2-arg `bwd` reads the
279
- * source, a 1-arg `bwd` reconstructs it; `lens(parent(s), spec)` builds a
280
- * complement-carrying lens. */
233
+ /** Untyped lens, inferring `R` from the closures or optic. A 2-arg `bwd` reads
234
+ * the source, a 1-arg `bwd` reconstructs it; `lens(parent(s), optic)` applies an
235
+ * optic value (pure or complement-carrying), chaining if several are given. */
281
236
  export declare function lens<P, R>(parent: Read<P>, fwd: (v: P) => R, bwd: (target: R, v: P) => P): Writable<Cell<R>>;
282
237
  export declare function lens<P extends readonly Read<unknown>[], R>(parents: P, fwd: (vals: ReadValues<P>) => R, bwd: (target: R, vals: ReadValues<P>) => ReadValuesOrSkip<P>): Writable<Cell<R>>;
283
238
  export declare function lens<S extends Record<string, Read<unknown>>, R>(parents: S, fwd: (vals: {
@@ -287,8 +242,10 @@ export declare function lens<S extends Record<string, Read<unknown>>, R>(parents
287
242
  }) => Partial<{
288
243
  [K in keyof S]: Inner<S[K]> | Skip;
289
244
  }>): Writable<Cell<R>>;
290
- export declare function lens<P, R, C>(parent: Read<P>, spec: StatefulLensSpec1<P, R, C>): Writable<Cell<R>>;
291
- export declare function lens<P extends readonly Read<unknown>[], R, C>(parents: P, spec: StatefulLensSpec<ReadValues<P>, R, C>): Writable<Cell<R>>;
245
+ export declare function lens<P, R>(parent: Read<P>, optic: AppliedOptic<P, R>): Writable<Cell<R>>;
246
+ export declare function lens<P, V1, R>(parent: Read<P>, o1: AppliedOptic<P, V1>, o2: AppliedOptic<V1, R>): Writable<Cell<R>>;
247
+ export declare function lens<P, V1, V2, R>(parent: Read<P>, o1: AppliedOptic<P, V1>, o2: AppliedOptic<V1, V2>, o3: AppliedOptic<V2, R>): Writable<Cell<R>>;
248
+ export declare function lens<P extends readonly Read<unknown>[], R>(parents: P, optic: AppliedOptic<ReadValues<P>, R>): Writable<Cell<R>>;
292
249
  export declare function effect(fn: () => (() => void) | void): () => void;
293
250
  /** Run all pending effects now, synchronously — the escape hatch for code that
294
251
  * must observe effect side-effects before yielding. Reads never need it. */