bireactive 0.3.0 → 0.3.1

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.
Files changed (46) hide show
  1. package/dist/automerge/doc-cell.d.ts +20 -0
  2. package/dist/automerge/doc-cell.js +80 -0
  3. package/dist/automerge/index.d.ts +3 -0
  4. package/dist/automerge/index.js +12 -0
  5. package/dist/automerge/reconcile.d.ts +5 -0
  6. package/dist/automerge/reconcile.js +63 -0
  7. package/dist/core/_counts.d.ts +48 -0
  8. package/dist/core/_counts.js +58 -0
  9. package/dist/core/cell.d.ts +148 -112
  10. package/dist/core/cell.js +946 -768
  11. package/dist/core/debug.d.ts +25 -0
  12. package/dist/core/debug.js +121 -0
  13. package/dist/core/index.d.ts +6 -1
  14. package/dist/core/index.js +5 -0
  15. package/dist/core/lenses/closed-form-policies.js +8 -3
  16. package/dist/core/lenses/index.d.ts +1 -0
  17. package/dist/core/lenses/index.js +1 -0
  18. package/dist/core/lenses/snap.d.ts +18 -0
  19. package/dist/core/lenses/snap.js +145 -0
  20. package/dist/core/optic.d.ts +13 -0
  21. package/dist/core/optic.js +44 -0
  22. package/dist/core/optics.d.ts +10 -0
  23. package/dist/core/optics.js +30 -0
  24. package/dist/core/store.d.ts +10 -0
  25. package/dist/core/store.js +85 -0
  26. package/dist/core/values/audio.js +4 -5
  27. package/dist/core/values/canvas.js +15 -18
  28. package/dist/core/values/str.js +8 -8
  29. package/dist/formats/lens.js +6 -9
  30. package/dist/jsx-dev-runtime.d.ts +2 -0
  31. package/dist/jsx-dev-runtime.js +5 -0
  32. package/dist/jsx-runtime.d.ts +54 -0
  33. package/dist/jsx-runtime.js +219 -0
  34. package/dist/schema/lens.js +5 -5
  35. package/dist/shapes/drag-behaviors.d.ts +56 -0
  36. package/dist/shapes/drag-behaviors.js +102 -0
  37. package/dist/shapes/drag-spec.d.ts +52 -0
  38. package/dist/shapes/drag-spec.js +112 -0
  39. package/dist/shapes/index.d.ts +3 -1
  40. package/dist/shapes/index.js +3 -1
  41. package/dist/shapes/interaction.d.ts +2 -3
  42. package/dist/shapes/interaction.js +77 -56
  43. package/dist/shapes/label.js +6 -0
  44. package/dist/shapes/layout.d.ts +47 -1
  45. package/dist/shapes/layout.js +59 -1
  46. package/package.json +24 -2
