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,20 @@
1
+ import type { DocHandle } from "@automerge/automerge-repo";
2
+ import { type Cell, type Writable } from "../core/cell.js";
3
+ import { type Store } from "../core/store.js";
4
+ /** Two-way bridge between an Automerge doc and the reactive graph. */
5
+ export interface DocBridge<T> {
6
+ /** Source cell mirroring the doc; writes (direct or via a lens) flow back to the CRDT. */
7
+ cell: Writable<Cell<T>>;
8
+ /** Deep `store` view over `cell` — `bridge.store.a.b.value = x` commits to the doc. */
9
+ store: Store<T>;
10
+ /** Point the same cell at a different doc, keeping every bound lens/view alive. */
11
+ retarget: (handle: DocHandle<T>) => void;
12
+ /** Detach both directions (call from `disconnectedCallback`). */
13
+ dispose: () => void;
14
+ }
15
+ /** Connect a `DocHandle` to a reactive cell + store, syncing both ways. */
16
+ export declare function connectDoc<T extends object>(handle: DocHandle<T>): DocBridge<T>;
17
+ /** Doc as a writable cell (page-lifetime; use {@link connectDoc} when you need disposal). */
18
+ export declare function docCell<T extends object>(handle: DocHandle<T>): Writable<Cell<T>>;
19
+ /** Doc as a deep store (page-lifetime; use {@link connectDoc} when you need disposal). */
20
+ export declare function docStore<T extends object>(handle: DocHandle<T>): Store<T>;
@@ -0,0 +1,80 @@
1
+ // doc-cell.ts — bridge an Automerge document to the reactive graph.
2
+ //
3
+ // An Automerge `DocHandle` is a writable source of truth that lives outside the
4
+ // cell graph: it emits `change` events and is mutated through `handle.change`.
5
+ // `connectDoc` wires it to a `Writable<Cell<T>>` in both directions so the doc
6
+ // becomes an ordinary cell you can lens, `store`, and bind in JSX.
7
+ //
8
+ // doc → cell : on every `change`, snapshot the doc into the cell.
9
+ // cell → doc : an effect mirrors each commit back via `reconcile` (minimal
10
+ // ops, so concurrent edits merge).
11
+ //
12
+ // The cell uses structural equality, so the two directions converge in one hop:
13
+ // a write reconciles into the doc, the doc echoes a `change`, the snapshot is
14
+ // deep-equal to what we already hold, and the engine stops. No flags, no echo
15
+ // storm. Because the doc is the apex, many independent lens/`store` views can
16
+ // hang off one `connectDoc` — that's the symmetric, no-primary topology: the
17
+ // CRDT is the shared core, every schema is just a projection.
18
+ import { cell, effect } from "../core/cell.js";
19
+ import { store } from "../core/store.js";
20
+ import { reconcile } from "./reconcile.js";
21
+ function deepEqual(a, b) {
22
+ if (Object.is(a, b))
23
+ return true;
24
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object")
25
+ return false;
26
+ const aArr = Array.isArray(a);
27
+ if (aArr !== Array.isArray(b))
28
+ return false;
29
+ const ak = Object.keys(a);
30
+ const bk = Object.keys(b);
31
+ if (ak.length !== bk.length)
32
+ return false;
33
+ for (const k of ak) {
34
+ if (!Object.hasOwn(b, k))
35
+ return false;
36
+ if (!deepEqual(a[k], b[k]))
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+ /** Wire an existing cell to a handle in both directions; returns an unbind. */
42
+ function bind(c, handle) {
43
+ const onChange = () => {
44
+ c.value = structuredClone(handle.doc());
45
+ };
46
+ handle.on("change", onChange);
47
+ const stop = effect(() => {
48
+ const next = c.value;
49
+ handle.change((d) => reconcile(d, next));
50
+ });
51
+ return () => {
52
+ stop();
53
+ handle.off("change", onChange);
54
+ };
55
+ }
56
+ /** Connect a `DocHandle` to a reactive cell + store, syncing both ways. */
57
+ export function connectDoc(handle) {
58
+ const c = cell(structuredClone(handle.doc()), { equals: deepEqual, name: "doc" });
59
+ let unbind = bind(c, handle);
60
+ return {
61
+ cell: c,
62
+ store: store(c),
63
+ retarget: next => {
64
+ unbind();
65
+ // Seed the cell from the new doc *before* re-binding, so the cell→doc
66
+ // effect doesn't push the old value into the freshly targeted doc.
67
+ c.value = structuredClone(next.doc());
68
+ unbind = bind(c, next);
69
+ },
70
+ dispose: () => unbind(),
71
+ };
72
+ }
73
+ /** Doc as a writable cell (page-lifetime; use {@link connectDoc} when you need disposal). */
74
+ export function docCell(handle) {
75
+ return connectDoc(handle).cell;
76
+ }
77
+ /** Doc as a deep store (page-lifetime; use {@link connectDoc} when you need disposal). */
78
+ export function docStore(handle) {
79
+ return connectDoc(handle).store;
80
+ }
@@ -0,0 +1,3 @@
1
+ export type { DocBridge } from "./doc-cell.js";
2
+ export { connectDoc, docCell, docStore } from "./doc-cell.js";
3
+ export { reconcile } from "./reconcile.js";
@@ -0,0 +1,12 @@
1
+ // @bireactive/automerge — view an Automerge CRDT through the reactive graph.
2
+ //
3
+ // `connectDoc(handle)` turns a `DocHandle` into a `Writable<Cell<T>>` plus a deep
4
+ // `store`, synced both ways. Lens/`store` projections off that one cell give you
5
+ // many schemas over a single shared doc with no privileged "primary" — the CRDT
6
+ // is the apex, every view is a leg. `reconcile` is the doc-side diff that keeps
7
+ // writes merge-friendly; it's exported for custom bridges.
8
+ //
9
+ // Automerge is an optional peer dependency: import this entry only when you've
10
+ // installed `@automerge/automerge-repo`.
11
+ export { connectDoc, docCell, docStore } from "./doc-cell.js";
12
+ export { reconcile } from "./reconcile.js";
@@ -0,0 +1,5 @@
1
+ type Any = any;
2
+ /** Minimally mutate the Automerge node `target` (inside `handle.change`) to equal
3
+ * the plain value `next`. */
4
+ export declare function reconcile(target: Any, next: Any): void;
5
+ export {};
@@ -0,0 +1,63 @@
1
+ // reconcile.ts — bring an Automerge document to equal a plain POJO with the
2
+ // *minimum* mutations, so concurrent edits merge instead of clobbering.
3
+ //
4
+ // Automerge's docs warn that spread-assignment (`d.x = {...d.x, k}`) replaces the
5
+ // whole object and destroys its merge history. The reactive side, by contrast,
6
+ // hands us a fresh immutable snapshot on every write (spread-replace all the way
7
+ // up). `reconcile` bridges the two: called inside `handle.change`, it walks the
8
+ // live doc against the snapshot and emits only the ops that actually differ —
9
+ // `updateText` for strings (char-level), in-place splices for lists, recursive
10
+ // descent for objects, scalar sets for the rest.
11
+ //
12
+ // List handling is intentionally simple for now: element-wise in place, with a
13
+ // tail push/truncate. Correct for edits/appends/truncations; a reorder or mid
14
+ // insert produces more ops than ideal. Identity-keyed list reconciliation is the
15
+ // obvious upgrade (mirror the `eachBy` lens's `by`).
16
+ import { updateText } from "@automerge/automerge-repo";
17
+ const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
18
+ /** Minimally mutate the Automerge node `target` (inside `handle.change`) to equal
19
+ * the plain value `next`. */
20
+ export function reconcile(target, next) {
21
+ if (Array.isArray(next) && Array.isArray(target))
22
+ reconcileList(target, next);
23
+ else
24
+ reconcileObject(target, next);
25
+ }
26
+ function reconcileObject(target, next) {
27
+ for (const k of Object.keys(target))
28
+ if (!(k in next))
29
+ delete target[k];
30
+ for (const k of Object.keys(next))
31
+ setKey(target, k, target[k], next[k], false);
32
+ }
33
+ function reconcileList(target, next) {
34
+ const shared = Math.min(target.length, next.length);
35
+ for (let i = 0; i < shared; i++)
36
+ setKey(target, i, target[i], next[i], true);
37
+ if (next.length < target.length)
38
+ target.splice(next.length);
39
+ else
40
+ for (let i = target.length; i < next.length; i++)
41
+ target.push(next[i]);
42
+ }
43
+ function setKey(parent, key, a, b, inList) {
44
+ if (typeof b === "string" && typeof a === "string") {
45
+ // Char-level merge for object text fields; list string elements just assign
46
+ // (path-relative updateText targets a keyed field, not an array slot).
47
+ if (a !== b) {
48
+ if (inList)
49
+ parent[key] = b;
50
+ else
51
+ updateText(parent, [key], b);
52
+ }
53
+ }
54
+ else if (Array.isArray(b) && Array.isArray(a)) {
55
+ reconcileList(a, b);
56
+ }
57
+ else if (isPlainObject(b) && isPlainObject(a)) {
58
+ reconcileObject(a, b);
59
+ }
60
+ else if (a !== b) {
61
+ parent[key] = b;
62
+ }
63
+ }
@@ -0,0 +1,48 @@
1
+ export interface Counts {
2
+ /** A computed/lens/merge getter actually ran (the forward "work" unit). */
3
+ recompute: number;
4
+ /** `propagate` entered (a mark sweep down `subs`). */
5
+ propagate: number;
6
+ /** `checkDirty` entered (a validity pull up `deps`). */
7
+ checkDirty: number;
8
+ /** A dynamic forward edge (`Link`) was created. */
9
+ link: number;
10
+ /** A dynamic forward edge was dropped. */
11
+ unlink: number;
12
+ /** A back-write was armed (legal). */
13
+ arm: number;
14
+ /** A structurally-impossible write was rejected at `arm` (no work done). */
15
+ armBlocked: number;
16
+ /** Nodes visited descending the back-path in `markDown`. */
17
+ markDownVisit: number;
18
+ /** A reverse edge was spliced onto a parent's up-list. */
19
+ linkChild: number;
20
+ /** A reverse edge was released (view unwatched). */
21
+ unlinkChild: number;
22
+ /** Frames entered resolving back-cones (`enterCone`). */
23
+ resolveConeVisit: number;
24
+ /** Nodes popped committing a back-write (`writeBack`). */
25
+ writeBackVisit: number;
26
+ /** Steps of the co-writer re-assert scan (the fan-in re-assert cost). */
27
+ reassertScan: number;
28
+ /** A user `put` (1→1, tuple, or stateful backward) was invoked. */
29
+ put: number;
30
+ /** A merge `fold` was invoked. */
31
+ fold: number;
32
+ /** A stateful `step` was invoked (backward commit path). */
33
+ step: number;
34
+ }
35
+ /** Live counter record. Mutated in place so importers hold a stable reference. */
36
+ export declare const counts: Counts;
37
+ /** The single gate. Read at every instrumented site; flip via `withCounts`. */
38
+ export declare let COUNTS: boolean;
39
+ /** Reset all counters to zero (keeps the same object identity). */
40
+ export declare function resetCounts(): void;
41
+ /** Shallow copy of the current counts. */
42
+ export declare function snapshotCounts(): Counts;
43
+ /** Run `fn` with counting on from a zero baseline; returns the result and the
44
+ * counts it accrued. Restores the prior gate state (counters left as measured). */
45
+ export declare function withCounts<T>(fn: () => T): {
46
+ result: T;
47
+ counts: Counts;
48
+ };
@@ -0,0 +1,58 @@
1
+ // Counts-first instrumentation — measurement scaffolding, not a public surface.
2
+ //
3
+ // The methodology (per the redesign brief): judge engine work by *counts*, not
4
+ // timings. Every metric here is a discrete, predictable event — a user callback
5
+ // invoked, a codepath entered, a node visited, a reverse edge spliced — so a
6
+ // correct minimal engine has a *calculable* target and any work above it is
7
+ // provably wasted. This is the gate for the unification: a change must not raise
8
+ // the forward counts and must hold the backward counts at their minimum.
9
+ //
10
+ // Off by default. `COUNTS` is one module-level gate; each instrumented site reads
11
+ // `if (COUNTS) counts.x++`, so a downstream minifier sees `if (false)` and drops
12
+ // it, and when on the cost is a single predictable branch. Flip via `withCounts`.
13
+ function fresh() {
14
+ return {
15
+ recompute: 0,
16
+ propagate: 0,
17
+ checkDirty: 0,
18
+ link: 0,
19
+ unlink: 0,
20
+ arm: 0,
21
+ armBlocked: 0,
22
+ markDownVisit: 0,
23
+ linkChild: 0,
24
+ unlinkChild: 0,
25
+ resolveConeVisit: 0,
26
+ writeBackVisit: 0,
27
+ reassertScan: 0,
28
+ put: 0,
29
+ fold: 0,
30
+ step: 0,
31
+ };
32
+ }
33
+ /** Live counter record. Mutated in place so importers hold a stable reference. */
34
+ export const counts = fresh();
35
+ /** The single gate. Read at every instrumented site; flip via `withCounts`. */
36
+ export let COUNTS = false;
37
+ /** Reset all counters to zero (keeps the same object identity). */
38
+ export function resetCounts() {
39
+ Object.assign(counts, fresh());
40
+ }
41
+ /** Shallow copy of the current counts. */
42
+ export function snapshotCounts() {
43
+ return { ...counts };
44
+ }
45
+ /** Run `fn` with counting on from a zero baseline; returns the result and the
46
+ * counts it accrued. Restores the prior gate state (counters left as measured). */
47
+ export function withCounts(fn) {
48
+ const prevOn = COUNTS;
49
+ resetCounts();
50
+ COUNTS = true;
51
+ try {
52
+ const result = fn();
53
+ return { result, counts: snapshotCounts() };
54
+ }
55
+ finally {
56
+ COUNTS = prevOn;
57
+ }
58
+ }