reptree 0.1.2 → 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
@@ -29,7 +29,7 @@ import { RepTree } from 'reptree';
29
29
  const tree = new RepTree('peer1');
30
30
 
31
31
  // Root vertex is created automatically
32
- const rootVertex = tree.rootVertex;
32
+ const rootVertex = tree.createRoot();
33
33
  rootVertex.name = 'Project';
34
34
 
35
35
  // Create a folder structure with properties
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,7 +496,7 @@ var Vertex = class {
495
496
  }
496
497
  };
497
498
 
498
- // src/RepTree.ts
499
+ // src/StateVector.ts
499
500
  function subtractRanges(rangesA, rangesB) {
500
501
  if (rangesB.length === 0) return rangesA.map((r) => [...r]);
501
502
  if (rangesA.length === 0) return [];
@@ -528,6 +529,159 @@ function subtractRanges(rangesA, rangesB) {
528
529
  }
529
530
  return result;
530
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
+
684
+ // src/RepTree.ts
531
685
  var _RepTree = class _RepTree {
532
686
  /**
533
687
  * @param peerId - The peer ID of the current client
@@ -546,30 +700,37 @@ var _RepTree = class _RepTree {
546
700
  this.parentIdBeforeMove = /* @__PURE__ */ new Map();
547
701
  this.opAppliedCallbacks = [];
548
702
  this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
549
- // State vector tracking operations from each peer
550
- this.stateVector = {};
703
+ this._stateVectorEnabled = true;
551
704
  this.peerId = peerId;
552
705
  this.state = new TreeState();
706
+ this.stateVector = new StateVector();
553
707
  if (ops != null && ops.length > 0) {
554
- let rootMoveOp;
555
- for (let i = 0; i < ops.length; i++) {
556
- if (isMoveVertexOp(ops[i]) && ops[i].parentId === null) {
557
- rootMoveOp = ops[i];
558
- break;
559
- }
560
- }
561
- if (rootMoveOp) {
562
- this.rootVertexId = rootMoveOp.targetId;
563
- } else {
564
- throw new Error("The operations has to contain a move operation with a parentId as null to set the root vertex");
565
- }
566
708
  this.applyOps(ops);
567
- this.ensureNullVertex();
709
+ const root = this.root;
710
+ if (!root) {
711
+ throw new Error("There has to be a root vertex in the operations");
712
+ }
568
713
  } else {
569
- this.rootVertexId = this.newVertexInternalWithUUID(null);
570
714
  this.ensureNullVertex();
571
715
  }
572
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");
731
+ }
732
+ return new Vertex(this, rootVertex);
733
+ }
573
734
  getMoveOps() {
574
735
  return this.moveOps;
575
736
  }
@@ -580,13 +741,6 @@ var _RepTree = class _RepTree {
580
741
  const vertex = this.state.getVertex(vertexId);
581
742
  return vertex ? new Vertex(this, vertex) : void 0;
582
743
  }
583
- get rootVertex() {
584
- const rootVertex = this.state.getVertex(this.rootVertexId);
585
- if (!rootVertex) {
586
- throw new Error("Root vertex not found");
587
- }
588
- return new Vertex(this, rootVertex);
589
- }
590
744
  getAllVertices() {
591
745
  return this.state.getAllVertices().map((v) => new Vertex(this, v));
592
746
  }
@@ -637,6 +791,17 @@ var _RepTree = class _RepTree {
637
791
  setMaxDepth(maxDepth) {
638
792
  this.maxDepth = maxDepth;
639
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
+ }
640
805
  newVertex(parentId, props = null) {
641
806
  const typedProps = props;
642
807
  const vertexId = this.newVertexInternalWithUUID(parentId);
@@ -693,6 +858,9 @@ var _RepTree = class _RepTree {
693
858
  path = path.replace(/^\/+/, "");
694
859
  path = path.replace(/\/+$/, "");
695
860
  const pathParts = path.split("/");
861
+ if (!this.rootVertexId) {
862
+ return void 0;
863
+ }
696
864
  const root = this.state.getVertex(this.rootVertexId);
697
865
  if (!root) {
698
866
  throw new Error("The root vertex is not found");
@@ -714,6 +882,9 @@ var _RepTree = class _RepTree {
714
882
  return void 0;
715
883
  }
716
884
  printTree() {
885
+ if (!this.rootVertexId) {
886
+ return "";
887
+ }
717
888
  return this.state.printTree(this.rootVertexId);
718
889
  }
719
890
  merge(ops) {
@@ -737,6 +908,12 @@ var _RepTree = class _RepTree {
737
908
  }
738
909
  }
739
910
  compareStructure(other) {
911
+ if (this.root?.id !== other.root?.id) {
912
+ return false;
913
+ }
914
+ if (!this.rootVertexId) {
915
+ return true;
916
+ }
740
917
  return _RepTree.compareVertices(this.rootVertexId, this, other);
741
918
  }
742
919
  compareMoveOps(other) {
@@ -963,7 +1140,9 @@ var _RepTree = class _RepTree {
963
1140
  }
964
1141
  reportOpAsApplied(op) {
965
1142
  this.knownOps.add(op.id.toString());
966
- this.updateStateVector(op);
1143
+ if (this._stateVectorEnabled) {
1144
+ this.stateVector.updateFromOp(op);
1145
+ }
967
1146
  for (const callback of this.opAppliedCallbacks) {
968
1147
  callback(op);
969
1148
  }
@@ -997,99 +1176,15 @@ var _RepTree = class _RepTree {
997
1176
  this.state.moveVertex(op.targetId, prevParentId);
998
1177
  }
999
1178
  // --- 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;
1024
- }
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;
1045
- }
1046
- }
1047
- if (!rangeExtendedOrMerged) {
1048
- if (insertIndex === -1) {
1049
- insertIndex = ranges.length;
1050
- }
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--;
1056
- }
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
1179
  /**
1064
1180
  * Returns the current state vector.
1065
1181
  * Returns a readonly reference to the internal state vector.
1066
1182
  */
1067
1183
  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
- }
1184
+ if (!this._stateVectorEnabled) {
1185
+ return null;
1091
1186
  }
1092
- return missingRanges;
1187
+ return this.stateVector.getState();
1093
1188
  }
1094
1189
  /**
1095
1190
  * Determines which operations are needed to synchronize
@@ -1099,7 +1194,11 @@ var _RepTree = class _RepTree {
1099
1194
  * @returns Operations that should be sent to the other peer, sorted by OpId.
1100
1195
  */
1101
1196
  getMissingOps(theirStateVector) {
1102
- const missingRanges = this.diffStateVectors(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);
1103
1202
  const missingOps = [];
1104
1203
  const allOps = [...this.moveOps, ...this.setPropertyOps];
1105
1204
  for (const op of allOps) {
@@ -1113,6 +1212,26 @@ var _RepTree = class _RepTree {
1113
1212
  missingOps.sort((a, b) => OpId.compare(a.id, b.id));
1114
1213
  return missingOps;
1115
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]);
1230
+ } else {
1231
+ this._stateVectorEnabled = false;
1232
+ this.stateVector = new StateVector();
1233
+ }
1234
+ }
1116
1235
  };
1117
1236
  _RepTree.NULL_VERTEX_ID = "0";
1118
1237
  _RepTree.DEFAULT_MAX_DEPTH = 1e5;
@@ -1121,6 +1240,7 @@ var RepTree = _RepTree;
1121
1240
  0 && (module.exports = {
1122
1241
  OpId,
1123
1242
  RepTree,
1243
+ StateVector,
1124
1244
  TreeState,
1125
1245
  Vertex,
1126
1246
  VertexState,