@@ -0,0 +1,25 @@
1
+ import { Cell } from "./cell.js";
2
+ type SomeCell = Cell<any>;
3
+ /** Short, stable display name: the cell's `name`, else `Ctor#n`. */
4
+ export declare function label(c: SomeCell): string;
5
+ /** `source` (no getter), `lens` (writable derived), or `computed` (read-only derived). */
6
+ export declare function kind(c: SomeCell): "source" | "lens" | "computed";
7
+ /** Upstream cells: eager lens parents unioned with dynamic forward deps. */
8
+ export declare function upstream(c: SomeCell): Cell<unknown>[];
9
+ /** One-line summary: `name = value [kind]` plus upstream labels, if any. */
10
+ export declare function explain(c: SomeCell): string;
11
+ export interface DumpOpts {
12
+ /** Max upstream depth to descend. Default `Infinity`. */
13
+ depth?: number;
14
+ /** Include `= value` in each line. Default `true`. */
15
+ values?: boolean;
16
+ }
17
+ /** Render the upstream graph of `root` as an indented tree (cycle-safe). */
18
+ export declare function dumpGraph(root: SomeCell, opts?: DumpOpts): string;
19
+ /** Run `fn` and collect the source cells written during it (back-writes included,
20
+ * since a view write commits through its sources' `_writeSource`). */
21
+ export declare function traceWrites<T>(fn: () => T): {
22
+ result: T;
23
+ writes: Cell<unknown>[];
24
+ };
25
+ export {};
@@ -0,0 +1,121 @@
1
+ // debug.ts — read-only inspection of the cell graph (dev-time, no hot-path cost).
2
+ //
3
+ // Three small tools: `explain` (one line about a cell), `dumpGraph` (the upstream
4
+ // dependency tree), and `traceWrites` (which sources a block of work actually
5
+ // wrote). All walk the same edges the engine maintains — lens parents
6
+ // (`parentEdges`, eager) unioned with dynamic forward deps (`deps`, post-read) —
7
+ // and label cells by their optional `name` (falling back to `Ctor#n`). Nothing
8
+ // here mutates graph state; reads go through `peek` so they don't track.
9
+ import { Cell, isLens, isReadonly, setCellWriteHook } from "./cell.js";
10
+ const edges = (c) => c;
11
+ const erase = (c) => c;
12
+ const ids = new WeakMap();
13
+ let nextId = 1;
14
+ function idOf(c) {
15
+ let i = ids.get(c);
16
+ if (i === undefined) {
17
+ i = nextId++;
18
+ ids.set(c, i);
19
+ }
20
+ return i;
21
+ }
22
+ /** Short, stable display name: the cell's `name`, else `Ctor#n`. */
23
+ export function label(c) {
24
+ return c.name ?? `${c.constructor.name}#${idOf(erase(c))}`;
25
+ }
26
+ /** `source` (no getter), `lens` (writable derived), or `computed` (read-only derived). */
27
+ export function kind(c) {
28
+ return isLens(c) ? "lens" : isReadonly(c) ? "computed" : "source";
29
+ }
30
+ function short(v) {
31
+ try {
32
+ if (typeof v === "string")
33
+ return JSON.stringify(v);
34
+ if (v === null || v === undefined || typeof v !== "object")
35
+ return String(v);
36
+ const s = JSON.stringify(v);
37
+ if (s === undefined)
38
+ return Object.prototype.toString.call(v);
39
+ return s.length > 48 ? `${s.slice(0, 47)}…` : s;
40
+ }
41
+ catch {
42
+ return "?";
43
+ }
44
+ }
45
+ /** Upstream cells: eager lens parents unioned with dynamic forward deps. */
46
+ export function upstream(c) {
47
+ const out = [];
48
+ const seen = new Set();
49
+ const push = (n) => {
50
+ if (n instanceof Cell && !seen.has(n)) {
51
+ seen.add(n);
52
+ out.push(n);
53
+ }
54
+ };
55
+ for (let e = edges(erase(c)).parentEdges; e !== undefined; e = e.nextParent)
56
+ push(e.parent);
57
+ for (let l = edges(erase(c)).deps; l !== undefined; l = l.nextDep)
58
+ push(l.dep);
59
+ return out;
60
+ }
61
+ /** One-line summary: `name = value [kind]` plus upstream labels, if any. */
62
+ export function explain(c) {
63
+ let v;
64
+ try {
65
+ v = c.peek();
66
+ }
67
+ catch {
68
+ v = "?";
69
+ }
70
+ const ups = upstream(c);
71
+ const tail = ups.length > 0 ? ` ← ${ups.map(label).join(", ")}` : "";
72
+ return `${label(c)} = ${short(v)} [${kind(c)}]${tail}`;
73
+ }
74
+ /** Render the upstream graph of `root` as an indented tree (cycle-safe). */
75
+ export function dumpGraph(root, opts = {}) {
76
+ const maxDepth = opts.depth ?? Number.POSITIVE_INFINITY;
77
+ const withValues = opts.values ?? true;
78
+ const lines = [];
79
+ const path = new Set();
80
+ const line = (c, indent) => {
81
+ if (!withValues)
82
+ return `${indent}${label(c)} [${kind(c)}]`;
83
+ let v;
84
+ try {
85
+ v = c.peek();
86
+ }
87
+ catch {
88
+ v = "?";
89
+ }
90
+ return `${indent}${label(c)} = ${short(v)} [${kind(c)}]`;
91
+ };
92
+ const walk = (c, indent, depth) => {
93
+ if (path.has(c)) {
94
+ lines.push(`${indent}${label(c)} ↺`); // back-edge into the current path
95
+ return;
96
+ }
97
+ lines.push(line(c, indent));
98
+ if (depth >= maxDepth)
99
+ return;
100
+ path.add(c);
101
+ for (const u of upstream(c))
102
+ walk(u, `${indent} `, depth + 1);
103
+ path.delete(c);
104
+ };
105
+ walk(root, "", 0);
106
+ return lines.join("\n");
107
+ }
108
+ /** Run `fn` and collect the source cells written during it (back-writes included,
109
+ * since a view write commits through its sources' `_writeSource`). */
110
+ export function traceWrites(fn) {
111
+ const writes = [];
112
+ const restore = setCellWriteHook(c => {
113
+ writes.push(c);
114
+ });
115
+ try {
116
+ return { result: fn(), writes };
117
+ }
118
+ finally {
119
+ restore();
120
+ }
121
+ }
@@ -1,7 +1,12 @@
1
- export { batch, Cell, type CellOptions, cachedDerive, cell, derive, effect, fieldLens, fieldOf, type Init, type Inner, isCell, isLens, isReadonly, lazy, lens, type Network, network, type Read, reader, readNow, SKIP, type Skip, type StatefulBwd, type StatefulLensSpec, setCellWriteHook, settle, transitiveDeps, untracked, type Val, type Writable, type WritableBrand, } from "./cell.js";
1
+ export { type Counts, counts, resetCounts, snapshotCounts, withCounts } from "./_counts.js";
2
+ export { batch, Cell, type CellOptions, cachedDerive, cell, derive, effect, fieldLens, fieldOf, type Init, type Inner, isCell, isLens, isReadonly, lazy, lens, type Network, network, type Optic, type Read, reader, readNow, SKIP, type Skip, type StatefulBwd, type StatefulBwd1, type StatefulLensSpec, type StatefulLensSpec1, setCellWriteHook, settle, transitiveDeps, untracked, type Val, type Writable, type WritableBrand, } from "./cell.js";
3
+ export { type DumpOpts, dumpGraph, explain, kind as cellKind, label as cellLabel, traceWrites, upstream, } from "./debug.js";
2
4
  export { bezier2, bezier3 } from "./derived-geometry.js";
