@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.
- package/README.md +1 -1
- package/package.json +4 -4
- 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)
|
|
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.
|
|
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.
|
|
38
|
-
"lib0": "^0.2.
|
|
37
|
+
"@jxsuite/schema": "^2.3.0",
|
|
38
|
+
"lib0": "^0.2.119",
|
|
39
39
|
"y-protocols": "^1.0.7",
|
|
40
|
-
"yjs": "13.6.
|
|
40
|
+
"yjs": "13.6.33"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/ops.ts
CHANGED
|
@@ -1,128 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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";
|