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/dist/index.d.ts CHANGED
@@ -124,7 +124,7 @@ declare class RepTree {
124
124
  private static NULL_VERTEX_ID;
125
125
  private static DEFAULT_MAX_DEPTH;
126
126
  readonly peerId: string;
127
- readonly rootVertexId: string;
127
+ private rootVertexId;
128
128
  private lamportClock;
129
129
  private state;
130
130
  private moveOps;
@@ -139,15 +139,16 @@ declare class RepTree {
139
139
  private opAppliedCallbacks;
140
140
  private maxDepth;
141
141
  private stateVector;
142
+ private _stateVectorEnabled;
142
143
  /**
143
144
  * @param peerId - The peer ID of the current client
144
145
  * @param ops - The operations to replicate an existing tree, if null - a new tree will be created
145
146
  */
146
147
  constructor(peerId: string, ops?: ReadonlyArray<VertexOperation> | null);
148
+ get root(): Vertex | undefined;
147
149
  getMoveOps(): ReadonlyArray<MoveVertex>;
148
150
  getAllOps(): ReadonlyArray<VertexOperation>;
149
151
  getVertex(vertexId: string): Vertex | undefined;
150
- get rootVertex(): Vertex;
151
152
  getAllVertices(): ReadonlyArray<Vertex>;
152
153
  getParent(vertexId: string): Vertex | undefined;
153
154
  getChildren(vertexId: string): Vertex[];
@@ -157,6 +158,7 @@ declare class RepTree {
157
158
  getVertexProperties(vertexId: string): Readonly<TreeVertexProperty[]>;
158
159
  popLocalOps(): VertexOperation[];
159
160
  setMaxDepth(maxDepth: number): void;
161
+ createRoot(): Vertex;
160
162
  newVertex(parentId: string, props?: Record<string, VertexPropertyType> | object | null): Vertex;
161
163
  newNamedVertex(parentId: string, name: string, props?: Record<string, VertexPropertyType> | object | null): Vertex;
162
164
  moveVertex(vertexId: string, parentId: string): void;
@@ -193,26 +195,11 @@ declare class RepTree {
193
195
  private reportOpAsApplied;
194
196
  private tryToMove;
195
197
  private undoMove;
196
- /**
197
- * Updates the state vector with a newly applied operation.
198
- * Assumes ranges are sorted and non-overlapping.
199
- *
200
- * @param op The operation that was just applied
201
- */
202
- private updateStateVector;
203
198
  /**
204
199
  * Returns the current state vector.
205
200
  * Returns a readonly reference to the internal state vector.
206
201
  */
207
- getStateVector(): Readonly<Record<string, number[][]>>;
208
- /**
209
- * Calculates which operation ranges we have that the other peer is missing
210
- * by comparing state vectors.
211
- *
212
- * @param theirStateVector The state vector from another peer
213
- * @returns Array of operation ID ranges that we have but they don't
214
- */
215
- private diffStateVectors;
202
+ getStateVector(): Readonly<Record<string, number[][]>> | null;
216
203
  /**
217
204
  * Determines which operations are needed to synchronize
218
205
  * with the provided state vector.
@@ -221,6 +208,15 @@ declare class RepTree {
221
208
  * @returns Operations that should be sent to the other peer, sorted by OpId.
222
209
  */
223
210
  getMissingOps(theirStateVector: Record<string, number[][]>): VertexOperation[];
211
+ /**
212
+ * Gets or sets whether state vector tracking is enabled
213
+ */
214
+ get stateVectorEnabled(): boolean;
215
+ /**
216
+ * Sets the state vector enabled status
217
+ * When enabled, rebuilds the state vector from existing operations if needed
218
+ */
219
+ set stateVectorEnabled(value: boolean);
224
220
  }
225
221
 
226
222
  declare class TreeState {
@@ -247,6 +243,63 @@ declare class TreeState {
247
243
  printTree(vertexId: TreeVertexId, indent?: string, isLast?: boolean): string;
248
244
  }
249
245
 
246
+ /**
247
+ * StateVector tracks operations that have been applied using a range-based representation.
248
+ * It's used for synchronization between peers to determine which operations need to be sent.
249
+ */
250
+ declare class StateVector {
251
+ private ranges;
252
+ /**
253
+ * Creates a new StateVector.
254
+ * @param initialState Optional initial state to copy from
255
+ */
256
+ constructor(initialState?: Record<string, number[][]>);
257
+ /**
258
+ * Updates the state vector with a newly applied operation.
259
+ * Assumes ranges are sorted and non-overlapping.
260
+ *
261
+ * @param peerId The peer ID of the operation
262
+ * @param counter The counter value of the operation
263
+ */
264
+ update(peerId: string, counter: number): void;
265
+ /**
266
+ * Updates the state vector with a newly applied operation.
267
+ *
268
+ * @param op The operation that was just applied
269
+ */
270
+ updateFromOp(op: VertexOperation): void;
271
+ /**
272
+ * Returns the current state vector.
273
+ * Returns a readonly reference to the internal state.
274
+ */
275
+ getState(): Readonly<Record<string, number[][]>>;
276
+ /**
277
+ * Calculates which operation ranges we have that the other state vector is missing
278
+ * by comparing state vectors.
279
+ *
280
+ * @param other The other state vector to compare against
281
+ * @returns Array of operation ID ranges that we have but they don't
282
+ */
283
+ diff(other: StateVector): OpIdRange[];
284
+ /**
285
+ * Checks if the state vector contains the given operation ID
286
+ *
287
+ * @param opId The operation ID to check
288
+ * @returns true if the operation is in the state vector, false otherwise
289
+ */
290
+ contains(opId: OpId): boolean;
291
+ /**
292
+ * Creates a copy of this state vector
293
+ */
294
+ clone(): StateVector;
295
+ /**
296
+ * Builds a state vector from an array of operations
297
+ * @param operations The operations to build the state vector from
298
+ * @returns A new StateVector instance
299
+ */
300
+ static fromOperations(operations: ReadonlyArray<VertexOperation>): StateVector;
301
+ }
302
+
250
303
  declare function uuid(): string;
251
304
 
252
- export { type MoveVertex, OpId, type OpIdRange, RepTree, type SetVertexProperty, TreeState, type TreeVertexId, type TreeVertexProperty, Vertex, type VertexChangeEvent, type VertexChildrenChangeEvent, type VertexMoveEvent, type VertexOperation, type VertexPropertyChangeEvent, type VertexPropertyType, VertexState, isMoveVertexOp, isSetPropertyOp, newMoveVertexOp, newSetTransientVertexPropertyOp, newSetVertexPropertyOp, uuid };
305
+ export { type MoveVertex, OpId, type OpIdRange, RepTree, type SetVertexProperty, StateVector, TreeState, type TreeVertexId, type TreeVertexProperty, Vertex, type VertexChangeEvent, type VertexChildrenChangeEvent, type VertexMoveEvent, type VertexOperation, type VertexPropertyChangeEvent, type VertexPropertyType, VertexState, isMoveVertexOp, isSetPropertyOp, newMoveVertexOp, newSetTransientVertexPropertyOp, newSetVertexPropertyOp, uuid };
package/dist/index.js CHANGED
@@ -459,7 +459,7 @@ var Vertex = class {
459
459
  }
460
460
  };
461
461
 
462
- // src/RepTree.ts
462
+ // src/StateVector.ts
463
463
  function subtractRanges(rangesA, rangesB) {
464
464
  if (rangesB.length === 0) return rangesA.map((r) => [...r]);
465
465
  if (rangesA.length === 0) return [];
@@ -492,6 +492,159 @@ function subtractRanges(rangesA, rangesB) {
492
492
  }
493
493
  return result;
494
494
  }
495
+ var StateVector = class _StateVector {
496
+ /**
497
+ * Creates a new StateVector.
498
+ * @param initialState Optional initial state to copy from
499
+ */
500
+ constructor(initialState = {}) {
501
+ this.ranges = {};
502
+ for (const [peerId, peerRanges] of Object.entries(initialState)) {
503
+ this.ranges[peerId] = peerRanges.map((range) => [...range]);
504
+ }
505
+ }
506
+ /**
507
+ * Updates the state vector with a newly applied operation.
508
+ * Assumes ranges are sorted and non-overlapping.
509
+ *
510
+ * @param peerId The peer ID of the operation
511
+ * @param counter The counter value of the operation
512
+ */
513
+ update(peerId, counter) {
514
+ if (!this.ranges[peerId]) {
515
+ this.ranges[peerId] = [];
516
+ }
517
+ const ranges = this.ranges[peerId];
518
+ if (ranges.length === 0) {
519
+ ranges.push([counter, counter]);
520
+ return;
521
+ }
522
+ let rangeExtendedOrMerged = false;
523
+ let insertIndex = -1;
524
+ for (let i = 0; i < ranges.length; i++) {
525
+ const range = ranges[i];
526
+ if (counter >= range[0] && counter <= range[1]) {
527
+ rangeExtendedOrMerged = true;
528
+ break;
529
+ }
530
+ if (counter === range[0] - 1) {
531
+ range[0] = counter;
532
+ rangeExtendedOrMerged = true;
533
+ if (i > 0 && range[0] === ranges[i - 1][1] + 1) {
534
+ ranges[i - 1][1] = range[1];
535
+ ranges.splice(i, 1);
536
+ }
537
+ break;
538
+ }
539
+ if (counter === range[1] + 1) {
540
+ range[1] = counter;
541
+ rangeExtendedOrMerged = true;
542
+ if (i < ranges.length - 1 && range[1] + 1 === ranges[i + 1][0]) {
543
+ range[1] = ranges[i + 1][1];
544
+ ranges.splice(i + 1, 1);
545
+ }
546
+ break;
547
+ }
548
+ if (counter < range[0] && insertIndex === -1) {
549
+ insertIndex = i;
550
+ }
551
+ }
552
+ if (!rangeExtendedOrMerged) {
553
+ if (insertIndex === -1) {
554
+ insertIndex = ranges.length;
555
+ }
556
+ ranges.splice(insertIndex, 0, [counter, counter]);
557
+ if (insertIndex > 0 && ranges[insertIndex][0] === ranges[insertIndex - 1][1] + 1) {
558
+ ranges[insertIndex - 1][1] = ranges[insertIndex][1];
559
+ ranges.splice(insertIndex, 1);
560
+ insertIndex--;
561
+ }
562
+ if (insertIndex < ranges.length - 1 && ranges[insertIndex][1] + 1 === ranges[insertIndex + 1][0]) {
563
+ ranges[insertIndex][1] = ranges[insertIndex + 1][1];
564
+ ranges.splice(insertIndex + 1, 1);
565
+ }
566
+ }
567
+ }
568
+ /**
569
+ * Updates the state vector with a newly applied operation.
570
+ *
571
+ * @param op The operation that was just applied
572
+ */
573
+ updateFromOp(op) {
574
+ this.update(op.id.peerId, op.id.counter);
575
+ }
576
+ /**
577
+ * Returns the current state vector.
578
+ * Returns a readonly reference to the internal state.
579
+ */
580
+ getState() {
581
+ return this.ranges;
582
+ }
583
+ /**
584
+ * Calculates which operation ranges we have that the other state vector is missing
585
+ * by comparing state vectors.
586
+ *
587
+ * @param other The other state vector to compare against
588
+ * @returns Array of operation ID ranges that we have but they don't
589
+ */
590
+ diff(other) {
591
+ const missingRanges = [];
592
+ const theirState = other.getState();
593
+ for (const [peerId, ourRanges] of Object.entries(this.ranges)) {
594
+ const theirRanges = theirState[peerId] || [];
595
+ const missing = subtractRanges(ourRanges, theirRanges);
596
+ for (const [start, end] of missing) {
597
+ if (start <= end) {
598
+ missingRanges.push({
599
+ peerId,
600
+ start,
601
+ end
602
+ });
603
+ }
604
+ }
605
+ }
606
+ return missingRanges;
607
+ }
608
+ /**
609
+ * Checks if the state vector contains the given operation ID
610
+ *
611
+ * @param opId The operation ID to check
612
+ * @returns true if the operation is in the state vector, false otherwise
613
+ */
614
+ contains(opId) {
615
+ const peerId = opId.peerId;
616
+ const counter = opId.counter;
617
+ if (!this.ranges[peerId]) {
618
+ return false;
619
+ }
620
+ for (const [start, end] of this.ranges[peerId]) {
621
+ if (counter >= start && counter <= end) {
622
+ return true;
623
+ }
624
+ }
625
+ return false;
626
+ }
627
+ /**
628
+ * Creates a copy of this state vector
629
+ */
630
+ clone() {
631
+ return new _StateVector(this.ranges);
632
+ }
633
+ /**
634
+ * Builds a state vector from an array of operations
635
+ * @param operations The operations to build the state vector from
636
+ * @returns A new StateVector instance
637
+ */
638
+ static fromOperations(operations) {
639
+ const stateVector = new _StateVector();
640
+ for (const op of operations) {
641
+ stateVector.updateFromOp(op);
642
+ }
643
+ return stateVector;
644
+ }
645
+ };
646
+
647
+ // src/RepTree.ts
495
648
  var _RepTree = class _RepTree {
496
649
  /**
497
650
  * @param peerId - The peer ID of the current client
@@ -510,30 +663,37 @@ var _RepTree = class _RepTree {
510
663
  this.parentIdBeforeMove = /* @__PURE__ */ new Map();
511
664
  this.opAppliedCallbacks = [];
512
665
  this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
513
- // State vector tracking operations from each peer
514
- this.stateVector = {};
666
+ this._stateVectorEnabled = true;
515
667
  this.peerId = peerId;
516
668
  this.state = new TreeState();
669
+ this.stateVector = new StateVector();
517
670
  if (ops != null && ops.length > 0) {
518
- let rootMoveOp;
519
- for (let i = 0; i < ops.length; i++) {
520
- if (isMoveVertexOp(ops[i]) && ops[i].parentId === null) {
521
- rootMoveOp = ops[i];
522
- break;
523
- }
524
- }
525
- if (rootMoveOp) {
526
- this.rootVertexId = rootMoveOp.targetId;
527
- } else {
528
- throw new Error("The operations has to contain a move operation with a parentId as null to set the root vertex");
529
- }
530
671
  this.applyOps(ops);
531
- this.ensureNullVertex();
672
+ const root = this.root;
673
+ if (!root) {
674
+ throw new Error("There has to be a root vertex in the operations");
675
+ }
532
676
  } else {
533
- this.rootVertexId = this.newVertexInternalWithUUID(null);
534
677
  this.ensureNullVertex();
535
678
  }
536
679
  }
680
+ get root() {
681
+ if (!this.rootVertexId) {
682
+ const vertices = this.state.getAllVertices();
683
+ for (const vertex of vertices) {
684
+ if (vertex.parentId === null && vertex.id !== _RepTree.NULL_VERTEX_ID) {
685
+ this.rootVertexId = vertex.id;
686
+ return new Vertex(this, vertex);
687
+ }
688
+ }
689
+ return void 0;
690
+ }
691
+ const rootVertex = this.state.getVertex(this.rootVertexId);
692
+ if (!rootVertex) {
693
+ throw new Error("Root vertex not found");
694
+ }
695
+ return new Vertex(this, rootVertex);
696
+ }
537
697
  getMoveOps() {
538
698
  return this.moveOps;
539
699
  }
@@ -544,13 +704,6 @@ var _RepTree = class _RepTree {
544
704
  const vertex = this.state.getVertex(vertexId);
545
705
  return vertex ? new Vertex(this, vertex) : void 0;
546
706
  }
547
- get rootVertex() {
548
- const rootVertex = this.state.getVertex(this.rootVertexId);
549
- if (!rootVertex) {
550
- throw new Error("Root vertex not found");
551
- }
552
- return new Vertex(this, rootVertex);
553
- }
554
707
  getAllVertices() {
555
708
  return this.state.getAllVertices().map((v) => new Vertex(this, v));
556
709
  }
@@ -601,6 +754,17 @@ var _RepTree = class _RepTree {
601
754
  setMaxDepth(maxDepth) {
602
755
  this.maxDepth = maxDepth;
603
756
  }
757
+ createRoot() {
758
+ if (this.rootVertexId) {
759
+ throw new Error("Root vertex already exists");
760
+ }
761
+ this.rootVertexId = this.newVertexInternalWithUUID(null);
762
+ const rootVertex = this.state.getVertex(this.rootVertexId);
763
+ if (!rootVertex) {
764
+ throw new Error("Root vertex not found");
765
+ }
766
+ return new Vertex(this, rootVertex);
767
+ }
604
768
  newVertex(parentId, props = null) {
605
769
  const typedProps = props;
606
770
  const vertexId = this.newVertexInternalWithUUID(parentId);
@@ -657,6 +821,9 @@ var _RepTree = class _RepTree {
657
821
  path = path.replace(/^\/+/, "");
658
822
  path = path.replace(/\/+$/, "");
659
823
  const pathParts = path.split("/");
824
+ if (!this.rootVertexId) {
825
+ return void 0;
826
+ }
660
827
  const root = this.state.getVertex(this.rootVertexId);
661
828
  if (!root) {
662
829
  throw new Error("The root vertex is not found");
@@ -678,6 +845,9 @@ var _RepTree = class _RepTree {
678
845
  return void 0;
679
846
  }
680
847
  printTree() {
848
+ if (!this.rootVertexId) {
849
+ return "";
850
+ }
681
851
  return this.state.printTree(this.rootVertexId);
682
852
  }
683
853
  merge(ops) {
@@ -701,6 +871,12 @@ var _RepTree = class _RepTree {
701
871
  }
702
872
  }
703
873
  compareStructure(other) {
874
+ if (this.root?.id !== other.root?.id) {
875
+ return false;
876
+ }
877
+ if (!this.rootVertexId) {
878
+ return true;
879
+ }
704
880
  return _RepTree.compareVertices(this.rootVertexId, this, other);
705
881
  }
706
882
  compareMoveOps(other) {
@@ -927,7 +1103,9 @@ var _RepTree = class _RepTree {
927
1103
  }
928
1104
  reportOpAsApplied(op) {
929
1105
  this.knownOps.add(op.id.toString());
930
- this.updateStateVector(op);
1106
+ if (this._stateVectorEnabled) {
1107
+ this.stateVector.updateFromOp(op);
1108
+ }
931
1109
  for (const callback of this.opAppliedCallbacks) {
932
1110
  callback(op);
933
1111
  }
@@ -961,99 +1139,15 @@ var _RepTree = class _RepTree {
961
1139
  this.state.moveVertex(op.targetId, prevParentId);
962
1140
  }
963
1141
  // --- Range-Based State Vector Methods ---
964
- /**
965
- * Updates the state vector with a newly applied operation.
966
- * Assumes ranges are sorted and non-overlapping.
967
- *
968
- * @param op The operation that was just applied
969
- */
970
- updateStateVector(op) {
971
- const peerId = op.id.peerId;
972
- const counter = op.id.counter;
973
- if (!this.stateVector[peerId]) {
974
- this.stateVector[peerId] = [];
975
- }
976
- const ranges = this.stateVector[peerId];
977
- if (ranges.length === 0) {
978
- ranges.push([counter, counter]);
979
- return;
980
- }
981
- let rangeExtendedOrMerged = false;
982
- let insertIndex = -1;
983
- for (let i = 0; i < ranges.length; i++) {
984
- const range = ranges[i];
985
- if (counter >= range[0] && counter <= range[1]) {
986
- rangeExtendedOrMerged = true;
987
- break;
988
- }
989
- if (counter === range[0] - 1) {
990
- range[0] = counter;
991
- rangeExtendedOrMerged = true;
992
- if (i > 0 && range[0] === ranges[i - 1][1] + 1) {
993
- ranges[i - 1][1] = range[1];
994
- ranges.splice(i, 1);
995
- }
996
- break;
997
- }
998
- if (counter === range[1] + 1) {
999
- range[1] = counter;
1000
- rangeExtendedOrMerged = true;
1001
- if (i < ranges.length - 1 && range[1] + 1 === ranges[i + 1][0]) {
1002
- range[1] = ranges[i + 1][1];
1003
- ranges.splice(i + 1, 1);
1004
- }
1005
- break;
1006
- }
1007
- if (counter < range[0] && insertIndex === -1) {
1008
- insertIndex = i;
1009
- }
1010
- }
1011
- if (!rangeExtendedOrMerged) {
1012
- if (insertIndex === -1) {
1013
- insertIndex = ranges.length;
1014
- }
1015
- ranges.splice(insertIndex, 0, [counter, counter]);
1016
- if (insertIndex > 0 && ranges[insertIndex][0] === ranges[insertIndex - 1][1] + 1) {
1017
- ranges[insertIndex - 1][1] = ranges[insertIndex][1];
1018
- ranges.splice(insertIndex, 1);
1019
- insertIndex--;
1020
- }
1021
- if (insertIndex < ranges.length - 1 && ranges[insertIndex][1] + 1 === ranges[insertIndex + 1][0]) {
1022
- ranges[insertIndex][1] = ranges[insertIndex + 1][1];
1023
- ranges.splice(insertIndex + 1, 1);
1024
- }
1025
- }
1026
- }
1027
1142
  /**
1028
1143
  * Returns the current state vector.
1029
1144
  * Returns a readonly reference to the internal state vector.
1030
1145
  */
1031
1146
  getStateVector() {
1032
- return this.stateVector;
1033
- }
1034
- /**
1035
- * Calculates which operation ranges we have that the other peer is missing
1036
- * by comparing state vectors.
1037
- *
1038
- * @param theirStateVector The state vector from another peer
1039
- * @returns Array of operation ID ranges that we have but they don't
1040
- */
1041
- diffStateVectors(theirStateVector) {
1042
- const missingRanges = [];
1043
- for (const [peerId, ourRanges] of Object.entries(this.stateVector)) {
1044
- const theirRanges = theirStateVector[peerId] || [];
1045
- const missing = subtractRanges(ourRanges, theirRanges);
1046
- for (const [start, end] of missing) {
1047
- if (start <= end) {
1048
- missingRanges.push({
1049
- peerId,
1050
- start,
1051
- end
1052
- });
1053
- }
1054
- }
1147
+ if (!this._stateVectorEnabled) {
1148
+ return null;
1055
1149
  }
1056
- return missingRanges;
1150
+ return this.stateVector.getState();
1057
1151
  }
1058
1152
  /**
1059
1153
  * Determines which operations are needed to synchronize
@@ -1063,7 +1157,11 @@ var _RepTree = class _RepTree {
1063
1157
  * @returns Operations that should be sent to the other peer, sorted by OpId.
1064
1158
  */
1065
1159
  getMissingOps(theirStateVector) {
1066
- const missingRanges = this.diffStateVectors(theirStateVector);
1160
+ if (!this._stateVectorEnabled) {
1161
+ return [...this.moveOps, ...this.setPropertyOps];
1162
+ }
1163
+ const otherStateVector = new StateVector(theirStateVector);
1164
+ const missingRanges = this.stateVector.diff(otherStateVector);
1067
1165
  const missingOps = [];
1068
1166
  const allOps = [...this.moveOps, ...this.setPropertyOps];
1069
1167
  for (const op of allOps) {
@@ -1077,6 +1175,26 @@ var _RepTree = class _RepTree {
1077
1175
  missingOps.sort((a, b) => OpId.compare(a.id, b.id));
1078
1176
  return missingOps;
1079
1177
  }
1178
+ /**
1179
+ * Gets or sets whether state vector tracking is enabled
1180
+ */
1181
+ get stateVectorEnabled() {
1182
+ return this._stateVectorEnabled;
1183
+ }
1184
+ /**
1185
+ * Sets the state vector enabled status
1186
+ * When enabled, rebuilds the state vector from existing operations if needed
1187
+ */
1188
+ set stateVectorEnabled(value) {
1189
+ if (value === this._stateVectorEnabled) return;
1190
+ if (value) {
1191
+ this._stateVectorEnabled = true;
1192
+ this.stateVector = StateVector.fromOperations([...this.moveOps, ...this.setPropertyOps]);
1193
+ } else {
1194
+ this._stateVectorEnabled = false;
1195
+ this.stateVector = new StateVector();
1196
+ }
1197
+ }
1080
1198
  };
1081
1199
  _RepTree.NULL_VERTEX_ID = "0";
1082
1200
  _RepTree.DEFAULT_MAX_DEPTH = 1e5;
@@ -1084,6 +1202,7 @@ var RepTree = _RepTree;
1084
1202
  export {
1085
1203
  OpId,
1086
1204
  RepTree,
1205
+ StateVector,
1087
1206
  TreeState,
1088
1207
  Vertex,
1089
1208
  VertexState,