reptree 0.4.0 → 0.6.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 +59 -108
- package/dist/index.cjs +37 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -32
- package/dist/index.d.ts +17 -32
- package/dist/index.js +37 -164
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,98 +1,97 @@
|
|
|
1
|
-
# RepTree
|
|
1
|
+
# RepTree - replicated trees with properties
|
|
2
2
|
|
|
3
|
-
A tree data structure
|
|
3
|
+
A JavaScript tree data structure for storing and syncing app state. It can be used both to represent and persist the state in the frontend and backend.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
>
|
|
7
|
-
> RepTree was created for the [Supa](https://github.com/supaorg/supa) project, an open-source alternative to ChatGPT.
|
|
5
|
+
RepTree uses [CRDTs](https://crdt.tech/) for seamless replication between users.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
> RepTree was created for the [Sila](https://github.com/silaorg/sila) project, an open-source alternative to ChatGPT.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
- A move tree CRDT is used for the tree structure (https://martin.kleppmann.com/papers/move-op.pdf).
|
|
13
|
-
- A last writer wins (LWW) CRDT is used for properties.
|
|
14
|
-
- Yjs integration for collaborative editing with various shared data types (Text, Array, Map, XML).
|
|
9
|
+
## What it solves
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
If you have a tree structure in your app where each vertex/node/leaf can be moved independently by multiple users, you need a solution that resolves conflicts when the same vertex is moved in different ways. Otherwise your tree can diverge or form loops. This includes folder structures (people creating and moving folders), 2D/3D scenes with objects being moved and parented, and Notion‑like documents where blocks with text and other properties are edited by users.
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
You probably also want properties on each vertex/node/leaf and to have them sync correctly between peers without conflicts. RepTree syncs properties too.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
19
16
|
|
|
20
17
|
```bash
|
|
21
18
|
npm install reptree
|
|
22
19
|
```
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
### Reactive vertex with Zod (optional)
|
|
27
|
-
|
|
21
|
+
### Example 1
|
|
28
22
|
```ts
|
|
29
|
-
import { RepTree } from
|
|
30
|
-
import { z } from 'zod';
|
|
23
|
+
import { RepTree } from "reptree";
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const Person = z.object({ name: z.string(), age: z.number().int().min(0) });
|
|
25
|
+
// Create a tree with a root
|
|
26
|
+
const tree = new RepTree("company-org-1");
|
|
27
|
+
const company = tree.createRoot();
|
|
37
28
|
|
|
38
|
-
|
|
29
|
+
// Create a node (we call them vertices in RepTree) in the root of our new tree
|
|
30
|
+
const devs = company.newNamedChild("developers");
|
|
31
|
+
const qa = company.newNamedChild("qa");
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```
|
|
33
|
+
// Create a vertex in another vertex
|
|
34
|
+
const alice = qa.newChild();
|
|
43
35
|
|
|
44
|
-
|
|
36
|
+
// Set properties
|
|
37
|
+
alice.setProperty("name", "Alice");
|
|
38
|
+
alice.setProperty("age", 32);
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
// Move the vertex inside a different vertex
|
|
41
|
+
alice.moveTo(devs);
|
|
48
42
|
|
|
49
|
-
|
|
43
|
+
// Bind a vertex to a type to set its properties like regular fields
|
|
44
|
+
const bob = qa.newChild().bind<{ name: string; age: number }>();
|
|
45
|
+
bob.name = "Bob";
|
|
46
|
+
bob.age = 33;
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
// Use a Zod type for runtime type checks
|
|
49
|
+
import { z } from "zod";
|
|
50
|
+
const Person = z.object({ name: z.string(), age: z.number().int().min(0) });
|
|
51
|
+
const casey = devs.newNamedChild("Casey").bind(Person);
|
|
52
|
+
casey.name = "Casey";
|
|
53
|
+
casey.age = 34;
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
### Example 2
|
|
58
57
|
|
|
59
58
|
```typescript
|
|
60
|
-
import { RepTree } from
|
|
59
|
+
import { RepTree } from "reptree";
|
|
61
60
|
|
|
62
61
|
// Create a new tree
|
|
63
|
-
const tree = new RepTree(
|
|
62
|
+
const tree = new RepTree("peer1");
|
|
64
63
|
const root = tree.createRoot();
|
|
65
|
-
root.name =
|
|
64
|
+
root.name = "Project";
|
|
66
65
|
|
|
67
66
|
// Create a folder structure with properties
|
|
68
|
-
const docsFolder = root.newNamedChild(
|
|
67
|
+
const docsFolder = root.newNamedChild("Docs");
|
|
69
68
|
docsFolder.setProperties({
|
|
70
|
-
type:
|
|
71
|
-
icon:
|
|
69
|
+
type: "folder",
|
|
70
|
+
icon: "folder-icon",
|
|
72
71
|
});
|
|
73
72
|
|
|
74
|
-
const imagesFolder = root.newNamedChild(
|
|
73
|
+
const imagesFolder = root.newNamedChild("Images");
|
|
75
74
|
imagesFolder.setProperties({
|
|
76
|
-
type:
|
|
77
|
-
icon:
|
|
75
|
+
type: "folder",
|
|
76
|
+
icon: "image-icon",
|
|
78
77
|
});
|
|
79
78
|
|
|
80
79
|
// Add files to folders
|
|
81
|
-
const readmeFile = docsFolder.newNamedChild(
|
|
80
|
+
const readmeFile = docsFolder.newNamedChild("README.md");
|
|
82
81
|
readmeFile.setProperties({
|
|
83
|
-
type:
|
|
82
|
+
type: "file",
|
|
84
83
|
size: 2048,
|
|
85
|
-
lastModified:
|
|
86
|
-
s3Path:
|
|
84
|
+
lastModified: "2023-10-15T14:22:10Z",
|
|
85
|
+
s3Path: "s3://my-bucket/docs/README.md",
|
|
87
86
|
});
|
|
88
87
|
|
|
89
|
-
const logoFile = imagesFolder.newNamedChild(
|
|
88
|
+
const logoFile = imagesFolder.newNamedChild("logo.png");
|
|
90
89
|
logoFile.setProperties({
|
|
91
|
-
type:
|
|
90
|
+
type: "file",
|
|
92
91
|
size: 15360,
|
|
93
|
-
dimensions:
|
|
94
|
-
format:
|
|
95
|
-
s3Path:
|
|
92
|
+
dimensions: "512x512",
|
|
93
|
+
format: "png",
|
|
94
|
+
s3Path: "s3://my-bucket/images/logo.png",
|
|
96
95
|
});
|
|
97
96
|
|
|
98
97
|
// Move a file to a different folder
|
|
@@ -102,65 +101,17 @@ logoFile.moveTo(docsFolder);
|
|
|
102
101
|
const docsFolderContents = docsFolder.children;
|
|
103
102
|
|
|
104
103
|
// Syncing between trees
|
|
105
|
-
const otherTree = new RepTree(
|
|
104
|
+
const otherTree = new RepTree("peer2");
|
|
106
105
|
const ops = tree.getAllOps();
|
|
107
106
|
otherTree.merge(ops);
|
|
108
107
|
```
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`vertex.newChild(props)` and `vertex.newNamedChild(name, props)` accept plain objects. RepTree will:
|
|
113
|
-
|
|
114
|
-
- Map `name` → `_n`, `createdAt` (Date) → `_c` (ISO)
|
|
115
|
-
- Filter unsupported types (non-primitive objects except Y.Doc)
|
|
116
|
-
- Ignore `props.name` if `newNamedChild` has an explicit `name`
|
|
117
|
-
- Forbid nested children in props for now
|
|
118
|
-
|
|
119
|
-
## Yjs Integration
|
|
120
|
-
|
|
121
|
-
RepTree supports [Yjs](https://github.com/yjs/yjs) documents as vertex properties, enabling real-time collaborative editing with a variety of shared data types:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
import { RepTree } from 'reptree';
|
|
125
|
-
import * as Y from 'yjs';
|
|
126
|
-
|
|
127
|
-
// Create a tree with a root vertex
|
|
128
|
-
const tree = new RepTree('peer1');
|
|
129
|
-
const root = tree.createRoot();
|
|
130
|
-
|
|
131
|
-
// Create a Yjs document
|
|
132
|
-
const ydoc = new Y.Doc();
|
|
133
|
-
const ytext = ydoc.getText('default');
|
|
134
|
-
ytext.insert(0, 'Hello world');
|
|
135
|
-
|
|
136
|
-
// Set the Yjs document as a property
|
|
137
|
-
root.setProperty('content', ydoc);
|
|
138
|
-
|
|
139
|
-
// Later, retrieve and modify the document
|
|
140
|
-
const retrievedDoc = root.getProperty('content') as Y.Doc;
|
|
141
|
-
retrievedDoc.getText('default').insert(retrievedDoc.getText('default').length, '!');
|
|
142
|
-
|
|
143
|
-
// Sync operations with another tree
|
|
144
|
-
const tree2 = new RepTree('peer2');
|
|
145
|
-
tree2.merge(tree.popLocalOps());
|
|
146
|
-
|
|
147
|
-
// Both trees now have the same Yjs document content
|
|
148
|
-
const root2 = tree2.root;
|
|
149
|
-
const doc2 = root2.getProperty('content') as Y.Doc;
|
|
150
|
-
console.log(doc2.getText('default').toString()); // 'Hello world!'
|
|
151
|
-
```
|
|
109
|
+
## CRDTs
|
|
152
110
|
|
|
153
|
-
|
|
154
|
-
-
|
|
155
|
-
|
|
156
|
-
- **Y.Array** - For ordered collections of data
|
|
157
|
-
- **Y.Map** - For key-value pairs and structured data
|
|
158
|
-
- **Y.XmlFragment/Y.XmlElement** - For XML-like structured content
|
|
159
|
-
- Complex nested data structures (arrays within maps, maps within arrays, etc.)
|
|
160
|
-
- Automatic CRDT synchronization between peers
|
|
161
|
-
- Conflict-free concurrent editing
|
|
162
|
-
- Integration with existing Yjs ecosystem (editors, frameworks, etc.)
|
|
111
|
+
RepTree uses two conflict-free replicated data types (CRDTs):
|
|
112
|
+
- A move tree CRDT for the tree structure (https://martin.kleppmann.com/papers/move-op.pdf).
|
|
113
|
+
- A last-writer-wins (LWW) CRDT is for properties.
|
|
163
114
|
|
|
164
115
|
## License
|
|
165
116
|
|
|
166
|
-
MIT
|
|
117
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -36,10 +26,7 @@ __export(index_exports, {
|
|
|
36
26
|
Vertex: () => Vertex,
|
|
37
27
|
VertexState: () => VertexState,
|
|
38
28
|
bindVertex: () => bindVertex,
|
|
39
|
-
defaultAliases: () => defaultAliases,
|
|
40
29
|
isAnyPropertyOp: () => isAnyPropertyOp,
|
|
41
|
-
isLWWPropertyOp: () => isLWWPropertyOp,
|
|
42
|
-
isModifyPropertyOp: () => isModifyPropertyOp,
|
|
43
30
|
isMoveVertexOp: () => isMoveVertexOp,
|
|
44
31
|
newMoveVertexOp: () => newMoveVertexOp,
|
|
45
32
|
newSetTransientVertexPropertyOp: () => newSetTransientVertexPropertyOp,
|
|
@@ -102,12 +89,6 @@ function isMoveVertexOp(op) {
|
|
|
102
89
|
function isAnyPropertyOp(op) {
|
|
103
90
|
return "key" in op;
|
|
104
91
|
}
|
|
105
|
-
function isLWWPropertyOp(op) {
|
|
106
|
-
return "key" in op && "value" in op && (!op.value || typeof op.value !== "object" || !("type" in op.value));
|
|
107
|
-
}
|
|
108
|
-
function isModifyPropertyOp(op) {
|
|
109
|
-
return "key" in op && "value" in op && typeof op.value === "object" && op.value !== null && "type" in op.value;
|
|
110
|
-
}
|
|
111
92
|
function newMoveVertexOp(clock, peerId, targetId, parentId) {
|
|
112
93
|
return { id: createOpId(clock, peerId), targetId, parentId };
|
|
113
94
|
}
|
|
@@ -373,7 +354,7 @@ var TreeState = class {
|
|
|
373
354
|
const vertex = this.getVertex(vertexId);
|
|
374
355
|
if (vertex) {
|
|
375
356
|
for (const prop of vertex.getAllProperties()) {
|
|
376
|
-
if (prop.key === "
|
|
357
|
+
if (prop.key === "name") {
|
|
377
358
|
vertexName = prop.value;
|
|
378
359
|
}
|
|
379
360
|
const propPrefix = indent + (isLast ? " " : "\u2502 ") + "\u2022 ";
|
|
@@ -386,8 +367,8 @@ var TreeState = class {
|
|
|
386
367
|
const sortedChildren = [...children].sort((a, b) => {
|
|
387
368
|
const vertexA = this.getVertex(a);
|
|
388
369
|
const vertexB = this.getVertex(b);
|
|
389
|
-
const nameA = vertexA?.getProperty("
|
|
390
|
-
const nameB = vertexB?.getProperty("
|
|
370
|
+
const nameA = vertexA?.getProperty("name");
|
|
371
|
+
const nameB = vertexB?.getProperty("name");
|
|
391
372
|
if (nameA && nameB) {
|
|
392
373
|
return nameA.localeCompare(nameB);
|
|
393
374
|
}
|
|
@@ -411,30 +392,10 @@ function uuid() {
|
|
|
411
392
|
}
|
|
412
393
|
|
|
413
394
|
// src/reactive.ts
|
|
414
|
-
var defaultAliases = [
|
|
415
|
-
{ publicKey: "name", internalKey: "_n" },
|
|
416
|
-
{
|
|
417
|
-
publicKey: "createdAt",
|
|
418
|
-
internalKey: "_c",
|
|
419
|
-
toPublic: (v) => typeof v === "string" ? new Date(v) : v,
|
|
420
|
-
toInternal: (v) => v instanceof Date ? v.toISOString() : v
|
|
421
|
-
}
|
|
422
|
-
];
|
|
423
|
-
function buildAliasMaps(aliases) {
|
|
424
|
-
const publicToInternal = /* @__PURE__ */ new Map();
|
|
425
|
-
const internalToPublic = /* @__PURE__ */ new Map();
|
|
426
|
-
for (const rule of aliases) {
|
|
427
|
-
publicToInternal.set(rule.publicKey, rule);
|
|
428
|
-
internalToPublic.set(rule.internalKey, rule);
|
|
429
|
-
}
|
|
430
|
-
return { publicToInternal, internalToPublic };
|
|
431
|
-
}
|
|
432
395
|
function bindVertex(tree, id, schemaOrOptions) {
|
|
433
|
-
const isOptions = typeof schemaOrOptions === "object" && schemaOrOptions !== null && (Object.prototype.hasOwnProperty.call(schemaOrOptions, "
|
|
396
|
+
const isOptions = typeof schemaOrOptions === "object" && schemaOrOptions !== null && (Object.prototype.hasOwnProperty.call(schemaOrOptions, "includeInternalKeys") || Object.prototype.hasOwnProperty.call(schemaOrOptions, "schema"));
|
|
434
397
|
const options = isOptions ? schemaOrOptions : { schema: schemaOrOptions };
|
|
435
398
|
const schema = options.schema;
|
|
436
|
-
const aliases = options.aliases ?? defaultAliases;
|
|
437
|
-
const { publicToInternal } = buildAliasMaps(aliases);
|
|
438
399
|
const obj = {};
|
|
439
400
|
Object.defineProperties(obj, {
|
|
440
401
|
$vertex: { get: () => tree.getVertex(id), enumerable: false, configurable: true },
|
|
@@ -471,19 +432,14 @@ function bindVertex(tree, id, schemaOrOptions) {
|
|
|
471
432
|
const transientProxy = new Proxy({}, {
|
|
472
433
|
set(_, prop, value) {
|
|
473
434
|
if (typeof prop === "string") {
|
|
474
|
-
|
|
475
|
-
const internalKey = rule?.internalKey ?? prop;
|
|
476
|
-
const internalValue = rule?.toInternal ? rule.toInternal(value) : value;
|
|
477
|
-
tree.setTransientVertexProperty(id, internalKey, internalValue);
|
|
435
|
+
tree.setTransientVertexProperty(id, prop, value);
|
|
478
436
|
}
|
|
479
437
|
return true;
|
|
480
438
|
},
|
|
481
439
|
get(_, prop) {
|
|
482
440
|
if (typeof prop !== "string") return void 0;
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
const rawValue = tree.getVertexProperty(id, internalKey, true);
|
|
486
|
-
return rule?.toPublic ? rule.toPublic(rawValue) : rawValue;
|
|
441
|
+
const rawValue = tree.getVertexProperty(id, prop, true);
|
|
442
|
+
return rawValue;
|
|
487
443
|
}
|
|
488
444
|
});
|
|
489
445
|
fn(transientProxy);
|
|
@@ -513,10 +469,8 @@ function bindVertex(tree, id, schemaOrOptions) {
|
|
|
513
469
|
if (prop in target) {
|
|
514
470
|
return Reflect.get(target, prop, receiver);
|
|
515
471
|
}
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
const rawValue = tree.getVertexProperty(id, internalKey, true);
|
|
519
|
-
return rule?.toPublic ? rule.toPublic(rawValue) : rawValue;
|
|
472
|
+
const rawValue = tree.getVertexProperty(id, prop, true);
|
|
473
|
+
return rawValue;
|
|
520
474
|
},
|
|
521
475
|
set(target, prop, value) {
|
|
522
476
|
if (typeof prop !== "string") {
|
|
@@ -530,19 +484,14 @@ function bindVertex(tree, id, schemaOrOptions) {
|
|
|
530
484
|
value = res.data;
|
|
531
485
|
}
|
|
532
486
|
}
|
|
533
|
-
|
|
534
|
-
const internalKey = rule?.internalKey ?? prop;
|
|
535
|
-
const internalValue = rule?.toInternal ? rule.toInternal(value) : value;
|
|
536
|
-
tree.setVertexProperty(id, internalKey, internalValue);
|
|
487
|
+
tree.setVertexProperty(id, prop, value);
|
|
537
488
|
return true;
|
|
538
489
|
},
|
|
539
490
|
deleteProperty(_target, prop) {
|
|
540
491
|
if (typeof prop !== "string") {
|
|
541
492
|
return true;
|
|
542
493
|
}
|
|
543
|
-
|
|
544
|
-
const internalKey = rule?.internalKey ?? prop;
|
|
545
|
-
tree.setVertexProperty(id, internalKey, void 0);
|
|
494
|
+
tree.setVertexProperty(id, prop, void 0);
|
|
546
495
|
return true;
|
|
547
496
|
}
|
|
548
497
|
});
|
|
@@ -550,7 +499,6 @@ function bindVertex(tree, id, schemaOrOptions) {
|
|
|
550
499
|
}
|
|
551
500
|
|
|
552
501
|
// src/Vertex.ts
|
|
553
|
-
var Y = __toESM(require("yjs"), 1);
|
|
554
502
|
var Vertex = class _Vertex {
|
|
555
503
|
constructor(tree, state) {
|
|
556
504
|
this.tree = tree;
|
|
@@ -560,10 +508,10 @@ var Vertex = class _Vertex {
|
|
|
560
508
|
return this.state.id;
|
|
561
509
|
}
|
|
562
510
|
get name() {
|
|
563
|
-
return this.getProperty("
|
|
511
|
+
return this.getProperty("name");
|
|
564
512
|
}
|
|
565
513
|
set name(name) {
|
|
566
|
-
this.tree.setVertexProperty(this.id, "
|
|
514
|
+
this.tree.setVertexProperty(this.id, "name", name);
|
|
567
515
|
}
|
|
568
516
|
get createdAt() {
|
|
569
517
|
const createdAt = this.getProperty("_c");
|
|
@@ -683,7 +631,6 @@ var Vertex = class _Vertex {
|
|
|
683
631
|
}
|
|
684
632
|
/**
|
|
685
633
|
* Normalizes an input props object for vertex creation:
|
|
686
|
-
* - Aliases name -> _n, createdAt -> _c (Date -> ISO string)
|
|
687
634
|
* - Filters unsupported field types with a console warning
|
|
688
635
|
* - When a name param is provided to newNamedChild, ignores conflicting name in props
|
|
689
636
|
*/
|
|
@@ -698,14 +645,9 @@ var Vertex = class _Vertex {
|
|
|
698
645
|
}
|
|
699
646
|
if (rawKey === "children") continue;
|
|
700
647
|
let key = rawKey;
|
|
701
|
-
if (rawKey === "name") {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
key = "_n";
|
|
707
|
-
} else if (rawKey === "createdAt") {
|
|
708
|
-
key = "_c";
|
|
648
|
+
if (rawKey === "name" && explicitName !== void 0) {
|
|
649
|
+
console.warn('newNamedChild: "name" in props is ignored because a name argument was provided');
|
|
650
|
+
continue;
|
|
709
651
|
}
|
|
710
652
|
let value = rawValue;
|
|
711
653
|
if (key === "_c") {
|
|
@@ -724,10 +666,8 @@ var Vertex = class _Vertex {
|
|
|
724
666
|
continue;
|
|
725
667
|
}
|
|
726
668
|
} else if (typeof value === "object" && value !== null) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
669
|
+
skipped.push(rawKey);
|
|
670
|
+
continue;
|
|
731
671
|
} else if (!isPrimitive(value)) {
|
|
732
672
|
skipped.push(rawKey);
|
|
733
673
|
continue;
|
|
@@ -927,7 +867,6 @@ var StateVector = class _StateVector {
|
|
|
927
867
|
};
|
|
928
868
|
|
|
929
869
|
// src/RepTree.ts
|
|
930
|
-
var Y2 = __toESM(require("yjs"), 1);
|
|
931
870
|
var _RepTree = class _RepTree {
|
|
932
871
|
/**
|
|
933
872
|
* @param peerId - The peer ID of the current client. Should be unique across all peers.
|
|
@@ -939,7 +878,7 @@ var _RepTree = class _RepTree {
|
|
|
939
878
|
this.setPropertyOps = [];
|
|
940
879
|
this.propertiesAndTheirOpIds = /* @__PURE__ */ new Map();
|
|
941
880
|
this.transientPropertiesAndTheirOpIds = /* @__PURE__ */ new Map();
|
|
942
|
-
|
|
881
|
+
// Observers for non-structural properties are not used
|
|
943
882
|
this.localOps = [];
|
|
944
883
|
this.pendingMovesWithMissingParent = /* @__PURE__ */ new Map();
|
|
945
884
|
this.pendingPropertiesWithMissingVertex = /* @__PURE__ */ new Map();
|
|
@@ -1041,6 +980,12 @@ var _RepTree = class _RepTree {
|
|
|
1041
980
|
this.localOps = [];
|
|
1042
981
|
return ops;
|
|
1043
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* This is the first vertex that will contain all other vertices.
|
|
985
|
+
* If you plan to replicate a tree then don't use this method and instead merge
|
|
986
|
+
* in the ops from another tree (that will also contain the root vertex).
|
|
987
|
+
* @returns The root vertex
|
|
988
|
+
*/
|
|
1044
989
|
createRoot() {
|
|
1045
990
|
if (this.rootVertexId) {
|
|
1046
991
|
throw new Error("Root vertex already exists");
|
|
@@ -1070,7 +1015,7 @@ var _RepTree = class _RepTree {
|
|
|
1070
1015
|
if (typedProps) {
|
|
1071
1016
|
this.setVertexProperties(vertexId, typedProps);
|
|
1072
1017
|
}
|
|
1073
|
-
this.setVertexProperty(vertexId, "
|
|
1018
|
+
this.setVertexProperty(vertexId, "name", name);
|
|
1074
1019
|
const vertex = this.state.getVertex(vertexId);
|
|
1075
1020
|
if (!vertex) {
|
|
1076
1021
|
throw new Error("Failed to create named vertex");
|
|
@@ -1088,21 +1033,15 @@ var _RepTree = class _RepTree {
|
|
|
1088
1033
|
}
|
|
1089
1034
|
setTransientVertexProperty(vertexId, key, value) {
|
|
1090
1035
|
this.lamportClock++;
|
|
1091
|
-
|
|
1092
|
-
if (value instanceof Y2.Doc) {
|
|
1093
|
-
const state = Y2.encodeStateAsUpdate(value);
|
|
1094
|
-
opValue = {
|
|
1095
|
-
type: "yjs",
|
|
1096
|
-
value: state
|
|
1097
|
-
};
|
|
1098
|
-
this.setupYjsObserver(value, vertexId, key);
|
|
1099
|
-
} else {
|
|
1100
|
-
opValue = value;
|
|
1101
|
-
}
|
|
1102
|
-
const op = newSetTransientVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, opValue);
|
|
1036
|
+
const op = newSetTransientVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, value);
|
|
1103
1037
|
this.localOps.push(op);
|
|
1104
1038
|
this.applyProperty(op);
|
|
1105
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Promotes all transient (temporary) properties to persistent properties.
|
|
1042
|
+
* @param vertexId - The ID of the vertex to commit transients for.
|
|
1043
|
+
* @returns
|
|
1044
|
+
*/
|
|
1106
1045
|
commitTransients(vertexId) {
|
|
1107
1046
|
const vertex = this.state.getVertex(vertexId);
|
|
1108
1047
|
if (!vertex) {
|
|
@@ -1119,18 +1058,7 @@ var _RepTree = class _RepTree {
|
|
|
1119
1058
|
}
|
|
1120
1059
|
setVertexProperty(vertexId, key, value) {
|
|
1121
1060
|
this.lamportClock++;
|
|
1122
|
-
|
|
1123
|
-
if (value instanceof Y2.Doc) {
|
|
1124
|
-
const state = Y2.encodeStateAsUpdate(value);
|
|
1125
|
-
opValue = {
|
|
1126
|
-
type: "yjs",
|
|
1127
|
-
value: state
|
|
1128
|
-
};
|
|
1129
|
-
this.setupYjsObserver(value, vertexId, key);
|
|
1130
|
-
} else {
|
|
1131
|
-
opValue = value;
|
|
1132
|
-
}
|
|
1133
|
-
const op = newSetVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, opValue);
|
|
1061
|
+
const op = newSetVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, value);
|
|
1134
1062
|
this.localOps.push(op);
|
|
1135
1063
|
this.applyProperty(op);
|
|
1136
1064
|
}
|
|
@@ -1161,7 +1089,7 @@ var _RepTree = class _RepTree {
|
|
|
1161
1089
|
const targetName = path[0];
|
|
1162
1090
|
const children = this.getChildren(vertex.id);
|
|
1163
1091
|
for (const child of children) {
|
|
1164
|
-
if (child.getProperty("
|
|
1092
|
+
if (child.getProperty("name") === targetName) {
|
|
1165
1093
|
return this.getVertexByPathArray(child, path.slice(1));
|
|
1166
1094
|
}
|
|
1167
1095
|
}
|
|
@@ -1293,13 +1221,7 @@ var _RepTree = class _RepTree {
|
|
|
1293
1221
|
if (!propB) {
|
|
1294
1222
|
return false;
|
|
1295
1223
|
}
|
|
1296
|
-
if (propA.value
|
|
1297
|
-
const snapshotA = Y2.snapshot(propA.value);
|
|
1298
|
-
const snapshotB = Y2.snapshot(propB.value);
|
|
1299
|
-
if (!Y2.equalSnapshots(snapshotA, snapshotB)) {
|
|
1300
|
-
return false;
|
|
1301
|
-
}
|
|
1302
|
-
} else if (propA.value !== propB.value) {
|
|
1224
|
+
if (propA.value !== propB.value) {
|
|
1303
1225
|
return false;
|
|
1304
1226
|
}
|
|
1305
1227
|
}
|
|
@@ -1396,40 +1318,6 @@ var _RepTree = class _RepTree {
|
|
|
1396
1318
|
this.state.setTransientProperty(op.targetId, op.key, op.value);
|
|
1397
1319
|
this.reportOpAsApplied(op);
|
|
1398
1320
|
}
|
|
1399
|
-
setupYjsObserver(doc, vertexId, key) {
|
|
1400
|
-
const propertyKey = `${key}@${vertexId}`;
|
|
1401
|
-
if (this.yjsObservers.has(propertyKey)) {
|
|
1402
|
-
const existingDoc = this.getVertexProperty(vertexId, key);
|
|
1403
|
-
if (existingDoc instanceof Y2.Doc) {
|
|
1404
|
-
existingDoc.off("update", this.yjsObservers.get(propertyKey));
|
|
1405
|
-
}
|
|
1406
|
-
this.yjsObservers.delete(propertyKey);
|
|
1407
|
-
}
|
|
1408
|
-
const ydocObserver = (update, origin, doc2, transaction) => {
|
|
1409
|
-
if (!transaction.local) {
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
const crdtValue = {
|
|
1413
|
-
type: "yjs",
|
|
1414
|
-
value: update
|
|
1415
|
-
};
|
|
1416
|
-
this.lamportClock++;
|
|
1417
|
-
const op = newSetVertexPropertyOp(
|
|
1418
|
-
this.lamportClock,
|
|
1419
|
-
this.peerId,
|
|
1420
|
-
vertexId,
|
|
1421
|
-
key,
|
|
1422
|
-
crdtValue
|
|
1423
|
-
);
|
|
1424
|
-
this.localOps.push(op);
|
|
1425
|
-
this.applyProperty(op);
|
|
1426
|
-
if (this._stateVectorEnabled) {
|
|
1427
|
-
this.stateVector.updateFromOp(op);
|
|
1428
|
-
}
|
|
1429
|
-
};
|
|
1430
|
-
doc.on("update", ydocObserver);
|
|
1431
|
-
this.yjsObservers.set(propertyKey, ydocObserver);
|
|
1432
|
-
}
|
|
1433
1321
|
applyProperty(op) {
|
|
1434
1322
|
const targetVertex = this.state.getVertex(op.targetId);
|
|
1435
1323
|
if (!targetVertex) {
|
|
@@ -1443,11 +1331,7 @@ var _RepTree = class _RepTree {
|
|
|
1443
1331
|
return;
|
|
1444
1332
|
}
|
|
1445
1333
|
this.updateLamportClock(op);
|
|
1446
|
-
|
|
1447
|
-
this.applyModifyProperty(op, targetVertex);
|
|
1448
|
-
} else {
|
|
1449
|
-
this.applyLLWProperty(op, targetVertex);
|
|
1450
|
-
}
|
|
1334
|
+
this.applyLLWProperty(op, targetVertex);
|
|
1451
1335
|
}
|
|
1452
1336
|
applyLLWProperty(op, targetVertex) {
|
|
1453
1337
|
const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
|
|
@@ -1469,28 +1353,7 @@ var _RepTree = class _RepTree {
|
|
|
1469
1353
|
}
|
|
1470
1354
|
}
|
|
1471
1355
|
}
|
|
1472
|
-
|
|
1473
|
-
if (op.transient) {
|
|
1474
|
-
console.warn("Not implemented: transient non LWW property");
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
|
-
this.setPropertyOps.push(op);
|
|
1478
|
-
const crdtValue = op.value;
|
|
1479
|
-
if (crdtValue.type !== "yjs") {
|
|
1480
|
-
throw new Error("Unknown CRDT type");
|
|
1481
|
-
}
|
|
1482
|
-
const ydoc = targetVertex.getProperty(op.key);
|
|
1483
|
-
if (ydoc instanceof Y2.Doc) {
|
|
1484
|
-
Y2.applyUpdate(ydoc, crdtValue.value);
|
|
1485
|
-
} else {
|
|
1486
|
-
const newDoc = new Y2.Doc();
|
|
1487
|
-
this.setupYjsObserver(newDoc, op.targetId, op.key);
|
|
1488
|
-
this.state.setProperty(op.targetId, op.key, newDoc);
|
|
1489
|
-
Y2.applyUpdate(newDoc, crdtValue.value);
|
|
1490
|
-
}
|
|
1491
|
-
this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
|
|
1492
|
-
this.reportOpAsApplied(op);
|
|
1493
|
-
}
|
|
1356
|
+
// Non-LWW modify-property flow removed
|
|
1494
1357
|
applyOperation(op) {
|
|
1495
1358
|
if (isMoveVertexOp(op)) {
|
|
1496
1359
|
this.applyMove(op);
|
|
@@ -1614,10 +1477,7 @@ var RepTree = _RepTree;
|
|
|
1614
1477
|
Vertex,
|
|
1615
1478
|
VertexState,
|
|
1616
1479
|
bindVertex,
|
|
1617
|
-
defaultAliases,
|
|
1618
1480
|
isAnyPropertyOp,
|
|
1619
|
-
isLWWPropertyOp,
|
|
1620
|
-
isModifyPropertyOp,
|
|
1621
1481
|
isMoveVertexOp,
|
|
1622
1482
|
newMoveVertexOp,
|
|
1623
1483
|
newSetTransientVertexPropertyOp,
|