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/dist/index.js CHANGED
@@ -460,6 +460,38 @@ var Vertex = class {
460
460
  };
461
461
 
462
462
  // src/RepTree.ts
463
+ function subtractRanges(rangesA, rangesB) {
464
+ if (rangesB.length === 0) return rangesA.map((r) => [...r]);
465
+ if (rangesA.length === 0) return [];
466
+ const result = [];
467
+ let indexB = 0;
468
+ for (const rangeA of rangesA) {
469
+ let currentStart = rangeA[0];
470
+ const endA = rangeA[1];
471
+ while (indexB < rangesB.length && rangesB[indexB][1] < currentStart) {
472
+ indexB++;
473
+ }
474
+ while (indexB < rangesB.length && rangesB[indexB][0] <= endA) {
475
+ const startB = rangesB[indexB][0];
476
+ const endB = rangesB[indexB][1];
477
+ if (currentStart < startB) {
478
+ result.push([currentStart, Math.min(endA, startB - 1)]);
479
+ }
480
+ currentStart = Math.max(currentStart, endB + 1);
481
+ if (currentStart > endA) break;
482
+ if (endB >= endA) break;
483
+ if (endB < currentStart) {
484
+ indexB++;
485
+ } else if (startB >= currentStart) {
486
+ indexB++;
487
+ }
488
+ }
489
+ if (currentStart <= endA) {
490
+ result.push([currentStart, endA]);
491
+ }
492
+ }
493
+ return result;
494
+ }
463
495
  var _RepTree = class _RepTree {
464
496
  /**
465
497
  * @param peerId - The peer ID of the current client
@@ -474,10 +506,12 @@ var _RepTree = class _RepTree {
474
506
  this.localOps = [];
475
507
  this.pendingMovesWithMissingParent = /* @__PURE__ */ new Map();
476
508
  this.pendingPropertiesWithMissingVertex = /* @__PURE__ */ new Map();
477
- this.appliedOps = /* @__PURE__ */ new Set();
509
+ this.knownOps = /* @__PURE__ */ new Set();
478
510
  this.parentIdBeforeMove = /* @__PURE__ */ new Map();
479
511
  this.opAppliedCallbacks = [];
480
512
  this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
513
+ // State vector tracking operations from each peer
514
+ this.stateVector = {};
481
515
  this.peerId = peerId;
482
516
  this.state = new TreeState();
483
517
  if (ops != null && ops.length > 0) {
@@ -494,10 +528,10 @@ var _RepTree = class _RepTree {
494
528
  throw new Error("The operations has to contain a move operation with a parentId as null to set the root vertex");
495
529
  }
496
530
  this.applyOps(ops);
497
- this.ensureTrashVertex();
531
+ this.ensureNullVertex();
498
532
  } else {
499
533
  this.rootVertexId = this.newVertexInternalWithUUID(null);
500
- this.ensureTrashVertex();
534
+ this.ensureNullVertex();
501
535
  }
502
536
  }
