reptree 0.1.1 → 0.1.3

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
@@ -28,18 +29,45 @@ import { RepTree } from 'reptree';
28
29
  const tree = new RepTree('peer1');
29
30
 
30
31
  // Root vertex is created automatically
31
- 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);
32
+ const rootVertex = tree.createRoot();
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
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  OpId: () => OpId,
24
24
  RepTree: () => RepTree,
25
+ StateVector: () => StateVector,
25
26
  TreeState: () => TreeState,
26
27
  Vertex: () => Vertex,
27
28
  VertexState: () => VertexState,
@@ -495,6 +496,191 @@ var Vertex = class {
495
496
  }
496
497
  };
497
498
 
499
+ // src/StateVector.ts
500
+ function subtractRanges(rangesA, rangesB) {
501
+ if (rangesB.length === 0) return rangesA.map((r) => [...r]);
502
+ if (rangesA.length === 0) return [];
503
+ const result = [];
504
+ let indexB = 0;
505
+ for (const rangeA of rangesA) {
506
+ let currentStart = rangeA[0];
507
+ const endA = rangeA[1];
508
+ while (indexB < rangesB.length && rangesB[indexB][1] < currentStart) {
509
+ indexB++;
510
+ }
511
+ while (indexB < rangesB.length && rangesB[indexB][0] <= endA) {
512
+ const startB = rangesB[indexB][0];
513
+ const endB = rangesB[indexB][1];
514
+ if (currentStart < startB) {
515
+ result.push([currentStart, Math.min(endA, startB - 1)]);
516
+ }
517
+ currentStart = Math.max(currentStart, endB + 1);
518
+ if (currentStart > endA) break;
519
+ if (endB >= endA) break;
520
+ if (endB < currentStart) {
521
+ indexB++;
522
+ } else if (startB >= currentStart) {
523
+ indexB++;
524
+ }
525
+ }
526
+ if (currentStart <= endA) {
527
+ result.push([currentStart, endA]);
528
+ }
529
+ }
530
+ return result;
531
+ }
532
+ var StateVector = class _StateVector {
533
+ /**
534
+ * Creates a new StateVector.
535
+ * @param initialState Optional initial state to copy from
536
+ */
537
+ constructor(initialState = {}) {
538
+ this.ranges = {};
539
+ for (const [peerId, peerRanges] of Object.entries(initialState)) {
540
+ this.ranges[peerId] = peerRanges.map((range) => [...range]);
541
+ }
542
+ }
543
+ /**
544
+ * Updates the state vector with a newly applied operation.
545
+ * Assumes ranges are sorted and non-overlapping.
546
+ *
547
+ * @param peerId The peer ID of the operation
548
+ * @param counter The counter value of the operation
549
+ */
550
+ update(peerId, counter) {
551
+ if (!this.ranges[peerId]) {
552
+ this.ranges[peerId] = [];
553
+ }
554
+ const ranges = this.ranges[peerId];
555
+ if (ranges.length === 0) {
556
+ ranges.push([counter, counter]);
557
+ return;
558
+ }
559
+ let rangeExtendedOrMerged = false;
560
+ let insertIndex = -1;
561
+ for (let i = 0; i < ranges.length; i++) {
562
+ const range = ranges[i];
563
+ if (counter >= range[0] && counter <= range[1]) {
564
+ rangeExtendedOrMerged = true;
565
+ break;
566
+ }
567
+ if (counter === range[0] - 1) {
568
+ range[0] = counter;
569
+ rangeExtendedOrMerged = true;
570
+ if (i > 0 && range[0] === ranges[i - 1][1] + 1) {
571
+ ranges[i - 1][1] = range[1];
572
+ ranges.splice(i, 1);
573
+ }
574
+ break;
575
+ }
576
+ if (counter === range[1] + 1) {
577
+ range[1] = counter;
578
+ rangeExtendedOrMerged = true;
579
+ if (i < ranges.length - 1 && range[1] + 1 === ranges[i + 1][0]) {
580
+ range[1] = ranges[i + 1][1];
581
+ ranges.splice(i + 1, 1);
582
+ }
583
+ break;
584
+ }
585
+ if (counter < range[0] && insertIndex === -1) {
586
+ insertIndex = i;
587
+ }
588
+ }
589
+ if (!rangeExtendedOrMerged) {
590
+ if (insertIndex === -1) {
591
+ insertIndex = ranges.length;
592
+ }
593
+ ranges.splice(insertIndex, 0, [counter, counter]);
594
+ if (insertIndex > 0 && ranges[insertIndex][0] === ranges[insertIndex - 1][1] + 1) {
595
+ ranges[insertIndex - 1][1] = ranges[insertIndex][1];
596
+ ranges.splice(insertIndex, 1);
597
+ insertIndex--;
598
+ }
599
+ if (insertIndex < ranges.length - 1 && ranges[insertIndex][1] + 1 === ranges[insertIndex + 1][0]) {
600
+ ranges[insertIndex][1] = ranges[insertIndex + 1][1];
601
+ ranges.splice(insertIndex + 1, 1);
602
+ }
603
+ }
604
+ }
605
+ /**
606
+ * Updates the state vector with a newly applied operation.
607
+ *
608
+ * @param op The operation that was just applied
609
+ */
610
+ updateFromOp(op) {
611
+ this.update(op.id.peerId, op.id.counter);
612
+ }
613
+ /**
614
+ * Returns the current state vector.
615
+ * Returns a readonly reference to the internal state.
616
+ */
617
+ getState() {
618
+ return this.ranges;
619
+ }
620
+ /**
621
+ * Calculates which operation ranges we have that the other state vector is missing
622
+ * by comparing state vectors.
623
+ *
624
+ * @param other The other state vector to compare against
625
+ * @returns Array of operation ID ranges that we have but they don't
626
+ */
627
+ diff(other) {
628
+ const missingRanges = [];
629
+ const theirState = other.getState();
630
+ for (const [peerId, ourRanges] of Object.entries(this.ranges)) {
631
+ const theirRanges = theirState[peerId] || [];
632
+ const missing = subtractRanges(ourRanges, theirRanges);
633
+ for (const [start, end] of missing) {
634
+ if (start <= end) {
635
+ missingRanges.push({
636
+ peerId,
637
+ start,
638
+ end
639
+ });
640
+ }
641
+ }
642
+ }
643
+ return missingRanges;
644
+ }
645
+ /**
646
+ * Checks if the state vector contains the given operation ID
647
+ *
648
+ * @param opId The operation ID to check
649
+ * @returns true if the operation is in the state vector, false otherwise
650
+ */
651
+ contains(opId) {
652
+ const peerId = opId.peerId;
653
+ const counter = opId.counter;
654
+ if (!this.ranges[peerId]) {
655
+ return false;
656
+ }
657
+ for (const [start, end] of this.ranges[peerId]) {
658
+ if (counter >= start && counter <= end) {
659
+ return true;
660
+ }
661
+ }
662
+ return false;
663
+ }
664
+ /**
665
+ * Creates a copy of this state vector
666
+ */
667
+ clone() {
668
+ return new _StateVector(this.ranges);
669
+ }
670
+ /**
671
+ * Builds a state vector from an array of operations
672
+ * @param operations The operations to build the state vector from
673
+ * @returns A new StateVector instance
674
+ */
675
+ static fromOperations(operations) {
676
+ const stateVector = new _StateVector();
677
+ for (const op of operations) {
678
+ stateVector.updateFromOp(op);
679
+ }
680
+ return stateVector;
681
+ }
682
+ };
683
+
498
684
  // src/RepTree.ts