3
5
  export * from "./lenses/index.js";
4
6
  export { each, type Lifecycle } from "./lifecycle.js";
7
+ export { atKey, compose, iso, optic } from "./optic.js";
8
+ export { at, fields } from "./optics.js";
9
+ export { type Store, store } from "./store.js";
5
10
  export { type Equals, type Lerp, type Linear, type Metric, type Pack, type Pivotal, requireEquals, requireLerp, requireLinear, requireMetric, requirePack, requirePivotal, type TraitDict, type Traits, } from "./traits.js";
6
11
  export { Anchor, Dir } from "./values/anchor.js";
7
12
  export { Audio, type AudioClip, audio, stamp as audioStamp } from "./values/audio.js";
@@ -1,7 +1,12 @@
1
+ export { counts, resetCounts, snapshotCounts, withCounts } from "./_counts.js";
1
2
  export { batch, Cell, cachedDerive, cell, derive, effect, fieldLens, fieldOf, isCell, isLens, isReadonly, lazy, lens, network, reader, readNow, SKIP, setCellWriteHook, settle, transitiveDeps, untracked, } from "./cell.js";
3
+ export { dumpGraph, explain, kind as cellKind, label as cellLabel, traceWrites, upstream, } from "./debug.js";
2
4
  export { bezier2, bezier3 } from "./derived-geometry.js";
3
5
  export * from "./lenses/index.js";
4
6
  export { each } from "./lifecycle.js";
7
+ export { atKey, compose, iso, optic } from "./optic.js";
8
+ export { at, fields } from "./optics.js";
9
+ export { store } from "./store.js";
5
10
  export { requireEquals, requireLerp, requireLinear, requireMetric, requirePack, requirePivotal, } from "./traits.js";
6
11
  export { Anchor, Dir } from "./values/anchor.js";
7
12
  export { Audio, audio, stamp as audioStamp } from "./values/audio.js";
@@ -383,9 +383,14 @@ export function pca(points) {
383
383
  // the current axis length and is absorbed (cluster left put).
384
384
  if (Math.abs(target) === c.lenThis)
385
385
  return { updates: vals.map(() => SKIP), complement: c };
386
- // Non-degenerate fast path: scale current cluster along axis.
386
+ // Non-degenerate fast path: scale current cluster along axis. The scale
387
+ // sets the axis length to |target|, so the complement is consistent
388
+ // without a post-write `step` (the engine no longer re-steps own writes).
387
389
  const k = target / c.lenThis;
388
- return { updates: scaleAlongAxis(vals, d.cx, d.cy, c.uX, c.uY, k), complement: c };
390
+ return {
391
+ updates: scaleAlongAxis(vals, d.cx, d.cy, c.uX, c.uY, k),
392
+ complement: { ...c, lenThis: Math.abs(target) },
393
+ };
389
394
  }
