reptree 0.1.0 → 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/README.md +42 -14
- package/dist/index.cjs +228 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -16
- package/dist/index.d.ts +48 -16
- package/dist/index.js +228 -73
- package/dist/index.js.map +1 -1
- package/package.json +13 -6
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.
|
|
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.
|
|
531
|
+
this.ensureNullVertex();
|
|
498
532
|
} else {
|
|
499
533
|
this.rootVertexId = this.newVertexInternalWithUUID(null);
|
|
500
|
-
this.
|
|
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.
|
|
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
|
-
|
|
763
|
-
const vertexId = _RepTree.
|
|
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
|
-
|
|
810
|
-
this.
|
|
811
|
-
|
|
812
|
-
|
|
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.
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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 (
|
|
901
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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.
|
|
1081
|
+
_RepTree.NULL_VERTEX_ID = "0";
|
|
927
1082
|
_RepTree.DEFAULT_MAX_DEPTH = 1e5;
|
|
928
1083
|
var RepTree = _RepTree;
|
|
929
1084
|
export {
|