499
685
  var _RepTree = class _RepTree {
500
686
  /**
@@ -510,31 +696,40 @@ var _RepTree = class _RepTree {
510
696
  this.localOps = [];
511
697
  this.pendingMovesWithMissingParent = /* @__PURE__ */ new Map();
512
698
  this.pendingPropertiesWithMissingVertex = /* @__PURE__ */ new Map();
513
- this.appliedOps = /* @__PURE__ */ new Set();
699
+ this.knownOps = /* @__PURE__ */ new Set();
514
700
  this.parentIdBeforeMove = /* @__PURE__ */ new Map();
515
701
  this.opAppliedCallbacks = [];
516
702
  this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
703
+ this._stateVectorEnabled = true;
517
704
  this.peerId = peerId;
518
705
  this.state = new TreeState();
706
+ this.stateVector = new StateVector();
519
707
  if (ops != null && ops.length > 0) {
520
- let rootMoveOp;
521
- for (let i = 0; i < ops.length; i++) {
522
- if (isMoveVertexOp(ops[i]) && ops[i].parentId === null) {
523
- rootMoveOp = ops[i];
524
- break;
525
- }
526
- }
527
- if (rootMoveOp) {
528
- this.rootVertexId = rootMoveOp.targetId;
529
- } else {
530
- throw new Error("The operations has to contain a move operation with a parentId as null to set the root vertex");
531
- }
532
708
  this.applyOps(ops);
533
- this.ensureTrashVertex();
709
+ const root = this.root;
710
+ if (!root) {
711
+ throw new Error("There has to be a root vertex in the operations");
712
+ }
534
713
  } else {
535
- this.rootVertexId = this.newVertexInternalWithUUID(null);
536
- this.ensureTrashVertex();
714
+ this.ensureNullVertex();
715
+ }
716
+ }
717
+ get root() {
718
+ if (!this.rootVertexId) {
719
+ const vertices = this.state.getAllVertices();
720
+ for (const vertex of vertices) {
721
+ if (vertex.parentId === null && vertex.id !== _RepTree.NULL_VERTEX_ID) {
722
+ this.rootVertexId = vertex.id;
723
+ return new Vertex(this, vertex);
724
+ }
725
+ }
726
+ return void 0;
727
+ }
728
+ const rootVertex = this.state.getVertex(this.rootVertexId);
729
+ if (!rootVertex) {
730
+ throw new Error("Root vertex not found");
537
731
  }
732
+ return new Vertex(this, rootVertex);
538
733
  }
