reptree 0.1.5 → 0.2.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.
- package/README.md +47 -1
- package/dist/index.cjs +151 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -11
- package/dist/index.d.ts +36 -11
- package/dist/index.js +138 -30
- package/dist/index.js.map +1 -1
- package/package.json +5 -9
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@ A tree data structure using CRDTs for seamless replication between peers.
|
|
|
8
8
|
|
|
9
9
|
## Description
|
|
10
10
|
|
|
11
|
-
RepTree uses
|
|
11
|
+
RepTree uses multiple conflict-free replicated data types (CRDTs) to manage seamless replication between peers:
|
|
12
12
|
- A move tree CRDT is used for the tree structure (https://martin.kleppmann.com/papers/move-op.pdf).
|
|
13
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).
|
|
14
15
|
|
|
15
16
|
RepTree can also be viewed as a hierarchical, distributed database. For more details on its database capabilities, see [RepTree as a Database](docs/database.md).
|
|
16
17
|
|
|
@@ -73,6 +74,51 @@ const ops = tree.getAllOps();
|
|
|
73
74
|
otherTree.merge(ops);
|
|
74
75
|
```
|
|
75
76
|
|
|
77
|
+
## Yjs Integration
|
|
78
|
+
|
|
79
|
+
RepTree supports [Yjs](https://github.com/yjs/yjs) documents as vertex properties, enabling real-time collaborative editing with a variety of shared data types:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { RepTree } from 'reptree';
|
|
83
|
+
import * as Y from 'yjs';
|
|
84
|
+
|
|
85
|
+
// Create a tree with a root vertex
|
|
86
|
+
const tree = new RepTree('peer1');
|
|
87
|
+
const root = tree.createRoot();
|
|
88
|
+
|
|
89
|
+
// Create a Yjs document
|
|
90
|
+
const ydoc = new Y.Doc();
|
|
91
|
+
const ytext = ydoc.getText('default');
|
|
92
|
+
ytext.insert(0, 'Hello world');
|
|
93
|
+
|
|
94
|
+
// Set the Yjs document as a property
|
|
95
|
+
root.setProperty('content', ydoc);
|
|
96
|
+
|
|
97
|
+
// Later, retrieve and modify the document
|
|
98
|
+
const retrievedDoc = root.getProperty('content') as Y.Doc;
|
|
99
|
+
retrievedDoc.getText('default').insert(retrievedDoc.getText('default').length, '!');
|
|
100
|
+
|
|
101
|
+
// Sync operations with another tree
|
|
102
|
+
const tree2 = new RepTree('peer2');
|
|
103
|
+
tree2.merge(tree.popLocalOps());
|
|
104
|
+
|
|
105
|
+
// Both trees now have the same Yjs document content
|
|
106
|
+
const root2 = tree2.root;
|
|
107
|
+
const doc2 = root2.getProperty('content') as Y.Doc;
|
|
108
|
+
console.log(doc2.getText('default').toString()); // 'Hello world!'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This integration allows for:
|
|
112
|
+
- Collaborative editing with multiple shared data types:
|
|
113
|
+
- **Y.Text** - For rich text editing with formatting attributes
|
|
114
|
+
- **Y.Array** - For ordered collections of data
|
|
115
|
+
- **Y.Map** - For key-value pairs and structured data
|
|
116
|
+
- **Y.XmlFragment/Y.XmlElement** - For XML-like structured content
|
|
117
|
+
- Complex nested data structures (arrays within maps, maps within arrays, etc.)
|
|
118
|
+
- Automatic CRDT synchronization between peers
|
|
119
|
+
- Conflict-free concurrent editing
|
|
120
|
+
- Integration with existing Yjs ecosystem (editors, frameworks, etc.)
|
|
121
|
+
|
|
76
122
|
## License
|
|
77
123
|
|
|
78
124
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -26,8 +36,10 @@ __export(index_exports, {
|
|
|
26
36
|
TreeState: () => TreeState,
|
|
27
37
|
Vertex: () => Vertex,
|
|
28
38
|
VertexState: () => VertexState,
|
|
39
|
+
isAnyPropertyOp: () => isAnyPropertyOp,
|
|
40
|
+
isLWWPropertyOp: () => isLWWPropertyOp,
|
|
41
|
+
isModifyPropertyOp: () => isModifyPropertyOp,
|
|
29
42
|
isMoveVertexOp: () => isMoveVertexOp,
|
|
30
|
-
isSetPropertyOp: () => isSetPropertyOp,
|
|
31
43
|
newMoveVertexOp: () => newMoveVertexOp,
|
|
32
44
|
newSetTransientVertexPropertyOp: () => newSetTransientVertexPropertyOp,
|
|
33
45
|
newSetVertexPropertyOp: () => newSetVertexPropertyOp,
|
|
@@ -89,9 +101,15 @@ var OpId = class _OpId {
|
|
|
89
101
|
function isMoveVertexOp(op) {
|
|
90
102
|
return "parentId" in op;
|
|
91
103
|
}
|
|
92
|
-
function
|
|
104
|
+
function isAnyPropertyOp(op) {
|
|
93
105
|
return "key" in op;
|
|
94
106
|
}
|
|
107
|
+
function isLWWPropertyOp(op) {
|
|
108
|
+
return "key" in op && "value" in op && (!op.value || typeof op.value !== "object" || !("type" in op.value));
|
|
109
|
+
}
|
|
110
|
+
function isModifyPropertyOp(op) {
|
|
111
|
+
return "key" in op && "value" in op && typeof op.value === "object" && op.value !== null && "type" in op.value;
|
|
112
|
+
}
|
|
95
113
|
function newMoveVertexOp(clock, peerId, targetId, parentId) {
|
|
96
114
|
return { id: new OpId(clock, peerId), targetId, parentId };
|
|
97
115
|
}
|
|
@@ -694,6 +712,7 @@ var StateVector = class _StateVector {
|
|
|
694
712
|
};
|
|
695
713
|
|
|
696
714
|
// src/RepTree.ts
|
|
715
|
+
var Y = __toESM(require("yjs"), 1);
|
|
697
716
|
var _RepTree = class _RepTree {
|
|
698
717
|
/**
|
|
699
718
|
* @param peerId - The peer ID of the current client. Should be unique across all peers.
|
|
@@ -705,13 +724,13 @@ var _RepTree = class _RepTree {
|
|
|
705
724
|
this.setPropertyOps = [];
|
|
706
725
|
this.propertiesAndTheirOpIds = /* @__PURE__ */ new Map();
|
|
707
726
|
this.transientPropertiesAndTheirOpIds = /* @__PURE__ */ new Map();
|
|
727
|
+
this.yjsObservers = /* @__PURE__ */ new Map();
|
|
708
728
|
this.localOps = [];
|
|
709
729
|
this.pendingMovesWithMissingParent = /* @__PURE__ */ new Map();
|
|
710
730
|
this.pendingPropertiesWithMissingVertex = /* @__PURE__ */ new Map();
|
|
711
731
|
this.knownOps = /* @__PURE__ */ new Set();
|
|
712
732
|
this.parentIdBeforeMove = /* @__PURE__ */ new Map();
|
|
713
733
|
this.opAppliedCallbacks = [];
|
|
714
|
-
this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
|
|
715
734
|
this._stateVectorEnabled = true;
|
|
716
735
|
this.peerId = peerId;
|
|
717
736
|
this.state = new TreeState();
|
|
@@ -743,6 +762,9 @@ var _RepTree = class _RepTree {
|
|
|
743
762
|
}
|
|
744
763
|
return new Vertex(this, rootVertex);
|
|
745
764
|
}
|
|
765
|
+
replicate(newPeerId) {
|
|
766
|
+
return new _RepTree(newPeerId, this.getAllOps());
|
|
767
|
+
}
|
|
746
768
|
getMoveOps() {
|
|
747
769
|
return this.moveOps;
|
|
748
770
|
}
|
|
@@ -795,14 +817,15 @@ var _RepTree = class _RepTree {
|
|
|
795
817
|
}
|
|
796
818
|
return vertex.getAllProperties();
|
|
797
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Returns all local operations and clears the local operations list.
|
|
822
|
+
* Can be used to get all operations that were generated from this peer and need to be sent to other peers.
|
|
823
|
+
*/
|
|
798
824
|
popLocalOps() {
|
|
799
825
|
const ops = this.localOps;
|
|
800
826
|
this.localOps = [];
|
|
801
827
|
return ops;
|
|
802
828
|
}
|
|
803
|
-
setMaxDepth(maxDepth) {
|
|
804
|
-
this.maxDepth = maxDepth;
|
|
805
|
-
}
|
|
806
829
|
createRoot() {
|
|
807
830
|
if (this.rootVertexId) {
|
|
808
831
|
throw new Error("Root vertex already exists");
|
|
@@ -850,13 +873,35 @@ var _RepTree = class _RepTree {
|
|
|
850
873
|
}
|
|
851
874
|
setTransientVertexProperty(vertexId, key, value) {
|
|
852
875
|
this.lamportClock++;
|
|
853
|
-
|
|
876
|
+
let opValue;
|
|
877
|
+
if (value instanceof Y.Doc) {
|
|
878
|
+
const state = Y.encodeStateAsUpdate(value);
|
|
879
|
+
opValue = {
|
|
880
|
+
type: "yjs",
|
|
881
|
+
value: state
|
|
882
|
+
};
|
|
883
|
+
this.setupYjsObserver(value, vertexId, key);
|
|
884
|
+
} else {
|
|
885
|
+
opValue = value;
|
|
886
|
+
}
|
|
887
|
+
const op = newSetTransientVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, opValue);
|
|
854
888
|
this.localOps.push(op);
|
|
855
889
|
this.applyProperty(op);
|
|
856
890
|
}
|
|
857
891
|
setVertexProperty(vertexId, key, value) {
|
|
858
892
|
this.lamportClock++;
|
|
859
|
-
|
|
893
|
+
let opValue;
|
|
894
|
+
if (value instanceof Y.Doc) {
|
|
895
|
+
const state = Y.encodeStateAsUpdate(value);
|
|
896
|
+
opValue = {
|
|
897
|
+
type: "yjs",
|
|
898
|
+
value: state
|
|
899
|
+
};
|
|
900
|
+
this.setupYjsObserver(value, vertexId, key);
|
|
901
|
+
} else {
|
|
902
|
+
opValue = value;
|
|
903
|
+
}
|
|
904
|
+
const op = newSetVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, opValue);
|
|
860
905
|
this.localOps.push(op);
|
|
861
906
|
this.applyProperty(op);
|
|
862
907
|
}
|
|
@@ -902,6 +947,14 @@ var _RepTree = class _RepTree {
|
|
|
902
947
|
merge(ops) {
|
|
903
948
|
this.applyOps(ops);
|
|
904
949
|
}
|
|
950
|
+
applyOps(ops) {
|
|
951
|
+
for (const op of ops) {
|
|
952
|
+
if (this.knownOps.has(op.id.toString())) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
this.applyOperation(op);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
905
958
|
/** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
|
|
906
959
|
applyOpsOptimizedForLotsOfMoves(ops) {
|
|
907
960
|
const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(op.id.toString()));
|
|
@@ -913,7 +966,7 @@ var _RepTree = class _RepTree {
|
|
|
913
966
|
this.applyMove(op);
|
|
914
967
|
}
|
|
915
968
|
}
|
|
916
|
-
const propertyOps = ops.filter((op) =>
|
|
969
|
+
const propertyOps = ops.filter((op) => isAnyPropertyOp(op) && !this.knownOps.has(op.id.toString()));
|
|
917
970
|
for (let i = 0, len = propertyOps.length; i < len; i++) {
|
|
918
971
|
const op = propertyOps[i];
|
|
919
972
|
this.applyProperty(op);
|
|
@@ -945,16 +998,16 @@ var _RepTree = class _RepTree {
|
|
|
945
998
|
isAncestor(childId, ancestorId) {
|
|
946
999
|
let targetId = childId;
|
|
947
1000
|
let vertex;
|
|
948
|
-
|
|
1001
|
+
const visitedVertices = /* @__PURE__ */ new Set();
|
|
949
1002
|
while (vertex = this.state.getVertex(targetId)) {
|
|
950
1003
|
if (vertex.parentId === ancestorId) return true;
|
|
951
1004
|
if (!vertex.parentId) return false;
|
|
952
|
-
if (
|
|
953
|
-
console.error(`isAncestor:
|
|
954
|
-
return
|
|
1005
|
+
if (visitedVertices.has(targetId)) {
|
|
1006
|
+
console.error(`isAncestor: cycle detected in the tree structure.`);
|
|
1007
|
+
return false;
|
|
955
1008
|
}
|
|
1009
|
+
visitedVertices.add(targetId);
|
|
956
1010
|
targetId = vertex.parentId;
|
|
957
|
-
depth++;
|
|
958
1011
|
}
|
|
959
1012
|
return false;
|
|
960
1013
|
}
|
|
@@ -1008,7 +1061,16 @@ var _RepTree = class _RepTree {
|
|
|
1008
1061
|
}
|
|
1009
1062
|
for (const propA of propertiesA) {
|
|
1010
1063
|
const propB = propertiesB.find((p) => p.key === propA.key);
|
|
1011
|
-
if (!propB
|
|
1064
|
+
if (!propB) {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
if (propA.value instanceof Y.Doc && propB.value instanceof Y.Doc) {
|
|
1068
|
+
const snapshotA = Y.snapshot(propA.value);
|
|
1069
|
+
const snapshotB = Y.snapshot(propB.value);
|
|
1070
|
+
if (!Y.equalSnapshots(snapshotA, snapshotB)) {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
} else if (propA.value !== propB.value) {
|
|
1012
1074
|
return false;
|
|
1013
1075
|
}
|
|
1014
1076
|
}
|
|
@@ -1095,7 +1157,7 @@ var _RepTree = class _RepTree {
|
|
|
1095
1157
|
}
|
|
1096
1158
|
this.applyPendingMovesForParent(op.targetId);
|
|
1097
1159
|
}
|
|
1098
|
-
|
|
1160
|
+
setLLWPropertyAndItsOpId(op) {
|
|
1099
1161
|
this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
|
|
1100
1162
|
this.state.setProperty(op.targetId, op.key, op.value);
|
|
1101
1163
|
this.reportOpAsApplied(op);
|
|
@@ -1105,6 +1167,40 @@ var _RepTree = class _RepTree {
|
|
|
1105
1167
|
this.state.setTransientProperty(op.targetId, op.key, op.value);
|
|
1106
1168
|
this.reportOpAsApplied(op);
|
|
1107
1169
|
}
|
|
1170
|
+
setupYjsObserver(doc, vertexId, key) {
|
|
1171
|
+
const propertyKey = `${key}@${vertexId}`;
|
|
1172
|
+
if (this.yjsObservers.has(propertyKey)) {
|
|
1173
|
+
const existingDoc = this.getVertexProperty(vertexId, key);
|
|
1174
|
+
if (existingDoc instanceof Y.Doc) {
|
|
1175
|
+
existingDoc.off("update", this.yjsObservers.get(propertyKey));
|
|
1176
|
+
}
|
|
1177
|
+
this.yjsObservers.delete(propertyKey);
|
|
1178
|
+
}
|
|
1179
|
+
const ydocObserver = (update, origin, doc2, transaction) => {
|
|
1180
|
+
if (!transaction.local) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const crdtValue = {
|
|
1184
|
+
type: "yjs",
|
|
1185
|
+
value: update
|
|
1186
|
+
};
|
|
1187
|
+
this.lamportClock++;
|
|
1188
|
+
const op = newSetVertexPropertyOp(
|
|
1189
|
+
this.lamportClock,
|
|
1190
|
+
this.peerId,
|
|
1191
|
+
vertexId,
|
|
1192
|
+
key,
|
|
1193
|
+
crdtValue
|
|
1194
|
+
);
|
|
1195
|
+
this.localOps.push(op);
|
|
1196
|
+
this.applyProperty(op);
|
|
1197
|
+
if (this._stateVectorEnabled) {
|
|
1198
|
+
this.stateVector.updateFromOp(op);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
doc.on("update", ydocObserver);
|
|
1202
|
+
this.yjsObservers.set(propertyKey, ydocObserver);
|
|
1203
|
+
}
|
|
1108
1204
|
applyProperty(op) {
|
|
1109
1205
|
const targetVertex = this.state.getVertex(op.targetId);
|
|
1110
1206
|
if (!targetVertex) {
|
|
@@ -1118,13 +1214,19 @@ var _RepTree = class _RepTree {
|
|
|
1118
1214
|
return;
|
|
1119
1215
|
}
|
|
1120
1216
|
this.updateLamportClock(op);
|
|
1217
|
+
if (isModifyPropertyOp(op)) {
|
|
1218
|
+
this.applyModifyProperty(op, targetVertex);
|
|
1219
|
+
} else {
|
|
1220
|
+
this.applyLLWProperty(op, targetVertex);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
applyLLWProperty(op, targetVertex) {
|
|
1121
1224
|
const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
|
|
1122
|
-
const prevProp = targetVertex.getProperty(op.key);
|
|
1123
1225
|
const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
|
|
1124
1226
|
if (!op.transient) {
|
|
1125
1227
|
this.setPropertyOps.push(op);
|
|
1126
|
-
if (!
|
|
1127
|
-
this.
|
|
1228
|
+
if (!prevOpId || op.id.isGreaterThan(prevOpId)) {
|
|
1229
|
+
this.setLLWPropertyAndItsOpId(op);
|
|
1128
1230
|
} else {
|
|
1129
1231
|
this.knownOps.add(op.id.toString());
|
|
1130
1232
|
}
|
|
@@ -1138,16 +1240,33 @@ var _RepTree = class _RepTree {
|
|
|
1138
1240
|
}
|
|
1139
1241
|
}
|
|
1140
1242
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1243
|
+
applyModifyProperty(op, targetVertex) {
|
|
1244
|
+
if (op.transient) {
|
|
1245
|
+
console.warn("Not implemented: transient non LWW property");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
this.setPropertyOps.push(op);
|
|
1249
|
+
const crdtValue = op.value;
|
|
1250
|
+
if (crdtValue.type !== "yjs") {
|
|
1251
|
+
throw new Error("Unknown CRDT type");
|
|
1252
|
+
}
|
|
1253
|
+
const ydoc = targetVertex.getProperty(op.key);
|
|
1254
|
+
if (ydoc instanceof Y.Doc) {
|
|
1255
|
+
Y.applyUpdate(ydoc, crdtValue.value);
|
|
1256
|
+
} else {
|
|
1257
|
+
const newDoc = new Y.Doc();
|
|
1258
|
+
this.setupYjsObserver(newDoc, op.targetId, op.key);
|
|
1259
|
+
this.state.setProperty(op.targetId, op.key, newDoc);
|
|
1260
|
+
Y.applyUpdate(newDoc, crdtValue.value);
|
|
1261
|
+
}
|
|
1262
|
+
this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
|
|
1263
|
+
this.reportOpAsApplied(op);
|
|
1264
|
+
}
|
|
1265
|
+
applyOperation(op) {
|
|
1266
|
+
if (isMoveVertexOp(op)) {
|
|
1267
|
+
this.applyMove(op);
|
|
1268
|
+
} else if (isAnyPropertyOp(op)) {
|
|
1269
|
+
this.applyProperty(op);
|
|
1151
1270
|
}
|
|
1152
1271
|
}
|
|
1153
1272
|
reportOpAsApplied(op) {
|
|
@@ -1246,7 +1365,6 @@ var _RepTree = class _RepTree {
|
|
|
1246
1365
|
}
|
|
1247
1366
|
};
|
|
1248
1367
|
_RepTree.NULL_VERTEX_ID = "0";
|
|
1249
|
-
_RepTree.DEFAULT_MAX_DEPTH = 1e5;
|
|
1250
1368
|
var RepTree = _RepTree;
|
|
1251
1369
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1252
1370
|
0 && (module.exports = {
|
|
@@ -1256,8 +1374,10 @@ var RepTree = _RepTree;
|
|
|
1256
1374
|
TreeState,
|
|
1257
1375
|
Vertex,
|
|
1258
1376
|
VertexState,
|
|
1377
|
+
isAnyPropertyOp,
|
|
1378
|
+
isLWWPropertyOp,
|
|
1379
|
+
isModifyPropertyOp,
|
|
1259
1380
|
isMoveVertexOp,
|
|
1260
|
-
isSetPropertyOp,
|
|
1261
1381
|
newMoveVertexOp,
|
|
1262
1382
|
newSetTransientVertexPropertyOp,
|
|
1263
1383
|
newSetVertexPropertyOp,
|