reptree 0.1.1 → 0.1.2

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,11 +8,12 @@ A tree data structure using CRDTs for seamless replication between peers.
8
8
 
9
9
  ## Description
10
10
 
11
- RepTree is a tree data structure for storing vertices with properties.
12
- It uses 2 conflict-free replicated data types (CRDTs) to manage seamless replication between peers:
11
+ RepTree uses 2 conflict-free replicated data types (CRDTs) to manage seamless replication between peers:
13
12
  - A move tree CRDT is used for the tree structure (https://martin.kleppmann.com/papers/move-op.pdf).
14
13
  - A last writer wins (LWW) CRDT is used for properties.
15
14
 
15
+ 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
+
16
17
  ## Installation
17
18
 
18
19
  ```bash
@@ -29,17 +30,44 @@ const tree = new RepTree('peer1');
29
30
 
30
31
  // Root vertex is created automatically
31
32
  const rootVertex = tree.rootVertex;
32
- const rootId = rootVertex.id;
33
-
34
- // Add child vertices
35
- const childVertex = tree.newVertex(rootId);
36
- const childId = childVertex.id;
37
-
38
- // Set properties
39
- tree.setVertexProperty(childId, 'name', 'Child Node');
40
-
41
- // Move vertices
42
- tree.moveVertex(childId, anotherParentId);
33
+ rootVertex.name = 'Project';
34
+
35
+ // Create a folder structure with properties
36
+ const docsFolder = rootVertex.newNamedChild('Docs');
37
+ docsFolder.setProperties({
38
+ type: 'folder',
39
+ icon: 'folder-icon'
40
+ });
41
+
42
+ const imagesFolder = rootVertex.newNamedChild('Images');
43
+ imagesFolder.setProperties({
44
+ type: 'folder',
45
+ icon: 'image-icon'
46
+ });
47
+
48
+ // Add files to folders
49
+ const readmeFile = docsFolder.newNamedChild('README.md');
50
+ readmeFile.setProperties({
51
+ type: 'file',
52
+ size: 2048,
53
+ lastModified: '2023-10-15T14:22:10Z',
54
+ s3Path: 's3://my-bucket/docs/README.md'
55
+ });
56
+
57
+ const logoFile = imagesFolder.newNamedChild('logo.png');
58
+ logoFile.setProperties({
59
+ type: 'file',
60
+ size: 15360,
61
+ dimensions: '512x512',
62
+ format: 'png',
63
+ s3Path: 's3://my-bucket/images/logo.png'
64
+ });
65
+
66
+ // Move a file to a different folder
67
+ logoFile.moveTo(docsFolder);
68
+
69
+ // Get children of a folder
70
+ const docsFolderContents = docsFolder.children;
43
71
 
44
72
  // Syncing between trees
45
73
  const otherTree = new RepTree('peer2');
package/dist/index.cjs CHANGED
@@ -496,6 +496,38 @@ var Vertex = class {
496
496
  };
497
497
 
498
498
  // src/RepTree.ts
499
+ function subtractRanges(rangesA, rangesB) {
500
+ if (rangesB.length === 0) return rangesA.map((r) => [...r]);
501
+ if (rangesA.length === 0) return [];
502
+ const result = [];
503
+ let indexB = 0;
504
+ for (const rangeA of rangesA) {
505
+ let currentStart = rangeA[0];
506
+ const endA = rangeA[1];
507
+ while (indexB < rangesB.length && rangesB[indexB][1] < currentStart) {
508
+ indexB++;
509
+ }
510
+ while (indexB < rangesB.length && rangesB[indexB][0] <= endA) {
511
+ const startB = rangesB[indexB][0];
512
+ const endB = rangesB[indexB][1];
513
+ if (currentStart < startB) {
514
+ result.push([currentStart, Math.min(endA, startB - 1)]);
515
+ }
516
+ currentStart = Math.max(currentStart, endB + 1);
517
+ if (currentStart > endA) break;
518
+ if (endB >= endA) break;
519
+ if (endB < currentStart) {
520
+ indexB++;
521
+ } else if (startB >= currentStart) {
522
+ indexB++;
523
+ }
524
+ }
525
+ if (currentStart <= endA) {
526
+ result.push([currentStart, endA]);
527
+ }
528
+ }
529
+ return result;
530
+ }
499
531
  var _RepTree = class _RepTree {
500
532
  /**
501
533
  * @param peerId - The peer ID of the current client
@@ -510,10 +542,12 @@ var _RepTree = class _RepTree {
510
542
  this.localOps = [];
511
543
  this.pendingMovesWithMissingParent = /* @__PURE__ */ new Map();
512
544
  this.pendingPropertiesWithMissingVertex = /* @__PURE__ */ new Map();
513
- this.appliedOps = /* @__PURE__ */ new Set();
545
+ this.knownOps = /* @__PURE__ */ new Set();
514
546
  this.parentIdBeforeMove = /* @__PURE__ */ new Map();
515
547
  this.opAppliedCallbacks = [];
516
548
  this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
549
+ // State vector tracking operations from each peer
550
+ this.stateVector = {};
517
551
  this.peerId = peerId;
518
552
  this.state = new TreeState();
519
553
  if (ops != null && ops.length > 0) {
@@ -530,10 +564,10 @@ var _RepTree = class _RepTree {
530
564
  throw new Error("The operations has to contain a move operation with a parentId as null to set the root vertex");
531
565
  }
532
566
  this.applyOps(ops);
533
- this.ensureTrashVertex();
567
+ this.ensureNullVertex();
534
568
  } else {
535
569
  this.rootVertexId = this.newVertexInternalWithUUID(null);
536
- this.ensureTrashVertex();
570
+ this.ensureNullVertex();
537
571
  }
538
572
  }
539
573
  getMoveOps() {
@@ -635,7 +669,7 @@ var _RepTree = class _RepTree {
635
669
  this.applyMove(op);
636
670
  }
637
671
  deleteVertex(vertexId) {
638
- this.moveVertex(vertexId, _RepTree.TRASH_VERTEX_ID);
672
+ this.moveVertex(vertexId, _RepTree.NULL_VERTEX_ID);
639
673
  }
640
674
  setTransientVertexProperty(vertexId, key, value) {
641
675
  this.lamportClock++;
@@ -685,6 +719,23 @@ var _RepTree = class _RepTree {
685
719
  merge(ops) {
686
720
  this.applyOps(ops);
687
721
  }
722
+ /** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
723
+ applyOpsOptimizedForLotsOfMoves(ops) {
724
+ const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(op.id.toString()));
725
+ if (newMoveOps.length > 0) {
726
+ const allMoveOps = [...this.moveOps, ...newMoveOps];
727
+ allMoveOps.sort((a, b) => OpId.compare(a.id, b.id));
728
+ for (let i = 0, len = allMoveOps.length; i < len; i++) {
729
+ const op = allMoveOps[i];
730
+ this.applyMove(op);
731
+ }
732
+ }
733
+ const propertyOps = ops.filter((op) => isSetPropertyOp(op) && !this.knownOps.has(op.id.toString()));
734
+ for (let i = 0, len = propertyOps.length; i < len; i++) {
735
+ const op = propertyOps[i];
736
+ this.applyProperty(op);
737
+ }
738
+ }
688
739
  compareStructure(other) {
689
740
  return _RepTree.compareVertices(this.rootVertexId, this, other);
690
741
  }
@@ -795,8 +846,8 @@ var _RepTree = class _RepTree {
795
846
  const vertexId = uuid();
796
847
  return this.newVertexInternal(vertexId, parentId);
797
848
  }
798
- ensureTrashVertex() {
799
- const vertexId = _RepTree.TRASH_VERTEX_ID;
849
+ ensureNullVertex() {
850
+ const vertexId = _RepTree.NULL_VERTEX_ID;
800
851
  if (this.state.getVertex(vertexId)) {
801
852
  return;
802
853
  }
@@ -808,6 +859,19 @@ var _RepTree = class _RepTree {
808
859
  this.lamportClock = operation.id.counter;
809
860
  }
810
861
  }
862
+ applyPendingMovesForParent(parentId) {
863
+ if (!this.state.getVertex(parentId)) {
864
+ return;
865
+ }
866
+ const pendingMoves = this.pendingMovesWithMissingParent.get(parentId);
867
+ if (!pendingMoves) {
868
+ return;
869
+ }
870
+ this.pendingMovesWithMissingParent.delete(parentId);
871
+ for (const pendingOp of pendingMoves) {
872
+ this.applyMove(pendingOp);
873
+ }
874
+ }
811
875
  applyMove(op) {
812
876
  if (op.parentId !== null && !this.state.getVertex(op.parentId)) {
813
877
  if (!this.pendingMovesWithMissingParent.has(op.parentId)) {
@@ -842,15 +906,52 @@ var _RepTree = class _RepTree {
842
906
  }
843
907
  this.applyPendingMovesForParent(op.targetId);
844
908
  }
845
- reportOpAsApplied(op) {
846
- this.appliedOps.add(op.id.toString());
847
- for (const callback of this.opAppliedCallbacks) {
848
- callback(op);
909
+ setPropertyAndItsOpId(op) {
910
+ this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
911
+ this.state.setProperty(op.targetId, op.key, op.value);
912
+ this.reportOpAsApplied(op);
913
+ }
914
+ setTransientPropertyAndItsOpId(op) {
915
+ this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
916
+ this.state.setTransientProperty(op.targetId, op.key, op.value);
917
+ this.reportOpAsApplied(op);
918
+ }
919
+ applyProperty(op) {
920
+ const targetVertex = this.state.getVertex(op.targetId);
921
+ if (!targetVertex) {
922
+ if (op.transient) {
923
+ return;
924
+ }
925
+ if (!this.pendingPropertiesWithMissingVertex.has(op.targetId)) {
926
+ this.pendingPropertiesWithMissingVertex.set(op.targetId, []);
927
+ }
928
+ this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
929
+ return;
930
+ }
931
+ this.updateLamportClock(op);
932
+ const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
933
+ const prevProp = targetVertex.getProperty(op.key);
934
+ const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
935
+ if (!op.transient) {
936
+ this.setPropertyOps.push(op);
937
+ if (!prevProp || !prevOpId || op.id.isGreaterThan(prevOpId)) {
938
+ this.setPropertyAndItsOpId(op);
939
+ } else {
940
+ this.knownOps.add(op.id.toString());
941
+ }
942
+ if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
943
+ this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
944
+ targetVertex.removeTransientProperty(op.key);
945
+ }
946
+ } else {
947
+ if (!prevTransientOpId || op.id.isGreaterThan(prevTransientOpId)) {
948
+ this.setTransientPropertyAndItsOpId(op);
949
+ }
849
950
  }
850
951
  }
851
952
  applyOps(ops) {
852
953
  for (const op of ops) {
853
- if (this.appliedOps.has(op.id.toString())) {
954
+ if (this.knownOps.has(op.id.toString())) {
854
955
  continue;
855
956
  }
856
957
  if (isMoveVertexOp(op)) {
@@ -860,34 +961,11 @@ var _RepTree = class _RepTree {
860
961
  }
861
962
  }
862
963
  }
863
- /** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
864
- applyOpsOptimizedForLotsOfMoves(ops) {
865
- const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.appliedOps.has(op.id.toString()));
866
- if (newMoveOps.length > 0) {
867
- const allMoveOps = [...this.moveOps, ...newMoveOps];
868
- allMoveOps.sort((a, b) => OpId.compare(a.id, b.id));
869
- for (let i = 0, len = allMoveOps.length; i < len; i++) {
870
- const op = allMoveOps[i];
871
- this.applyMove(op);
872
- }
873
- }
874
- const propertyOps = ops.filter((op) => isSetPropertyOp(op) && !this.appliedOps.has(op.id.toString()));
875
- for (let i = 0, len = propertyOps.length; i < len; i++) {
876
- const op = propertyOps[i];
877
- this.applyProperty(op);
878
- }
879
- }
880
- applyPendingMovesForParent(parentId) {
881
- if (!this.state.getVertex(parentId)) {
882
- return;
883
- }
884
- const pendingMoves = this.pendingMovesWithMissingParent.get(parentId);
885
- if (!pendingMoves) {
886
- return;
887
- }
888
- this.pendingMovesWithMissingParent.delete(parentId);
889
- for (const pendingOp of pendingMoves) {
890
- this.applyMove(pendingOp);
964
+ reportOpAsApplied(op) {
965
+ this.knownOps.add(op.id.toString());
966
+ this.updateStateVector(op);
967
+ for (const callback of this.opAppliedCallbacks) {
968
+ callback(op);
891
969
  }
892
970
  }
893
971
  tryToMove(op) {
@@ -900,6 +978,7 @@ var _RepTree = class _RepTree {
900
978
  this.state.moveVertex(op.targetId, op.parentId);
901
979
  if (!targetVertex) {
902
980
  const pendingProperties = this.pendingPropertiesWithMissingVertex.get(op.targetId) || [];
981
+ this.pendingPropertiesWithMissingVertex.delete(op.targetId);
903
982
  for (const prop of pendingProperties) {
904
983
  this.setPropertyAndItsOpId(prop);
905
984
  }
@@ -917,49 +996,125 @@ var _RepTree = class _RepTree {
917
996
  }
918
997
  this.state.moveVertex(op.targetId, prevParentId);
919
998
  }
920
- setPropertyAndItsOpId(op) {
921
- this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
922
- this.state.setProperty(op.targetId, op.key, op.value);
923
- this.reportOpAsApplied(op);
924
- }
925
- setTransientPropertyAndItsOpId(op) {
926
- this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
927
- this.state.setTransientProperty(op.targetId, op.key, op.value);
928
- this.reportOpAsApplied(op);
929
- }
930
- applyProperty(op) {
931
- const targetVertex = this.state.getVertex(op.targetId);
932
- if (!targetVertex) {
933
- if (op.transient) {
934
- return;
999
+ // --- Range-Based State Vector Methods ---
1000
+ /**
1001
+ * Updates the state vector with a newly applied operation.
1002
+ * Assumes ranges are sorted and non-overlapping.
1003
+ *
1004
+ * @param op The operation that was just applied
1005
+ */
1006
+ updateStateVector(op) {
1007
+ const peerId = op.id.peerId;
1008
+ const counter = op.id.counter;
1009
+ if (!this.stateVector[peerId]) {
1010
+ this.stateVector[peerId] = [];
1011
+ }
1012
+ const ranges = this.stateVector[peerId];
1013
+ if (ranges.length === 0) {
1014
+ ranges.push([counter, counter]);
1015
+ return;
1016
+ }
1017
+ let rangeExtendedOrMerged = false;
1018
+ let insertIndex = -1;
1019
+ for (let i = 0; i < ranges.length; i++) {
1020
+ const range = ranges[i];
1021
+ if (counter >= range[0] && counter <= range[1]) {
1022
+ rangeExtendedOrMerged = true;
1023
+ break;
935
1024
  }
936
- if (!this.pendingPropertiesWithMissingVertex.has(op.targetId)) {
937
- this.pendingPropertiesWithMissingVertex.set(op.targetId, []);
1025
+ if (counter === range[0] - 1) {
1026
+ range[0] = counter;
1027
+ rangeExtendedOrMerged = true;
1028
+ if (i > 0 && range[0] === ranges[i - 1][1] + 1) {
1029
+ ranges[i - 1][1] = range[1];
1030
+ ranges.splice(i, 1);
1031
+ }
1032
+ break;
1033
+ }
1034
+ if (counter === range[1] + 1) {
1035
+ range[1] = counter;
1036
+ rangeExtendedOrMerged = true;
1037
+ if (i < ranges.length - 1 && range[1] + 1 === ranges[i + 1][0]) {
1038
+ range[1] = ranges[i + 1][1];
1039
+ ranges.splice(i + 1, 1);
1040
+ }
1041
+ break;
1042
+ }
1043
+ if (counter < range[0] && insertIndex === -1) {
1044
+ insertIndex = i;
938
1045
  }
939
- this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
940
- return;
941
1046
  }
942
- this.updateLamportClock(op);
943
- const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
944
- const prevProp = targetVertex.getProperty(op.key);
945
- const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
946
- if (!op.transient) {
947
- this.setPropertyOps.push(op);
948
- if (!prevProp || !prevOpId || op.id.isGreaterThan(prevOpId)) {
949
- this.setPropertyAndItsOpId(op);
1047
+ if (!rangeExtendedOrMerged) {
1048
+ if (insertIndex === -1) {
1049
+ insertIndex = ranges.length;
950
1050
  }
951
- if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
952
- this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
953
- targetVertex.removeTransientProperty(op.key);
1051
+ ranges.splice(insertIndex, 0, [counter, counter]);
1052
+ if (insertIndex > 0 && ranges[insertIndex][0] === ranges[insertIndex - 1][1] + 1) {
1053
+ ranges[insertIndex - 1][1] = ranges[insertIndex][1];
1054
+ ranges.splice(insertIndex, 1);
1055
+ insertIndex--;
954
1056
  }
955
- } else {
956
- if (!prevTransientOpId || op.id.isGreaterThan(prevTransientOpId)) {
957
- this.setTransientPropertyAndItsOpId(op);
1057
+ if (insertIndex < ranges.length - 1 && ranges[insertIndex][1] + 1 === ranges[insertIndex + 1][0]) {
1058
+ ranges[insertIndex][1] = ranges[insertIndex + 1][1];
1059
+ ranges.splice(insertIndex + 1, 1);
1060
+ }
1061
+ }
1062
+ }
1063
+ /**
1064
+ * Returns the current state vector.
1065
+ * Returns a readonly reference to the internal state vector.
1066
+ */
1067
+ getStateVector() {
1068
+ return this.stateVector;
1069
+ }
1070
+ /**
1071
+ * Calculates which operation ranges we have that the other peer is missing
1072
+ * by comparing state vectors.
1073
+ *
1074
+ * @param theirStateVector The state vector from another peer
1075
+ * @returns Array of operation ID ranges that we have but they don't
1076
+ */
1077
+ diffStateVectors(theirStateVector) {
1078
+ const missingRanges = [];
1079
+ for (const [peerId, ourRanges] of Object.entries(this.stateVector)) {
1080
+ const theirRanges = theirStateVector[peerId] || [];
1081
+ const missing = subtractRanges(ourRanges, theirRanges);
1082
+ for (const [start, end] of missing) {
1083
+ if (start <= end) {
1084
+ missingRanges.push({
1085
+ peerId,
1086
+ start,
1087
+ end
1088
+ });
1089
+ }
1090
+ }
1091
+ }
1092
+ return missingRanges;
1093
+ }
1094
+ /**
1095
+ * Determines which operations are needed to synchronize
1096
+ * with the provided state vector.
1097
+ *
1098
+ * @param theirStateVector The state vector from another peer
1099
+ * @returns Operations that should be sent to the other peer, sorted by OpId.
1100
+ */
1101
+ getMissingOps(theirStateVector) {
1102
+ const missingRanges = this.diffStateVectors(theirStateVector);
1103
+ const missingOps = [];
1104
+ const allOps = [...this.moveOps, ...this.setPropertyOps];
1105
+ for (const op of allOps) {
1106
+ for (const range of missingRanges) {
1107
+ if (op.id.peerId === range.peerId && op.id.counter >= range.start && op.id.counter <= range.end) {
1108
+ missingOps.push(op);
1109
+ break;
1110
+ }
958
1111
  }
959
1112
  }
1113
+ missingOps.sort((a, b) => OpId.compare(a.id, b.id));
1114
+ return missingOps;
960
1115
  }
961
1116
  };
962
- _RepTree.TRASH_VERTEX_ID = "t";
1117
+ _RepTree.NULL_VERTEX_ID = "0";
963
1118
  _RepTree.DEFAULT_MAX_DEPTH = 1e5;
964
1119
  var RepTree = _RepTree;
965
1120
  // Annotate the CommonJS export names for ESM import in node: