reptree 0.1.2 → 0.1.4

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
@@ -27,19 +27,17 @@ import { RepTree } from 'reptree';
27
27
 
28
28
  // Create a new tree
29
29
  const tree = new RepTree('peer1');
30
-
31
- // Root vertex is created automatically
32
- const rootVertex = tree.rootVertex;
33
- rootVertex.name = 'Project';
30
+ const root = tree.createRoot();
31
+ root.name = 'Project';
34
32
 
35
33
  // Create a folder structure with properties
36
- const docsFolder = rootVertex.newNamedChild('Docs');
34
+ const docsFolder = root.newNamedChild('Docs');
37
35
  docsFolder.setProperties({
38
36
  type: 'folder',
39
37
  icon: 'folder-icon'
40
38
  });
41
39
 
42
- const imagesFolder = rootVertex.newNamedChild('Images');
40
+ const imagesFolder = root.newNamedChild('Images');
43
41
  imagesFolder.setProperties({
44
42
  type: 'folder',
45
43
  icon: 'image-icon'
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,
@@ -360,9 +361,21 @@ var TreeState = class {
360
361
  }
361
362
  }
362
363
  const children = this.getChildrenIds(vertexId);
363
- for (let i = 0; i < children.length; i++) {
364
- const childId = children[i];
365
- const isLastChild = i === children.length - 1;
364
+ const sortedChildren = [...children].sort((a, b) => {
365
+ const vertexA = this.getVertex(a);
366
+ const vertexB = this.getVertex(b);
367
+ const nameA = vertexA?.getProperty("_n");
368
+ const nameB = vertexB?.getProperty("_n");
369
+ if (nameA && nameB) {
370
+ return nameA.localeCompare(nameB);
371
+ }
372
+ if (nameA) return -1;
373
+ if (nameB) return 1;
374
+ return a.localeCompare(b);
375
+ });
376
+ for (let i = 0; i < sortedChildren.length; i++) {
377
+ const childId = sortedChildren[i];
378
+ const isLastChild = i === sortedChildren.length - 1;
366
379
  result += this.printTree(childId, indent + (isLast ? " " : "\u2502 "), isLastChild);
367
380
  }
368
381
  return result;
@@ -495,7 +508,7 @@ var Vertex = class {
495
508
  }
496
509
  };
497
510
 
498
- // src/RepTree.ts
511
+ // src/StateVector.ts
499
512
  function subtractRanges(rangesA, rangesB) {
500
513
  if (rangesB.length === 0) return rangesA.map((r) => [...r]);
501
514
  if (rangesA.length === 0) return [];
@@ -528,12 +541,165 @@ function subtractRanges(rangesA, rangesB) {
528
541
  }
529
542
  return result;
530
543
  }
544
+ var StateVector = class _StateVector {
545
+ /**
546
+ * Creates a new StateVector.
547
+ * @param initialState Optional initial state to copy from
548
+ */
549
+ constructor(initialState = {}) {
550
+ this.ranges = {};
551
+ for (const [peerId, peerRanges] of Object.entries(initialState)) {
552
+ this.ranges[peerId] = peerRanges.map((range) => [...range]);
553
+ }
554
+ }
555
+ /**
556
+ * Updates the state vector with a newly applied operation.
557
+ * Assumes ranges are sorted and non-overlapping.
558
+ *
559
+ * @param peerId The peer ID of the operation
560
+ * @param counter The counter value of the operation
561
+ */
562
+ update(peerId, counter) {
563
+ if (!this.ranges[peerId]) {
564
+ this.ranges[peerId] = [];
565
+ }
566
+ const ranges = this.ranges[peerId];
567
+ if (ranges.length === 0) {
568
+ ranges.push([counter, counter]);
569
+ return;
570
+ }
571
+ let rangeExtendedOrMerged = false;
572
+ let insertIndex = -1;
573
+ for (let i = 0; i < ranges.length; i++) {
574
+ const range = ranges[i];
575
+ if (counter >= range[0] && counter <= range[1]) {
576
+ rangeExtendedOrMerged = true;
577
+ break;
578
+ }
579
+ if (counter === range[0] - 1) {
580
+ range[0] = counter;
581
+ rangeExtendedOrMerged = true;
582
+ if (i > 0 && range[0] === ranges[i - 1][1] + 1) {
583
+ ranges[i - 1][1] = range[1];
584
+ ranges.splice(i, 1);
585
+ }
586
+ break;
587
+ }
588
+ if (counter === range[1] + 1) {
589
+ range[1] = counter;
590
+ rangeExtendedOrMerged = true;
591
+ if (i < ranges.length - 1 && range[1] + 1 === ranges[i + 1][0]) {
592
+ range[1] = ranges[i + 1][1];
593
+ ranges.splice(i + 1, 1);
594
+ }
595
+ break;
596
+ }
597
+ if (counter < range[0] && insertIndex === -1) {
598
+ insertIndex = i;
599
+ }
600
+ }
601
+ if (!rangeExtendedOrMerged) {
602
+ if (insertIndex === -1) {
603
+ insertIndex = ranges.length;
604
+ }
605
+ ranges.splice(insertIndex, 0, [counter, counter]);
606
+ if (insertIndex > 0 && ranges[insertIndex][0] === ranges[insertIndex - 1][1] + 1) {
607
+ ranges[insertIndex - 1][1] = ranges[insertIndex][1];
608
+ ranges.splice(insertIndex, 1);
609
+ insertIndex--;
610
+ }
611
+ if (insertIndex < ranges.length - 1 && ranges[insertIndex][1] + 1 === ranges[insertIndex + 1][0]) {
612
+ ranges[insertIndex][1] = ranges[insertIndex + 1][1];
613
+ ranges.splice(insertIndex + 1, 1);
614
+ }
615
+ }
616
+ }
617
+ /**
618
+ * Updates the state vector with a newly applied operation.
619
+ *
620
+ * @param op The operation that was just applied
621
+ */
622
+ updateFromOp(op) {
623
+ this.update(op.id.peerId, op.id.counter);
624
+ }
625
+ /**
626
+ * Returns the current state vector.
627
+ * Returns a readonly reference to the internal state.
628
+ */
629
+ getState() {
630
+ return this.ranges;
631
+ }
632
+ /**
633
+ * Calculates which operation ranges we have that the other state vector is missing
634
+ * by comparing state vectors.
635
+ *
636
+ * @param other The other state vector to compare against
637
+ * @returns Array of operation ID ranges that we have but they don't
638
+ */
639
+ diff(other) {
640
+ const missingRanges = [];
641
+ const theirState = other.getState();
642
+ for (const [peerId, ourRanges] of Object.entries(this.ranges)) {
643
+ const theirRanges = theirState[peerId] || [];
644
+ const missing = subtractRanges(ourRanges, theirRanges);
645
+ for (const [start, end] of missing) {
646
+ if (start <= end) {
647
+ missingRanges.push({
648
+ peerId,
649
+ start,
650
+ end
651
+ });
652
+ }
653
+ }
654
+ }
655
+ return missingRanges;
656
+ }
657
+ /**
658
+ * Checks if the state vector contains the given operation ID
659
+ *
660
+ * @param opId The operation ID to check
661
+ * @returns true if the operation is in the state vector, false otherwise
662
+ */
663
+ contains(opId) {
664
+ const peerId = opId.peerId;
665
+ const counter = opId.counter;
666
+ if (!this.ranges[peerId]) {
667
+ return false;
668
+ }
669
+ for (const [start, end] of this.ranges[peerId]) {
670
+ if (counter >= start && counter <= end) {
671
+ return true;
672
+ }
673
+ }
674
+ return false;
675
+ }
676
+ /**
677
+ * Creates a copy of this state vector
678
+ */
679
+ clone() {
680
+ return new _StateVector(this.ranges);
681
+ }
682
+ /**
683
+ * Builds a state vector from an array of operations
684
+ * @param operations The operations to build the state vector from
685
+ * @returns A new StateVector instance
686
+ */
687
+ static fromOperations(operations) {
688
+ const stateVector = new _StateVector();
689
+ for (const op of operations) {
690
+ stateVector.updateFromOp(op);
691
+ }
692
+ return stateVector;
693
+ }
694
+ };
695
+
696
+ // src/RepTree.ts
531
697
  var _RepTree = class _RepTree {
532
698
  /**
533
- * @param peerId - The peer ID of the current client
534
- * @param ops - The operations to replicate an existing tree, if null - a new tree will be created
699
+ * @param peerId - The peer ID of the current client. Should be unique across all peers.
700
+ * @param ops - The operations to replicate an existing tree, if not provided - an empty tree will be created without a root vertex
535
701
  */
536
- constructor(peerId, ops = null) {
702
+ constructor(peerId, ops) {
537
703
  this.lamportClock = 0;
538
704
  this.moveOps = [];
539
705
  this.setPropertyOps = [];
@@ -546,30 +712,37 @@ var _RepTree = class _RepTree {
546
712
  this.parentIdBeforeMove = /* @__PURE__ */ new Map();
547
713
  this.opAppliedCallbacks = [];
548
714
  this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
549
- // State vector tracking operations from each peer
550
- this.stateVector = {};
715
+ this._stateVectorEnabled = true;
551
716
  this.peerId = peerId;
552
717
  this.state = new TreeState();
553
- 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
- }
718
+ this.stateVector = new StateVector();
719
+ if (ops && ops.length > 0) {
566
720
  this.applyOps(ops);
567
- this.ensureNullVertex();
721
+ const root = this.root;
722
+ if (!root) {
723
+ throw new Error("There has to be a root vertex in the operations");
724
+ }
568
725
  } else {
569
- this.rootVertexId = this.newVertexInternalWithUUID(null);
570
726
  this.ensureNullVertex();
571
727
  }
572
728
  }
729
+ get root() {
730
+ if (!this.rootVertexId) {
731
+ const vertices = this.state.getAllVertices();
732
+ for (const vertex of vertices) {
733
+ if (vertex.parentId === null && vertex.id !== _RepTree.NULL_VERTEX_ID) {
734
+ this.rootVertexId = vertex.id;
735
+ return new Vertex(this, vertex);
736
+ }
737
+ }
738
+ return void 0;
739
+ }
740
+ const rootVertex = this.state.getVertex(this.rootVertexId);
741
+ if (!rootVertex) {
742
+ throw new Error("Root vertex not found");
743
+ }
744
+ return new Vertex(this, rootVertex);
745
+ }
573
746
  getMoveOps() {
574
747
  return this.moveOps;
575
748
  }
@@ -580,13 +753,6 @@ var _RepTree = class _RepTree {
580
753
  const vertex = this.state.getVertex(vertexId);
581
754
  return vertex ? new Vertex(this, vertex) : void 0;
582
755
  }
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
756
  getAllVertices() {
591
757
  return this.state.getAllVertices().map((v) => new Vertex(this, v));
592
758
  }
@@ -637,6 +803,17 @@ var _RepTree = class _RepTree {
637
803
  setMaxDepth(maxDepth) {
638
804
  this.maxDepth = maxDepth;
639
805
  }
806
+ createRoot() {
807
+ if (this.rootVertexId) {
808
+ throw new Error("Root vertex already exists");
809
+ }
810
+ this.rootVertexId = this.newVertexInternalWithUUID(null);
811
+ const rootVertex = this.state.getVertex(this.rootVertexId);
812
+ if (!rootVertex) {
813
+ throw new Error("Root vertex not found");
814
+ }
815
+ return new Vertex(this, rootVertex);
816
+ }
640
817
  newVertex(parentId, props = null) {
641
818
  const typedProps = props;
642
819
  const vertexId = this.newVertexInternalWithUUID(parentId);
@@ -693,6 +870,9 @@ var _RepTree = class _RepTree {
693
870
  path = path.replace(/^\/+/, "");
694
871
  path = path.replace(/\/+$/, "");
695
872
  const pathParts = path.split("/");
873
+ if (!this.rootVertexId) {
874
+ return void 0;
875
+ }
696
876
  const root = this.state.getVertex(this.rootVertexId);
697
877
  if (!root) {
698
878
  throw new Error("The root vertex is not found");
@@ -714,6 +894,9 @@ var _RepTree = class _RepTree {
714
894
  return void 0;
715
895
  }
716
896
  printTree() {
897
+ if (!this.rootVertexId) {
898
+ return "";
899
+ }
717
900
  return this.state.printTree(this.rootVertexId);
718
901
  }
719
902
  merge(ops) {
@@ -737,6 +920,12 @@ var _RepTree = class _RepTree {
737
920
  }
738
921
  }
739
922
  compareStructure(other) {
923
+ if (this.root?.id !== other.root?.id) {
924
+ return false;
925
+ }
926
+ if (!this.rootVertexId) {
927
+ return true;
928
+ }
740
929
  return _RepTree.compareVertices(this.rootVertexId, this, other);
741
930
  }
742
931
  compareMoveOps(other) {
@@ -963,7 +1152,9 @@ var _RepTree = class _RepTree {
963
1152
  }
964
1153
  reportOpAsApplied(op) {
965
1154
  this.knownOps.add(op.id.toString());
966
- this.updateStateVector(op);
1155
+ if (this._stateVectorEnabled) {
1156
+ this.stateVector.updateFromOp(op);
1157
+ }
967
1158
  for (const callback of this.opAppliedCallbacks) {
968
1159
  callback(op);
969
1160
  }
@@ -997,99 +1188,15 @@ var _RepTree = class _RepTree {
997
1188
  this.state.moveVertex(op.targetId, prevParentId);
998
1189
  }
999
1190
  // --- 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
1191
  /**
1064
1192
  * Returns the current state vector.
1065
1193
  * Returns a readonly reference to the internal state vector.
1066
1194
  */
1067
1195
  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
- }
1196
+ if (!this._stateVectorEnabled) {
1197
+ return null;
1091
1198
  }
1092
- return missingRanges;
1199
+ return this.stateVector.getState();
1093
1200
  }
1094
1201
  /**
1095
1202
  * Determines which operations are needed to synchronize
@@ -1099,7 +1206,11 @@ var _RepTree = class _RepTree {
1099
1206
  * @returns Operations that should be sent to the other peer, sorted by OpId.
1100
1207
  */
1101
1208
  getMissingOps(theirStateVector) {
1102
- const missingRanges = this.diffStateVectors(theirStateVector);
1209
+ if (!this._stateVectorEnabled) {
1210
+ return [...this.moveOps, ...this.setPropertyOps];
1211
+ }
1212
+ const otherStateVector = new StateVector(theirStateVector);
1213
+ const missingRanges = this.stateVector.diff(otherStateVector);
1103
1214
  const missingOps = [];
1104
1215
  const allOps = [...this.moveOps, ...this.setPropertyOps];
1105
1216
  for (const op of allOps) {
@@ -1113,6 +1224,26 @@ var _RepTree = class _RepTree {
1113
1224
  missingOps.sort((a, b) => OpId.compare(a.id, b.id));
1114
1225
  return missingOps;
1115
1226
  }
1227
+ /**
1228
+ * Gets or sets whether state vector tracking is enabled
1229
+ */
1230
+ get stateVectorEnabled() {
1231
+ return this._stateVectorEnabled;
1232
+ }
1233
+ /**
1234
+ * Sets the state vector enabled status
1235
+ * When enabled, rebuilds the state vector from existing operations if needed
1236
+ */
1237
+ set stateVectorEnabled(value) {
1238
+ if (value === this._stateVectorEnabled) return;
1239
+ if (value) {
1240
+ this._stateVectorEnabled = true;
1241
+ this.stateVector = StateVector.fromOperations([...this.moveOps, ...this.setPropertyOps]);
1242
+ } else {
1243
+ this._stateVectorEnabled = false;
1244
+ this.stateVector = new StateVector();
1245
+ }
1246
+ }
1116
1247
  };
1117
1248
  _RepTree.NULL_VERTEX_ID = "0";
1118
1249
  _RepTree.DEFAULT_MAX_DEPTH = 1e5;
@@ -1121,6 +1252,7 @@ var RepTree = _RepTree;
1121
1252
  0 && (module.exports = {
1122
1253
  OpId,
1123
1254
  RepTree,
1255
+ StateVector,
1124
1256
  TreeState,
1125
1257
  Vertex,
1126
1258
  VertexState,