503
537
  getMoveOps() {
@@ -599,7 +633,7 @@ var _RepTree = class _RepTree {
599
633
  this.applyMove(op);
600
634
  }
601
635
  deleteVertex(vertexId) {
602
- this.moveVertex(vertexId, _RepTree.TRASH_VERTEX_ID);
636
+ this.moveVertex(vertexId, _RepTree.NULL_VERTEX_ID);
603
637
  }
604
638
  setTransientVertexProperty(vertexId, key, value) {
605
639
  this.lamportClock++;
@@ -649,6 +683,23 @@ var _RepTree = class _RepTree {
649
683
  merge(ops) {
650
684
  this.applyOps(ops);
651
685
  }
686
+ /** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
687
+ applyOpsOptimizedForLotsOfMoves(ops) {
688
+ const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(op.id.toString()));
689
+ if (newMoveOps.length > 0) {
690
+ const allMoveOps = [...this.moveOps, ...newMoveOps];
691
+ allMoveOps.sort((a, b) => OpId.compare(a.id, b.id));
692
+ for (let i = 0, len = allMoveOps.length; i < len; i++) {
693
+ const op = allMoveOps[i];
694
+ this.applyMove(op);
695
+ }
696
+ }
697
+ const propertyOps = ops.filter((op) => isSetPropertyOp(op) && !this.knownOps.has(op.id.toString()));
698
+ for (let i = 0, len = propertyOps.length; i < len; i++) {
699
+ const op = propertyOps[i];
700
+ this.applyProperty(op);
701
+ }
702
+ }
652
703
  compareStructure(other) {
653
704
  return _RepTree.compareVertices(this.rootVertexId, this, other);
654
705
  }
@@ -759,8 +810,8 @@ var _RepTree = class _RepTree {
759
810
  const vertexId = uuid();
760
811
  return this.newVertexInternal(vertexId, parentId);
761
812
  }
762
- ensureTrashVertex() {
763
- const vertexId = _RepTree.TRASH_VERTEX_ID;
813
+ ensureNullVertex() {
814
+ const vertexId = _RepTree.NULL_VERTEX_ID;
764
815
  if (this.state.getVertex(vertexId)) {
765
816
  return;
766
817
  }
@@ -772,6 +823,19 @@ var _RepTree = class _RepTree {
772
823
  this.lamportClock = operation.id.counter;
773
824
  }
774
825
  }
826
+ applyPendingMovesForParent(parentId) {
827
+ if (!this.state.getVertex(parentId)) {
828
+ return;
829
+ }
830
+ const pendingMoves = this.pendingMovesWithMissingParent.get(parentId);
831
+ if (!pendingMoves) {
832
+ return;
833
+ }
834
+ this.pendingMovesWithMissingParent.delete(parentId);
835
+ for (const pendingOp of pendingMoves) {
836
+ this.applyMove(pendingOp);
837
+ }
838
+ }
775
839
  applyMove(op) {
776
840
  if (op.parentId !== null && !this.state.getVertex(op.parentId)) {
777
841
  if (!this.pendingMovesWithMissingParent.has(op.parentId)) {
@@ -806,15 +870,52 @@ var _RepTree = class _RepTree {
806
870
  }
807
871
  this.applyPendingMovesForParent(op.targetId);
808
872
  }
809
- reportOpAsApplied(op) {
810
- this.appliedOps.add(op.id.toString());
811
- for (const callback of this.opAppliedCallbacks) {
812
- callback(op);
873
+ setPropertyAndItsOpId(op) {
874
+ this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
875
+ this.state.setProperty(op.targetId, op.key, op.value);
876
+ this.reportOpAsApplied(op);
877
+ }
878
+ setTransientPropertyAndItsOpId(op) {
879
+ this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
880
+ this.state.setTransientProperty(op.targetId, op.key, op.value);
881
+ this.reportOpAsApplied(op);
882
+ }
883
+ applyProperty(op) {
884
+ const targetVertex = this.state.getVertex(op.targetId);
885
+ if (!targetVertex) {
886
+ if (op.transient) {
887
+ return;
888
+ }
889
+ if (!this.pendingPropertiesWithMissingVertex.has(op.targetId)) {
890
+ this.pendingPropertiesWithMissingVertex.set(op.targetId, []);
891
+ }
892
+ this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
893
+ return;
894
+ }
895
+ this.updateLamportClock(op);
896
+ const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
897
+ const prevProp = targetVertex.getProperty(op.key);
898
+ const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
899
+ if (!op.transient) {
900
+ this.setPropertyOps.push(op);
901
+ if (!prevProp || !prevOpId || op.id.isGreaterThan(prevOpId)) {
902
+ this.setPropertyAndItsOpId(op);
903
+ } else {
904
+ this.knownOps.add(op.id.toString());
905
+ }
906
+ if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
907
+ this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
908
+ targetVertex.removeTransientProperty(op.key);
909
+ }
910
+ } else {
911
+ if (!prevTransientOpId || op.id.isGreaterThan(prevTransientOpId)) {
912
+ this.setTransientPropertyAndItsOpId(op);
913
+ }
813
914
  }
814
915
  }
815
916
  applyOps(ops) {
816
917
  for (const op of ops) {
817
- if (this.appliedOps.has(op.id.toString())) {
918
+ if (this.knownOps.has(op.id.toString())) {
818
919
  continue;
819
920
  }
820
921
  if (isMoveVertexOp(op)) {
@@ -824,34 +925,11 @@ var _RepTree = class _RepTree {
824
925
  }
825
926
  }
826
927
  }
827
- /** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
828
- applyOpsOptimizedForLotsOfMoves(ops) {
829
- const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.appliedOps.has(op.id.toString()));
830
- if (newMoveOps.length > 0) {
831
- const allMoveOps = [...this.moveOps, ...newMoveOps];
832
- allMoveOps.sort((a, b) => OpId.compare(a.id, b.id));
833
- for (let i = 0, len = allMoveOps.length; i < len; i++) {
834
- const op = allMoveOps[i];
835
- this.applyMove(op);
836
- }
837
- }
838
- const propertyOps = ops.filter((op) => isSetPropertyOp(op) && !this.appliedOps.has(op.id.toString()));
839
- for (let i = 0, len = propertyOps.length; i < len; i++) {
840
- const op = propertyOps[i];
841
- this.applyProperty(op);
842
- }
843
- }
844
- applyPendingMovesForParent(parentId) {
845
- if (!this.state.getVertex(parentId)) {
846
- return;
847
- }
848
- const pendingMoves = this.pendingMovesWithMissingParent.get(parentId);
849
- if (!pendingMoves) {
850
- return;
851
- }
852
- this.pendingMovesWithMissingParent.delete(parentId);
853
- for (const pendingOp of pendingMoves) {
854
- this.applyMove(pendingOp);
928
+ reportOpAsApplied(op) {
929
+ this.knownOps.add(op.id.toString());
930
+ this.updateStateVector(op);
931
+ for (const callback of this.opAppliedCallbacks) {
932
+ callback(op);
855
933
  }
856
934
  }
857
935
  tryToMove(op) {
@@ -864,6 +942,7 @@ var _RepTree = class _RepTree {
864
942
  this.state.moveVertex(op.targetId, op.parentId);
865
943
  if (!targetVertex) {
866
944
  const pendingProperties = this.pendingPropertiesWithMissingVertex.get(op.targetId) || [];
945
+ this.pendingPropertiesWithMissingVertex.delete(op.targetId);
867
946
  for (const prop of pendingProperties) {
868
947
  this.setPropertyAndItsOpId(prop);
869
948
  }
@@ -881,49 +960,125 @@ var _RepTree = class _RepTree {
881
960
  }
882
961
  this.state.moveVertex(op.targetId, prevParentId);
883
962
  }
884
- setPropertyAndItsOpId(op) {
885
- this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
886
- this.state.setProperty(op.targetId, op.key, op.value);
887
- this.reportOpAsApplied(op);
888
- }
889
- setTransientPropertyAndItsOpId(op) {
890
- this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
891
- this.state.setTransientProperty(op.targetId, op.key, op.value);
892
- this.reportOpAsApplied(op);
893
- }
894
- applyProperty(op) {
895
- const targetVertex = this.state.getVertex(op.targetId);
896
- if (!targetVertex) {
897
- if (op.transient) {
898
- return;
963
+ // --- 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;
899
988
  }
900
- if (!this.pendingPropertiesWithMissingVertex.has(op.targetId)) {
901
- this.pendingPropertiesWithMissingVertex.set(op.targetId, []);
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;
902
1009
  }
903
- this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
904
- return;
905
1010
  }
906
- this.updateLamportClock(op);
907
- const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
908
- const prevProp = targetVertex.getProperty(op.key);
909
- const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
910
- if (!op.transient) {
911
- this.setPropertyOps.push(op);
912
- if (!prevProp || !prevOpId || op.id.isGreaterThan(prevOpId)) {
913
- this.setPropertyAndItsOpId(op);
1011
+ if (!rangeExtendedOrMerged) {
1012
+ if (insertIndex === -1) {
1013
+ insertIndex = ranges.length;
914
1014
  }
915
- if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
916
- this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
917
- targetVertex.removeTransientProperty(op.key);
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--;
918
1020
  }
919
- } else {
920
- if (!prevTransientOpId || op.id.isGreaterThan(prevTransientOpId)) {
921
- this.setTransientPropertyAndItsOpId(op);
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
+ /**
1028
+ * Returns the current state vector.
1029
+ * Returns a readonly reference to the internal state vector.
1030
+ */
1031
+ 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
+ }
1055
+ }
1056
+ return missingRanges;
1057
+ }
1058
+ /**
1059
+ * Determines which operations are needed to synchronize
1060
+ * with the provided state vector.
1061
+ *
1062
+ * @param theirStateVector The state vector from another peer
1063
+ * @returns Operations that should be sent to the other peer, sorted by OpId.
1064
+ */
1065
+ getMissingOps(theirStateVector) {
1066
+ const missingRanges = this.diffStateVectors(theirStateVector);
1067
+ const missingOps = [];
1068
+ const allOps = [...this.moveOps, ...this.setPropertyOps];
1069
+ for (const op of allOps) {
1070
+ for (const range of missingRanges) {
1071
+ if (op.id.peerId === range.peerId && op.id.counter >= range.start && op.id.counter <= range.end) {
1072
+ missingOps.push(op);
1073
+ break;
1074
+ }
922
1075
  }
923
1076
  }
1077
+ missingOps.sort((a, b) => OpId.compare(a.id, b.id));
1078
+ return missingOps;
924
1079
  }
925
1080
  };
926
- _RepTree.TRASH_VERTEX_ID = "t";
1081
+ _RepTree.NULL_VERTEX_ID = "0";
927
1082
  _RepTree.DEFAULT_MAX_DEPTH = 1e5;
928
1083
  var RepTree = _RepTree;
929
1084
  export {