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 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 2 conflict-free replicated data types (CRDTs) to manage seamless replication between peers:
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 isSetPropertyOp(op) {
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
- const op = newSetTransientVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, value);
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
- const op = newSetVertexPropertyOp(this.lamportClock, this.peerId, vertexId, key, value);
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) => isSetPropertyOp(op) && !this.knownOps.has(op.id.toString()));
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
- let depth = 0;
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 (depth > this.maxDepth) {
953
- console.error(`isAncestor: max depth of ${this.maxDepth} reached. Perhaps, we have an infinite loop here.`);
954
- return true;
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 || propA.value !== propB.value) {
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
- setPropertyAndItsOpId(op) {
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 (!prevProp || !prevOpId || op.id.isGreaterThan(prevOpId)) {
1127
- this.setPropertyAndItsOpId(op);
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
- applyOps(ops) {
1142
- for (const op of ops) {
1143
- if (this.knownOps.has(op.id.toString())) {
1144
- continue;
1145
- }
1146
- if (isMoveVertexOp(op)) {
1147
- this.applyMove(op);
1148
- } else if (isSetPropertyOp(op)) {
1149
- this.applyProperty(op);
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,