390
395
  // Degenerate: reconstruct from complement. Centroid still
391
396
  // derivable from current source (mean translates always work).
@@ -403,7 +408,7 @@ export function pca(points) {
403
408
  const b = c.projOther[i] * c.lenOther;
404
409
  out[i] = { x: cx + a * c.uX + b * c.vX, y: cy + a * c.uY + b * c.vY };
405
410
  }
406
- return { updates: out, complement: c };
411
+ return { updates: out, complement: { ...c, lenThis: Math.abs(target) } };
407
412
  },
408
413
  });
409
414
  };
@@ -4,4 +4,5 @@ export { bbox, meanDiff, procrustes } from "./decompositions.js";
4
4
  export { bezierGestalt, crossfade, mean, meanSpread, mix, select, spread, timeSeries, } from "./domain-aggregates.js";
5
5
  export { angle, clampedMean, diff, distance, pulleySum, reflection, vecLerp } from "./geometry.js";
6
6
  export { type ContinuousOpts, continuous, type RememberOpts, remember, } from "./memory.js";
7
+ export { type ClosestOpts, hullWeights, nearestIndex } from "./snap.js";
7
8
  export { bundle, type FactorOpts, type FactorResult, factor, factorTuple, type OutputSpec, type PackedInput, } from "./typed-factor.js";
@@ -4,4 +4,5 @@ export { bbox, meanDiff, procrustes } from "./decompositions.js";
4
4
  export { bezierGestalt, crossfade, mean, meanSpread, mix, select, spread, timeSeries, } from "./domain-aggregates.js";
5
5
  export { angle, clampedMean, diff, distance, pulleySum, reflection, vecLerp } from "./geometry.js";
6
6
  export { continuous, remember, } from "./memory.js";
7
+ export { hullWeights, nearestIndex } from "./snap.js";
7
8
  export { bundle, factor, factorTuple, } from "./typed-factor.js";