539
734
  getMoveOps() {
540
735
  return this.moveOps;
@@ -546,13 +741,6 @@ var _RepTree = class _RepTree {
546
741
  const vertex = this.state.getVertex(vertexId);
547
742
  return vertex ? new Vertex(this, vertex) : void 0;
548
743
  }
549
- get rootVertex() {
550
- const rootVertex = this.state.getVertex(this.rootVertexId);
551
- if (!rootVertex) {
552
- throw new Error("Root vertex not found");
553
- }
554
- return new Vertex(this, rootVertex);
555
- }
556
744
  getAllVertices() {
557
745
  return this.state.getAllVertices().map((v) => new Vertex(this, v));
558
746
  }
@@ -603,6 +791,17 @@ var _RepTree = class _RepTree {
603
791
  setMaxDepth(maxDepth) {
604
792
  this.maxDepth = maxDepth;
605
793
  }
794
+ createRoot() {
795
+ if (this.rootVertexId) {
796
+ throw new Error("Root vertex already exists");
797
+ }
798
+ this.rootVertexId = this.newVertexInternalWithUUID(null);
799
+ const rootVertex = this.state.getVertex(this.rootVertexId);
800
+ if (!rootVertex) {
801
+ throw new Error("Root vertex not found");
802
+ }
803
+ return new Vertex(this, rootVertex);
804
+ }
606
805
  newVertex(parentId, props = null) {
607
806
  const typedProps = props;
608
807
  const vertexId = this.newVertexInternalWithUUID(parentId);
@@ -635,7 +834,7 @@ var _RepTree = class _RepTree {
635
834
  this.applyMove(op);
636
835
  }
637
836
  deleteVertex(vertexId) {
638
- this.moveVertex(vertexId, _RepTree.TRASH_VERTEX_ID);
837
+ this.moveVertex(vertexId, _RepTree.NULL_VERTEX_ID);
639
838
  }
640
839
  setTransientVertexProperty(vertexId, key, value) {
641
840
  this.lamportClock++;
@@ -659,6 +858,9 @@ var _RepTree = class _RepTree {
659
858
  path = path.replace(/^\/+/, "");
660
859
  path = path.replace(/\/+$/, "");
661
860
  const pathParts = path.split("/");
861
+ if (!this.rootVertexId) {
862
+ return void 0;
863
+ }
662
864
  const root = this.state.getVertex(this.rootVertexId);
663
865
  if (!root) {
664
866
  throw new Error("The root vertex is not found");
@@ -680,12 +882,38 @@ var _RepTree = class _RepTree {
680
882
  return void 0;
681
883
  }
682
884
  printTree() {
885
+ if (!this.rootVertexId) {
886
+ return "";
887
+ }
683
888
  return this.state.printTree(this.rootVertexId);
684
889
  }
685
890
  merge(ops) {
686
891
  this.applyOps(ops);
687
892
  }
893
+ /** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
894
+ applyOpsOptimizedForLotsOfMoves(ops) {
895
+ const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(op.id.toString()));
896
+ if (newMoveOps.length > 0) {
897
+ const allMoveOps = [...this.moveOps, ...newMoveOps];
898
+ allMoveOps.sort((a, b) => OpId.compare(a.id, b.id));
899
+ for (let i = 0, len = allMoveOps.length; i < len; i++) {
900
+ const op = allMoveOps[i];
901
+ this.applyMove(op);
902
+ }
903
+ }
904
+ const propertyOps = ops.filter((op) => isSetPropertyOp(op) && !this.knownOps.has(op.id.toString()));
905
+ for (let i = 0, len = propertyOps.length; i < len; i++) {
906
+ const op = propertyOps[i];
907
+ this.applyProperty(op);
908
+ }
909
+ }
688
910
  compareStructure(other) {
911
+ if (this.root?.id !== other.root?.id) {
912
+ return false;
913
+ }
914
+ if (!this.rootVertexId) {
915
+ return true;
916
+ }
689
917
  return _RepTree.compareVertices(this.rootVertexId, this, other);
690
918
  }
691
919
  compareMoveOps(other) {
@@ -795,8 +1023,8 @@ var _RepTree = class _RepTree {
795
1023
  const vertexId = uuid();
796
1024
  return this.newVertexInternal(vertexId, parentId);
797
1025
  }
798
- ensureTrashVertex() {
799
- const vertexId = _RepTree.TRASH_VERTEX_ID;
1026
+ ensureNullVertex() {
1027
+ const vertexId = _RepTree.NULL_VERTEX_ID;
800
1028
  if (this.state.getVertex(vertexId)) {
801
1029
  return;
802
1030
  }
@@ -808,6 +1036,19 @@ var _RepTree = class _RepTree {
808
1036
  this.lamportClock = operation.id.counter;
809
1037
  }
810
1038
  }
1039
+ applyPendingMovesForParent(parentId) {
1040
+ if (!this.state.getVertex(parentId)) {
1041
+ return;
1042
+ }
1043
+ const pendingMoves = this.pendingMovesWithMissingParent.get(parentId);
1044
+ if (!pendingMoves) {
1045
+ return;
1046
+ }
1047
+ this.pendingMovesWithMissingParent.delete(parentId);
1048
+ for (const pendingOp of pendingMoves) {
1049
+ this.applyMove(pendingOp);
1050
+ }
1051
+ }
811
1052
  applyMove(op) {
812
1053
  if (op.parentId !== null && !this.state.getVertex(op.parentId)) {
813
1054
  if (!this.pendingMovesWithMissingParent.has(op.parentId)) {
@@ -842,15 +1083,52 @@ var _RepTree = class _RepTree {
842
1083
  }
843
1084
  this.applyPendingMovesForParent(op.targetId);
844
1085
  }
845
- reportOpAsApplied(op) {
846
- this.appliedOps.add(op.id.toString());
847
- for (const callback of this.opAppliedCallbacks) {
848
- callback(op);
1086
+ setPropertyAndItsOpId(op) {
1087
+ this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
1088
+ this.state.setProperty(op.targetId, op.key, op.value);
1089
+ this.reportOpAsApplied(op);
1090
+ }
1091
+ setTransientPropertyAndItsOpId(op) {
1092
+ this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
1093
+ this.state.setTransientProperty(op.targetId, op.key, op.value);
1094
+ this.reportOpAsApplied(op);
1095
+ }
1096
+ applyProperty(op) {
1097
+ const targetVertex = this.state.getVertex(op.targetId);
1098
+ if (!targetVertex) {
1099
+ if (op.transient) {
1100
+ return;
1101
+ }
1102
+ if (!this.pendingPropertiesWithMissingVertex.has(op.targetId)) {
1103
+ this.pendingPropertiesWithMissingVertex.set(op.targetId, []);
1104
+ }
1105
+ this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
1106
+ return;
1107
+ }
1108
+ this.updateLamportClock(op);
1109
+ const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
1110
+ const prevProp = targetVertex.getProperty(op.key);
1111
+ const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
1112
+ if (!op.transient) {
1113
+ this.setPropertyOps.push(op);
1114
+ if (!prevProp || !prevOpId || op.id.isGreaterThan(prevOpId)) {
1115
+ this.setPropertyAndItsOpId(op);
1116
+ } else {
1117
+ this.knownOps.add(op.id.toString());
1118
+ }
1119
+ if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
1120
+ this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
1121
+ targetVertex.removeTransientProperty(op.key);
1122
+ }
1123
+ } else {
1124
+ if (!prevTransientOpId || op.id.isGreaterThan(prevTransientOpId)) {
1125
+ this.setTransientPropertyAndItsOpId(op);
1126
+ }
849
1127
  }
850
1128
  }
851
1129
  applyOps(ops) {
852
1130
  for (const op of ops) {
853
- if (this.appliedOps.has(op.id.toString())) {
1131
+ if (this.knownOps.has(op.id.toString())) {
854
1132
  continue;
855
1133
  }
856
1134
  if (isMoveVertexOp(op)) {
@@ -860,34 +1138,13 @@ var _RepTree = class _RepTree {
860
1138
  }
861
1139
  }
862
1140
  }
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;
1141
+ reportOpAsApplied(op) {
1142
+ this.knownOps.add(op.id.toString());
1143
+ if (this._stateVectorEnabled) {
1144
+ this.stateVector.updateFromOp(op);
887
1145
  }
888
- this.pendingMovesWithMissingParent.delete(parentId);
889
- for (const pendingOp of pendingMoves) {
890
- this.applyMove(pendingOp);
1146
+ for (const callback of this.opAppliedCallbacks) {
1147
+ callback(op);
891
1148
  }
892
1149
  }
893
1150
  tryToMove(op) {
@@ -900,6 +1157,7 @@ var _RepTree = class _RepTree {
900
1157
  this.state.moveVertex(op.targetId, op.parentId);
901
1158
  if (!targetVertex) {
902
1159
  const pendingProperties = this.pendingPropertiesWithMissingVertex.get(op.targetId) || [];
1160
+ this.pendingPropertiesWithMissingVertex.delete(op.targetId);
903
1161
  for (const prop of pendingProperties) {
904
1162
  this.setPropertyAndItsOpId(prop);
905
1163
  }
@@ -917,55 +1175,72 @@ var _RepTree = class _RepTree {
917
1175
  }
918
1176
  this.state.moveVertex(op.targetId, prevParentId);
919
1177
  }
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);
1178
+ // --- Range-Based State Vector Methods ---
1179
+ /**
1180
+ * Returns the current state vector.
1181
+ * Returns a readonly reference to the internal state vector.
1182
+ */
1183
+ getStateVector() {
1184
+ if (!this._stateVectorEnabled) {
1185
+ return null;
1186
+ }
1187
+ return this.stateVector.getState();
929
1188
  }
930
- applyProperty(op) {
931
- const targetVertex = this.state.getVertex(op.targetId);
932
- if (!targetVertex) {
933
- if (op.transient) {
934
- return;
935
- }
936
- if (!this.pendingPropertiesWithMissingVertex.has(op.targetId)) {
937
- this.pendingPropertiesWithMissingVertex.set(op.targetId, []);
1189
+ /**
1190
+ * Determines which operations are needed to synchronize
1191
+ * with the provided state vector.
1192
+ *
1193
+ * @param theirStateVector The state vector from another peer
1194
+ * @returns Operations that should be sent to the other peer, sorted by OpId.
1195
+ */
1196
+ getMissingOps(theirStateVector) {
1197
+ if (!this._stateVectorEnabled) {
1198
+ return [...this.moveOps, ...this.setPropertyOps];
1199
+ }
1200
+ const otherStateVector = new StateVector(theirStateVector);
1201
+ const missingRanges = this.stateVector.diff(otherStateVector);
1202
+ const missingOps = [];
1203
+ const allOps = [...this.moveOps, ...this.setPropertyOps];
1204
+ for (const op of allOps) {
1205
+ for (const range of missingRanges) {
1206
+ if (op.id.peerId === range.peerId && op.id.counter >= range.start && op.id.counter <= range.end) {
1207
+ missingOps.push(op);
1208
+ break;
1209
+ }
938
1210
  }
939
- this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
940
- return;
941
1211
  }
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);
950
- }
951
- if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
952
- this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
953
- targetVertex.removeTransientProperty(op.key);
954
- }
1212
+ missingOps.sort((a, b) => OpId.compare(a.id, b.id));
1213
+ return missingOps;
1214
+ }
1215
+ /**
1216
+ * Gets or sets whether state vector tracking is enabled
1217
+ */
1218
+ get stateVectorEnabled() {
1219
+ return this._stateVectorEnabled;
1220
+ }
1221
+ /**
1222
+ * Sets the state vector enabled status
1223
+ * When enabled, rebuilds the state vector from existing operations if needed
1224
+ */
1225
+ set stateVectorEnabled(value) {
1226
+ if (value === this._stateVectorEnabled) return;
1227
+ if (value) {
1228
+ this._stateVectorEnabled = true;
1229
+ this.stateVector = StateVector.fromOperations([...this.moveOps, ...this.setPropertyOps]);
955
1230
  } else {
956
- if (!prevTransientOpId || op.id.isGreaterThan(prevTransientOpId)) {
957
- this.setTransientPropertyAndItsOpId(op);
958
- }
1231
+ this._stateVectorEnabled = false;
1232
+ this.stateVector = new StateVector();
959
1233
  }
960
1234
  }
961
1235
  };
962
- _RepTree.TRASH_VERTEX_ID = "t";
1236
+ _RepTree.NULL_VERTEX_ID = "0";
963
1237
  _RepTree.DEFAULT_MAX_DEPTH = 1e5;
964
1238
  var RepTree = _RepTree;
965
1239
  // Annotate the CommonJS export names for ESM import in node:
966
1240
  0 && (module.exports = {
967
1241
  OpId,
968
1242
  RepTree,
1243
+ StateVector,
969
1244
  TreeState,
970
1245
  Vertex,
971
1246
  VertexState,