@jxsuite/collab 0.9.0 → 0.10.0

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 (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -4
  3. package/src/ops.ts +12 -124
package/README.md CHANGED
@@ -24,7 +24,7 @@ The CRDT granularity deliberately equals Studio's op-log granularity: mutators e
24
24
  - `applyDocOpsToY(doc, ops, origin)` replays Studio's recorded forward `JxDocOp`s onto the Y tree in one origin-tagged transaction (`LOCAL_ORIGIN` for user edits, `MIRROR_ORIGIN` for derived writes, `SEED_ORIGIN` for bootstraps).
25
25
  - `yEventsToDocOps(events)` converts a remote transaction's deep events back into `JxDocOp`s. Call it **inside** the `observeDeep` callback. It returns `null` for shapes it cannot convert safely; reconcile by diffing instead.
26
26
  - `diffDocs(a, b)` produces ops transforming `a` into `b` (invariant: `apply(clone(a), diff(a,b)) ≡ b`); returns `null` past `maxOps`, in which case `replaceYStructure()` hard-replaces and bumps `meta.canonicalRev` so stale mirror writes are discarded.
27
- - `applyDocOpToDoc` (in `./ops`, yjs-free) is the canonical plain-JSON applier shared with Studio's history replay and canvas shadow doc.
27
+ - `applyDocOpToDoc`, `inverseOf` and `applyDocOpsWithInverse` (in `./ops`, yjs-free) re-export the canonical plain-JSON op vocabulary from `@jxsuite/schema/doc-ops`, shared with Studio's history replay and the canvas shadow doc, so a host without yjs can apply the same ops.
28
28
 
29
29
  ## The wire envelope (`./envelope`)
30
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/collab",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Realtime co-editing for Jx Studio — Y.Doc schema, op bridge, structural differ, wire envelope, and provider contracts shared by every collab backend",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,9 +34,9 @@
34
34
  "test:coverage": "bun test --isolate --coverage"
35
35
  },
36
36
  "dependencies": {
37
- "@jxsuite/schema": "^2.2.0",
38
- "lib0": "^0.2.117",
37
+ "@jxsuite/schema": "^2.3.0",
38
+ "lib0": "^0.2.119",
39
39
  "y-protocols": "^1.0.7",
40
- "yjs": "13.6.32"
40
+ "yjs": "13.6.33"
41
41
  }
42
42
  }
package/src/ops.ts CHANGED
@@ -1,128 +1,16 @@
1
1
  /**
2
- * The canonical Jx document-op vocabulary and its pure applier. A `JxDocOp` is a value-carrying,
3
- * replayable mutation of a plain-JSON Jx document tree, addressed by `JxPath` (alternating
4
- * "children" / index segments for structure, plus node keys). Studio's transaction pipeline records
5
- * forward/inverse pairs of these for history replay; the collab bridge mirrors them into a shared
6
- * Y.Doc.
2
+ * The Jx document-op vocabulary, re-exported from its home in `@jxsuite/schema/doc-ops`.
7
3
  *
8
- * This module is deliberately yjs-free and dependency-light: the slim canvas-iframe bundle imports
9
- * the applier for its non-reactive shadow doc, and it MUST stay byte-identical in behavior to the
10
- * parent's history replay — both sides import this one implementation.
4
+ * It moved so a host with no yjs (a Durable Object, a headless tool) can edit a document the way
5
+ * Studio does. This subpath stays so every existing `@jxsuite/collab/ops` import keeps working.
11
6
  */
12
7
 
13
- import type { JxMutableNode, JxPath } from "@jxsuite/schema/types";
14
-
15
- /** Value-carrying document mutation, replayable in either direction. */
16
- export type JxDocOp =
17
- /** Set (or delete, when value is undefined) a key on the node at path. */
18
- | { op: "set-key"; path: JxPath; key: string; value?: unknown }
19
- /** Insert a node at parentPath.children[index]. */
20
- | { op: "insert-child"; parentPath: JxPath; index: number; node: unknown }
21
- /** Remove parentPath.children[index]. */
22
- | { op: "remove-child"; parentPath: JxPath; index: number }
23
- /** Replace parentPath.children[index] with node. */
24
- | { op: "set-child"; parentPath: JxPath; index: number; node: unknown }
25
- /** Raw two-splice move: remove fromParent.children[fromIndex], insert at toParent[toIndex]. */
26
- | {
27
- op: "move-child";
28
- fromParentPath: JxPath;
29
- fromIndex: number;
30
- toParentPath: JxPath;
31
- toIndex: number;
32
- };
33
-
34
- export interface JxDocOpPair {
35
- forward: JxDocOp;
36
- inverse: JxDocOp;
37
- }
38
-
39
- /** JSON round-trip clone — also normalizes away reactive proxies / functions / undefined. */
40
- function jsonClone<T>(value: T): T {
41
- // oxlint-disable-next-line unicorn/prefer-structured-clone -- structuredClone throws on reactive proxies; JSON normalization is the point
42
- return JSON.parse(JSON.stringify(value)) as T;
43
- }
44
-
45
- /** Deep-clone a recorded value (undefined/null pass through; reactive proxies are read through). */
46
- export function cloneValue<T>(v: T): T {
47
- return v === undefined || v === null ? v : (jsonClone(v as object) as T);
48
- }
49
-
50
- /**
51
- * Walk a plain document tree by path. Paths address nodes by construction; missing segments and
52
- * non-node leaves surface as undefined (typed as node for caller ergonomics, matching Studio's
53
- * historical helper).
54
- */
55
- export function getNodeAtPath(doc: JxMutableNode, path: JxPath): JxMutableNode {
56
- let node = doc;
57
- for (const key of path) {
58
- if (node == null) {
59
- return undefined as unknown as JxMutableNode;
60
- }
61
- node = node[key] as JxMutableNode;
62
- }
63
- return node;
64
- }
65
-
66
- /** The node's children array, lazily created; throws if `node` is itself a children array or mapped. */
67
- export function childArray(node: JxMutableNode): (JxMutableNode | string)[] {
68
- // Defense-in-depth: a path that resolves to a children array (rather than a node) would
69
- // Otherwise get a bogus `.children` property tacked on here, silently storing the insert where
70
- // Nothing renders. Callers must pass a node; fail loudly if they don't.
71
- if (Array.isArray(node)) {
72
- throw new TypeError("Cannot insert into a children array; parentPath must point at a node");
73
- }
74
- if (!node.children) {
75
- node.children = [];
76
- }
77
- if (!Array.isArray(node.children)) {
78
- throw new TypeError("Cannot insert into mapped-array children; edit the map template instead");
79
- }
80
- return node.children;
81
- }
82
-
83
- /**
84
- * Apply a replayable doc op to a bare document tree. Values/nodes are cloned in, so the tree never
85
- * aliases the op object. Throws (with a machine-readable reason) when a target path is missing.
86
- */
87
- export function applyDocOpToDoc(doc: JxMutableNode, op: JxDocOp): void {
88
- switch (op.op) {
89
- case "set-key": {
90
- const node = getNodeAtPath(doc, op.path);
91
- if (!node) {
92
- throw new Error(`doc-op-node-not-found:${op.path.join("/")}`);
93
- }
94
- const target = node as Record<string, unknown>;
95
- if (op.value === undefined) {
96
- delete target[op.key];
97
- } else {
98
- target[op.key] = cloneValue(op.value);
99
- }
100
- return;
101
- }
102
- case "insert-child": {
103
- const parent = getNodeAtPath(doc, op.parentPath);
104
- childArray(parent).splice(op.index, 0, cloneValue(op.node) as JxMutableNode);
105
- return;
106
- }
107
- case "remove-child": {
108
- const parent = getNodeAtPath(doc, op.parentPath);
109
- childArray(parent).splice(op.index, 1);
110
- return;
111
- }
112
- case "set-child": {
113
- const parent = getNodeAtPath(doc, op.parentPath);
114
- childArray(parent).splice(op.index, 1, cloneValue(op.node) as JxMutableNode);
115
- return;
116
- }
117
- case "move-child": {
118
- const fromParent = getNodeAtPath(doc, op.fromParentPath);
119
- const toParent = getNodeAtPath(doc, op.toParentPath);
120
- const [node] = childArray(fromParent).splice(op.fromIndex, 1);
121
- childArray(toParent).splice(op.toIndex, 0, node!);
122
- return;
123
- }
124
- default: {
125
- throw new Error(`unknown-doc-op:${(op as JxDocOp).op}`);
126
- }
127
- }
128
- }
8
+ export {
9
+ applyDocOpToDoc,
10
+ applyDocOpsWithInverse,
11
+ childArray,
12
+ cloneValue,
13
+ getNodeAtPath,
14
+ inverseOf,
15
+ } from "@jxsuite/schema/doc-ops";
16
+ export type { JxDocOp, JxDocOpPair } from "@jxsuite/schema/doc-ops";