@@ -0,0 +1,18 @@
1
+ import { type Cell, type Read } from "../cell.js";
2
+ type V = {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ /** Convex-hull barycentric weights of `q` over `pts` (Σ = 1, all ≥ 0,
7
+ * clamped to the hull). Closed form for K ≤ 3; Frank–Wolfe for K > 3. */
8
+ export declare function hullWeights(q: V, pts: readonly V[]): number[];
9
+ export interface ClosestOpts {
10
+ /** Hysteresis margin (px): the current pick is kept until a rival is
11
+ * nearer by more than this. Dragology's stickiness. Default 0. */
12
+ sticky?: number;
13
+ }
14
+ /** Index of the candidate nearest `pointer`, with hysteresis. Read-only
15
+ * selection: the stickiness state lives in the lens complement (the
16
+ * sanctioned place for path-dependence), so reads stay pure. */
17
+ export declare function nearestIndex(pointer: Read<V>, candidates: readonly Read<V>[], opts?: ClosestOpts): Cell<number>;
18
+ export {};
@@ -0,0 +1,145 @@
1
+ // snap.ts — the pointer math the `d` drag algebra (shapes/drag-spec.ts) and the
2
+ // demos build on. Candidate positions are live layout cells, so there's no
3
+ // speculative re-render to recover them:
4
+ //
5
+ // • hullWeights — barycentric weights of a point in the convex hull of K
6
+ // targets (closed form for K ≤ 3, Frank–Wolfe for K > 3). Powers
7
+ // `d.between`'s blend and any pointer-in-hull interpolation.
8
+ // • nearestIndex — the candidate nearest the pointer, with hysteresis. The
9
+ // "stickiness" lives in the lens complement (the sanctioned home for
10
+ // path-dependence), so reads stay pure.
11
+ import { lens, SKIP } from "../cell.js";
12
+ const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
13
+ const dot = (a, b) => a.x * b.x + a.y * b.y;
14
+ const dist2 = (a, b) => {
15
+ const dx = a.x - b.x;
16
+ const dy = a.y - b.y;
17
+ return dx * dx + dy * dy;
18
+ };
19
+ // ── convex-hull barycentric weights ─────────────────────────────────
20
+ /** Project `q` onto segment p0→p1; weights `[1−t, t]`, t clamped to [0,1]. */
21
+ function segmentWeights(q, p0, p1) {
22
+ const d = sub(p1, p0);
23
+ const len2 = dot(d, d);
24
+ if (len2 < 1e-18)
25
+ return [0.5, 0.5];
26
+ const t = Math.max(0, Math.min(1, dot(sub(q, p0), d) / len2));
27
+ return [1 - t, t];
28
+ }
29
+ /** Barycentric weights of `q` in triangle (p0,p1,p2), CLAMPED to the
30
+ * triangle: inside → the true coords; outside → the nearest point on the
31
+ * hull (an edge projection or a vertex), so the blend never extrapolates. */
32
+ function triangleWeights(q, p0, p1, p2) {
33
+ const v0 = sub(p1, p0);
34
+ const v1 = sub(p2, p0);
35
+ const v2 = sub(q, p0);
36
+ const d00 = dot(v0, v0);
37
+ const d01 = dot(v0, v1);
38
+ const d11 = dot(v1, v1);
39
+ const d20 = dot(v2, v0);
40
+ const d21 = dot(v2, v1);
41
+ const denom = d00 * d11 - d01 * d01;
42
+ if (Math.abs(denom) > 1e-18) {
43
+ const b1 = (d11 * d20 - d01 * d21) / denom;
44
+ const b2 = (d00 * d21 - d01 * d20) / denom;
45
+ const b0 = 1 - b1 - b2;
46
+ if (b0 >= -1e-9 && b1 >= -1e-9 && b2 >= -1e-9) {
47
+ const s = b0 + b1 + b2;
48
+ return [b0 / s, b1 / s, b2 / s];
49
+ }
50
+ }
51
+ // Outside (or degenerate): nearest point on the three edges.
52
+ const e01 = segmentWeights(q, p0, p1);
53
+ const e12 = segmentWeights(q, p1, p2);
54
+ const e20 = segmentWeights(q, p2, p0);
55
+ const at01 = { x: p0.x * e01[0] + p1.x * e01[1], y: p0.y * e01[0] + p1.y * e01[1] };
56
+ const at12 = { x: p1.x * e12[0] + p2.x * e12[1], y: p1.y * e12[0] + p2.y * e12[1] };
57
+ const at20 = { x: p2.x * e20[0] + p0.x * e20[1], y: p2.y * e20[0] + p0.y * e20[1] };
58
+ const c = [
59
+ [dist2(q, at01), [e01[0], e01[1], 0]],
60
+ [dist2(q, at12), [0, e12[0], e12[1]]],
61
+ [dist2(q, at20), [e20[1], 0, e20[0]]],
62
+ ];
63
+ c.sort((a, b) => a[0] - b[0]);
64
+ return c[0][1];
65
+ }
66
+ /** Frank–Wolfe projection of `q` onto the convex hull of `pts` in
67
+ * barycentric coordinates (used for K > 3). Minimises |Σ wᵢ·pᵢ − q|² over
68
+ * the simplex; O(K·iters), plenty fast for the handful of targets a drag
69
+ * ever offers. */
70
+ function hullProjectWeights(q, pts, iters = 60) {
71
+ const K = pts.length;
72
+ const w = new Array(K).fill(1 / K);
73
+ for (let t = 0; t < iters; t++) {
74
+ let cx = 0;
75
+ let cy = 0;
76
+ for (let i = 0; i < K; i++) {
77
+ cx += w[i] * pts[i].x;
78
+ cy += w[i] * pts[i].y;
79
+ }
80
+ const rx = cx - q.x;
81
+ const ry = cy - q.y;
82
+ let best = 0;
83
+ let bestG = Number.POSITIVE_INFINITY;
84
+ for (let i = 0; i < K; i++) {
85
+ const g = rx * pts[i].x + ry * pts[i].y;
86
+ if (g < bestG) {
87
+ bestG = g;
88
+ best = i;
89
+ }
90
+ }
91
+ const gamma = 2 / (t + 2);
92
+ for (let i = 0; i < K; i++)
93
+ w[i] = w[i] * (1 - gamma);
94
+ w[best] = w[best] + gamma;
95
+ }
96
+ return w;
97
+ }
98
+ /** Convex-hull barycentric weights of `q` over `pts` (Σ = 1, all ≥ 0,
99
+ * clamped to the hull). Closed form for K ≤ 3; Frank–Wolfe for K > 3. */
100
+ export function hullWeights(q, pts) {
101
+ const K = pts.length;
102
+ if (K === 0)
103
+ return [];
104
+ if (K === 1)
105
+ return [1];
106
+ if (K === 2)
107
+ return segmentWeights(q, pts[0], pts[1]);
108
+ if (K === 3)
109
+ return triangleWeights(q, pts[0], pts[1], pts[2]);
110
+ return hullProjectWeights(q, pts);
111
+ }
112
+ function pick(sources, prev, sticky) {
113
+ const p = sources[0];
114
+ let best = -1;
115
+ let bestD = Number.POSITIVE_INFINITY;
116
+ for (let i = 1; i < sources.length; i++) {
117
+ const d = dist2(sources[i], p);
118
+ if (d < bestD) {
119
+ bestD = d;
120
+ best = i - 1;
121
+ }
122
+ }
123
+ if (sticky > 0 && prev >= 0 && prev + 1 < sources.length) {
124
+ const prevD = Math.sqrt(dist2(sources[prev + 1], p));
125
+ if (prevD - Math.sqrt(bestD) < sticky)
126
+ return prev;
127
+ }
128
+ return best;
129
+ }
130
+ /** Index of the candidate nearest `pointer`, with hysteresis. Read-only
131
+ * selection: the stickiness state lives in the lens complement (the
132
+ * sanctioned place for path-dependence), so reads stay pure. */
133
+ export function nearestIndex(pointer, candidates, opts = {}) {
134
+ const sticky = opts.sticky ?? 0;
135
+ const parents = [pointer, ...candidates];
136
+ return lens(parents, {
137
+ init: (sources) => ({ index: pick(sources, -1, 0) }),
138
+ step: (sources, c) => ({ index: pick(sources, c.index, sticky) }),
139
+ fwd: (_sources, c) => c.index,
140
+ bwd: (_t, sources, c) => ({
141
+ updates: sources.map(() => SKIP),
142
+ complement: c,
143
+ }),
144
+ });
145
+ }
@@ -0,0 +1,13 @@
1
+ import type { Optic } from "./cell.js";
2
+ /** Build an optic from a forward and a backward. A 2-arg `put(b, a)` reads the
3
+ * source; a 1-arg `put(b)` reconstructs it (and is treated as an `iso`). */
4
+ export declare function optic<A, B>(get: (a: A) => B, put: (b: B, a: A) => A): Optic<A, B>;
5
+ /** A lossless, source-independent optic (an isomorphism): `to`/`from` invert. */
6
+ export declare function iso<A, B>(to: (a: A) => B, from: (b: B) => A): Optic<A, B>;
7
+ /** Field optic: project key `K`, putting back with a spread-replace. */
8
+ export declare function atKey<T, K extends keyof T>(key: K): Optic<T, T[K]>;
9
+ /** Compose optics left-to-right into one: `compose(a, b, c)` is `a` then `b` then
10
+ * `c`. Typed for up to three; falls back to `Optic<unknown, unknown>` beyond. */
11
+ export declare function compose<A, B>(a: Optic<A, B>): Optic<A, B>;
12
+ export declare function compose<A, B, C>(a: Optic<A, B>, b: Optic<B, C>): Optic<A, C>;
13
+ export declare function compose<A, B, C, D>(a: Optic<A, B>, b: Optic<B, C>, c: Optic<C, D>): Optic<A, D>;
@@ -0,0 +1,44 @@
1
+ // optic.ts — lenses as first-class values, independent of any `Cell`.
2
+ //
3
+ // A `Cell` lens binds a transform to a specific source. An `Optic<A, B>` is that
4
+ // transform *unbound* — a pair of pure functions you can compose, store, and
5
+ // hand around, then apply to a source with `cell.through(optic)` (≡ `lens(cell,
6
+ // o.get, o.put)`). Composition is ordinary lens composition: `(f ∘ g).put(c, a) =
7
+ // f.put(g.put(c, f.get(a)), a)`, so the inner source is reconstructed from `a` on
8
+ // every back-write. An `iso` is the lossless special case whose `put` ignores the
9
+ // source (`readsSource = false`), letting `through` bind a cheaper 1-arg backward.
10
+ //
11
+ // This is deliberately the pure/stateless slice of the lens algebra: no
12
+ // complement, no effects. Stateful and effectful optics-as-values are future work
13
+ // (see _notes); for now reach for `lens(parent, spec)` when you need a complement.
14
+ //
15
+ // The `Optic` type lives in cell.ts (so cell.ts stays import-free and its
16
+ // `Cell.through` can name it); this module is the constructor/algebra surface.
17
+ function make(get, put, readsSource) {
18
+ return {
19
+ get,
20
+ put,
21
+ readsSource,
22
+ through(next) {
23
+ // Composed backward reconstructs the inner B from the outer A, so it always
24
+ // reads the source regardless of either side's own `readsSource`.
25
+ return make(a => next.get(get(a)), (c, a) => put(next.put(c, get(a)), a), true);
26
+ },
27
+ };
28
+ }
29
+ /** Build an optic from a forward and a backward. A 2-arg `put(b, a)` reads the
30
+ * source; a 1-arg `put(b)` reconstructs it (and is treated as an `iso`). */
31
+ export function optic(get, put) {
32
+ return make(get, put, put.length >= 2);
33
+ }
34
+ /** A lossless, source-independent optic (an isomorphism): `to`/`from` invert. */
35
+ export function iso(to, from) {
36
+ return make(to, b => from(b), false);
37
+ }
38
+ /** Field optic: project key `K`, putting back with a spread-replace. */
39
+ export function atKey(key) {
40
+ return make(t => t[key], (v, t) => ({ ...t, [key]: v }), true);
41
+ }
42
+ export function compose(...optics) {
43
+ return optics.reduce((a, b) => a.through(b));
44
+ }
@@ -0,0 +1,10 @@
1
+ import { Cell, type Read, type Writable } from "./cell.js";
2
+ /** Writable field view of `c.value[key]` (spread-replace put). A read-only
3
+ * parent yields a read-only view. */
4
+ export declare function at<T, K extends keyof T>(c: Writable<Cell<T>>, key: K): Writable<Cell<T[K]>>;
5
+ export declare function at<T, K extends keyof T>(c: Read<T>, key: K): Cell<T[K]>;
6
+ /** Lens view of every field, lazily and memoized — `const { r, g, b } =
7
+ * fields(rgb)` yields one writable `at` per key. */
8
+ export declare function fields<T extends object>(c: Writable<Cell<T>>): {
9
+ [K in keyof T]-?: Writable<Cell<T[K]>>;
10
+ };
@@ -0,0 +1,30 @@
1
+ // optics.ts — plain-record field optics over a `Cell<T>`.
2
+ //
3
+ // `fieldOf` / `fieldLens` (cell.ts) project a value-class field and need the
4
+ // field's Cell constructor, so `Vec.x` comes back as a typed `Num` carrying its
5
+ // domain methods. For a plain record you just want `Cell<T[K]>` with the same
6
+ // spread-replace put — `at` / `fields` supply that with full key inference and
7
+ // no constructor argument. Both are thin sugar over `fieldOf` with the base
8
+ // `Cell` as the result type.
9
+ import { Cell, fieldOf } from "./cell.js";
10
+ export function at(c, key) {
11
+ const ctor = Cell;
12
+ return fieldOf(c, key, ctor);
13
+ }
14
+ /** Lens view of every field, lazily and memoized — `const { r, g, b } =
15
+ * fields(rgb)` yields one writable `at` per key. */
16
+ export function fields(c) {
17
+ const cache = new Map();
18
+ return new Proxy(Object.create(null), {
19
+ get(_t, key) {
20
+ if (typeof key === "symbol")
21
+ return undefined;
22
+ let v = cache.get(key);
23
+ if (v === undefined) {
24
+ v = at(c, key);
25
+ cache.set(key, v);
26
+ }
27
+ return v;
28
+ },
29
+ });
30
+ }
@@ -0,0 +1,10 @@
1
+ import type { Cell, Writable } from "./cell.js";
2
+ /** Deep store view: the cell itself, plus a `Store` per object field. Primitives
3
+ * and functions bottom out at the plain `Writable<Cell<T>>`. */
4
+ export type Store<T> = Writable<Cell<T>> & (T extends readonly any[] ? unknown : T extends (...args: any[]) => any ? unknown : T extends object ? {
5
+ [K in keyof T]-?: Store<T[K]>;
6
+ } : unknown);
7
+ /** Deep, lens-backed store view of `cell`. Field access returns a nested `Store`;
8
+ * write through `.value` at any depth. Works over any writable cell, including a
9
+ * lens, so the store stays one source of truth with the rest of the graph. */
10
+ export declare function store<T>(cell: Writable<Cell<T>>): Store<T>;
@@ -0,0 +1,85 @@
1
+ // store.ts — a lens-backed deep store proxy over a `Cell`.
2
+ //
3
+ // Signal libraries grow a separate `store` primitive for nested objects:
4
+ // `store.user.name` reads a path, `setStore(...)` writes one. Here that's just
5
+ // field lenses (`at`) under a recursive proxy — no second reactive primitive, no
6
+ // fine-grained store engine. `store(cell).a.b` is a `Cell` (a chain of `at`
7
+ // lenses), so it composes with everything else: read `.value`, write `.value`,
8
+ // pass it to a component, bind it in JSX. Writes are spread-replace puts straight
9
+ // back to the root cell, so the whole tree stays one source of truth.
10
+ //
11
+ // Navigation yields Stores; the leaf is written through `.value`
12
+ // (`store.user.name.value = "x"`). A property whose name collides with the cell
13
+ // surface below (`value`, `peek`, `lens`, `derive`, `merge`, `through`) resolves
14
+ // to the cell member, not a field — drop to `at(cell, key)` for such a field.
15
+ import { at } from "./optics.js";
16
+ // Cell members forwarded to the underlying cell rather than treated as a field,
17
+ // plus the object protocol methods JS itself may touch.
18
+ const FORWARD = new Set([
19
+ "value",
20
+ "peek",
21
+ "lens",
22
+ "derive",
23
+ "merge",
24
+ "through",
25
+ "toString",
26
+ "valueOf",
27
+ "toJSON",
28
+ "constructor",
29
+ ]);
30
+ // One proxy per cell, so `store(c).a === store(c).a` and child stores are stable.
31
+ const wrapped = new WeakMap();
32
+ function wrap(cell) {
33
+ const hit = wrapped.get(cell);
34
+ if (hit !== undefined)
35
+ return hit;
36
+ // Per-key field lenses and their child stores, both memoized.
37
+ const lensFor = new Map();
38
+ const fieldLens = (key) => {
39
+ let l = lensFor.get(key);
40
+ if (l === undefined) {
41
+ l = at(cell, key);
42
+ lensFor.set(key, l);
43
+ }
44
+ return l;
45
+ };
46
+ const childStores = new Map();
47
+ const proxy = new Proxy(cell, {
48
+ get(target, key) {
49
+ if (typeof key === "symbol" || FORWARD.has(key)) {
50
+ const v = target[key];
51
+ return typeof v === "function" ? v.bind(target) : v;
52
+ }
53
+ if (key === "then")
54
+ return undefined; // never a thenable
55
+ let s = childStores.get(key);
56
+ if (s === undefined) {
57
+ s = wrap(fieldLens(key));
58
+ childStores.set(key, s);
59
+ }
60
+ return s;
61
+ },
62
+ set(target, key, value) {
63
+ if (typeof key === "symbol" || FORWARD.has(key)) {
64
+ target[key] = value;
65
+ return true;
66
+ }
67
+ fieldLens(key).value = value;
68
+ return true;
69
+ },
70
+ has(target, key) {
71
+ if (typeof key === "symbol" || FORWARD.has(key))
72
+ return true;
73
+ const v = target.peek();
74
+ return typeof v === "object" && v !== null && key in v;
75
+ },
76
+ });
77
+ wrapped.set(cell, proxy);
78
+ return proxy;
79
+ }
80
+ /** Deep, lens-backed store view of `cell`. Field access returns a nested `Store`;
81
+ * write through `.value` at any depth. Works over any writable cell, including a
82
+ * lens, so the store stays one source of truth with the rest of the graph. */
83
+ export function store(cell) {
84
+ return wrap(cell);
85
+ }
@@ -76,16 +76,15 @@ export class Audio extends Cell {
76
76
  normalize(target = 1) {
77
77
  const tf = reader(target);
78
78
  const self = this;
79
- return Audio.lens([self], {
80
- init: ([s]) => peak(s),
81
- step: ([s], c, external) => (external ? peak(s) : c),
82
- fwd: ([s]) => {
79
+ return Audio.lens(self, {
80
+ init: s => peak(s),
81
+ fwd: s => {
83
82
  const p = peak(s);
84
83
  return p === 0 ? s : scaled(s, tf() / p);
85
84
  },
86
85
  bwd: (view, _src, c) => {
87
86
  const t = tf();
88
- return { updates: [t === 0 ? view : scaled(view, c / t)], complement: c };
87
+ return { update: t === 0 ? view : scaled(view, c / t), complement: c };
89
88
  },
90
89
  });
91
90
  }