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.
@@ -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
+ }
@@ -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))
@@ -40,8 +41,13 @@ function deepEqual(a, b) {
40
41
  }
41
42
  /** Wire an existing cell to a handle in both directions; returns an unbind. */
42
43
  function bind(c, handle, by, replace) {
43
- const onChange = () => {
44
- c.value = structuredClone(handle.doc());
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(() => {
@@ -2,14 +2,15 @@ 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
- /** Predicate over an object key (or list index): return `true` to *replace* that
6
- * field's value wholesale (a scalar assignment an Automerge `put`) instead of
7
- * recursively merging into it. Use this for opaque JSON blobs that a downstream
8
- * bridge can only consume as whole-object puts — e.g. tldraw's `richText`, whose
9
- * patch applier rejects nested text `splice`s and mis-reads nested `del`s. The
10
- * value is still only written when it actually differs, so unrelated commits
11
- * don't churn it. */
12
- export type Replace = (key: string | number) => boolean;
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>;
13
14
  /** Minimally mutate the Automerge node `target` (inside `handle.change`) to equal
14
15
  * the plain value `next`. Pass `by` for identity-keyed list reconciliation, and
15
16
  * `replace` to assign chosen keys wholesale instead of merging into them. */
@@ -16,6 +16,13 @@
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);
20
27
  /** Structural equality, used to skip a wholesale `replace` write when the field
21
28
  * is already deep-equal (so reconciling an unrelated change doesn't rewrite it).
@@ -55,7 +62,7 @@ function deepEq(a, b) {
55
62
  * the plain value `next`. Pass `by` for identity-keyed list reconciliation, and
56
63
  * `replace` to assign chosen keys wholesale instead of merging into them. */
57
64
  export function reconcile(target, next, by, replace) {
58
- const ctx = { by, replace };
65
+ const ctx = { by, replace: replace && toReplacePred(replace), path: [] };
59
66
  if (Array.isArray(next) && Array.isArray(target))
60
67
  reconcileList(target, next, ctx);
61
68
  else
@@ -136,32 +143,38 @@ function lcs(a, b) {
136
143
  return keep;
137
144
  }
138
145
  function setKey(parent, key, a, b, inList, ctx) {
139
- // Wholesale-replace keys bypass the merge entirely: assign the value so
140
- // Automerge emits a single `put` of the (re)built subtree — no nested text
141
- // `splice`s, no `del`s which is all some bridges can apply. Guard on
142
- // deep-equality so reconciling an unrelated change doesn't rewrite it.
143
- if (ctx.replace?.(key)) {
144
- if (!deepEq(a, b))
145
- parent[key] = b;
146
- return;
147
- }
148
- if (typeof b === "string" && typeof a === "string") {
149
- // Char-level merge for object text fields; list string elements just assign
150
- // (path-relative updateText targets a keyed field, not an array slot).
151
- if (a !== b) {
152
- if (inList)
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))
153
154
  parent[key] = b;
154
- else
155
- 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;
156
175
  }
157
176
  }
158
- else if (Array.isArray(b) && Array.isArray(a)) {
159
- reconcileList(a, b, ctx);
160
- }
161
- else if (isPlainObject(b) && isPlainObject(a)) {
162
- reconcileObject(a, b, ctx);
163
- }
164
- else if (a !== b) {
165
- parent[key] = b;
177
+ finally {
178
+ ctx.path.pop();
166
179
  }
167
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. */