reptree 0.1.1 → 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 +42 -14
- package/dist/index.cjs +369 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -18
- package/dist/index.d.ts +103 -18
- package/dist/index.js +368 -94
- package/dist/index.js.map +1 -1
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -8,11 +8,12 @@ A tree data structure using CRDTs for seamless replication between peers.
|
|
|
8
8
|
|
|
9
9
|
## Description
|
|
10
10
|
|
|
11
|
-
RepTree
|
|
12
|
-
It uses 2 conflict-free replicated data types (CRDTs) to manage seamless replication between peers:
|
|
11
|
+
RepTree uses 2 conflict-free replicated data types (CRDTs) to manage seamless replication between peers:
|
|
13
12
|
- A move tree CRDT is used for the tree structure (https://martin.kleppmann.com/papers/move-op.pdf).
|
|
14
13
|
- A last writer wins (LWW) CRDT is used for properties.
|
|
15
14
|
|
|
15
|
+
RepTree can also be viewed as a hierarchical, distributed database. For more details on its database capabilities, see [RepTree as a Database](docs/database.md).
|
|
16
|
+
|
|
16
17
|
## Installation
|
|
17
18
|
|
|
18
19
|
```bash
|
|
@@ -28,18 +29,45 @@ import { RepTree } from 'reptree';
|
|
|
28
29
|
const tree = new RepTree('peer1');
|
|
29
30
|
|
|
30
31
|
// Root vertex is created automatically
|
|
31
|
-
const rootVertex = tree.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
const rootVertex = tree.createRoot();
|
|
33
|
+
rootVertex.name = 'Project';
|
|
34
|
+
|
|
35
|
+
// Create a folder structure with properties
|
|
36
|
+
const docsFolder = rootVertex.newNamedChild('Docs');
|
|
37
|
+
docsFolder.setProperties({
|
|
38
|
+
type: 'folder',
|
|
39
|
+
icon: 'folder-icon'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const imagesFolder = rootVertex.newNamedChild('Images');
|
|
43
|
+
imagesFolder.setProperties({
|
|
44
|
+
type: 'folder',
|
|
45
|
+
icon: 'image-icon'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add files to folders
|
|
49
|
+
const readmeFile = docsFolder.newNamedChild('README.md');
|
|
50
|
+
readmeFile.setProperties({
|
|
51
|
+
type: 'file',
|
|
52
|
+
size: 2048,
|
|
53
|
+
lastModified: '2023-10-15T14:22:10Z',
|
|
54
|
+
s3Path: 's3://my-bucket/docs/README.md'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const logoFile = imagesFolder.newNamedChild('logo.png');
|
|
58
|
+
logoFile.setProperties({
|
|
59
|
+
type: 'file',
|
|
60
|
+
size: 15360,
|
|
61
|
+
dimensions: '512x512',
|
|
62
|
+
format: 'png',
|
|
63
|
+
s3Path: 's3://my-bucket/images/logo.png'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Move a file to a different folder
|
|
67
|
+
logoFile.moveTo(docsFolder);
|
|
68
|
+
|
|
69
|
+
// Get children of a folder
|
|
70
|
+
const docsFolderContents = docsFolder.children;
|
|
43
71
|
|
|
44
72
|
// Syncing between trees
|
|
45
73
|
const otherTree = new RepTree('peer2');
|
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,6 +496,191 @@ var Vertex = class {
|
|
|
495
496
|
}
|
|
496
497
|
};
|
|
497
498
|
|
|
499
|
+
// src/StateVector.ts
|
|
500
|
+
function subtractRanges(rangesA, rangesB) {
|
|
501
|
+
if (rangesB.length === 0) return rangesA.map((r) => [...r]);
|
|
502
|
+
if (rangesA.length === 0) return [];
|
|
503
|
+
const result = [];
|
|
504
|
+
let indexB = 0;
|
|
505
|
+
for (const rangeA of rangesA) {
|
|
506
|
+
let currentStart = rangeA[0];
|
|
507
|
+
const endA = rangeA[1];
|
|
508
|
+
while (indexB < rangesB.length && rangesB[indexB][1] < currentStart) {
|
|
509
|
+
indexB++;
|
|
510
|
+
}
|
|
511
|
+
while (indexB < rangesB.length && rangesB[indexB][0] <= endA) {
|
|
512
|
+
const startB = rangesB[indexB][0];
|
|
513
|
+
const endB = rangesB[indexB][1];
|
|
514
|
+
if (currentStart < startB) {
|
|
515
|
+
result.push([currentStart, Math.min(endA, startB - 1)]);
|
|
516
|
+
}
|
|
517
|
+
currentStart = Math.max(currentStart, endB + 1);
|
|
518
|
+
if (currentStart > endA) break;
|
|
519
|
+
if (endB >= endA) break;
|
|
520
|
+
if (endB < currentStart) {
|
|
521
|
+
indexB++;
|
|
522
|
+
} else if (startB >= currentStart) {
|
|
523
|
+
indexB++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (currentStart <= endA) {
|
|
527
|
+
result.push([currentStart, endA]);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
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
|
+
|
|
498
684
|
// src/RepTree.ts
|
|
499
685
|
var _RepTree = class _RepTree {
|
|
500
686
|
/**
|
|
@@ -510,31 +696,40 @@ var _RepTree = class _RepTree {
|
|
|
510
696
|
this.localOps = [];
|
|
511
697
|
this.pendingMovesWithMissingParent = /* @__PURE__ */ new Map();
|
|
512
698
|
this.pendingPropertiesWithMissingVertex = /* @__PURE__ */ new Map();
|
|
513
|
-
this.
|
|
699
|
+
this.knownOps = /* @__PURE__ */ new Set();
|
|
514
700
|
this.parentIdBeforeMove = /* @__PURE__ */ new Map();
|
|
515
701
|
this.opAppliedCallbacks = [];
|
|
516
702
|
this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
|
|
703
|
+
this._stateVectorEnabled = true;
|
|
517
704
|
this.peerId = peerId;
|
|
518
705
|
this.state = new TreeState();
|
|
706
|
+
this.stateVector = new StateVector();
|
|
519
707
|
if (ops != null && ops.length > 0) {
|
|
520
|
-
let rootMoveOp;
|
|
521
|
-
for (let i = 0; i < ops.length; i++) {
|
|
522
|
-
if (isMoveVertexOp(ops[i]) && ops[i].parentId === null) {
|
|
523
|
-
rootMoveOp = ops[i];
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
if (rootMoveOp) {
|
|
528
|
-
this.rootVertexId = rootMoveOp.targetId;
|
|
529
|
-
} else {
|
|
530
|
-
throw new Error("The operations has to contain a move operation with a parentId as null to set the root vertex");
|
|
531
|
-
}
|
|
532
708
|
this.applyOps(ops);
|
|
533
|
-
this.
|
|
709
|
+
const root = this.root;
|
|
710
|
+
if (!root) {
|
|
711
|
+
throw new Error("There has to be a root vertex in the operations");
|
|
712
|
+
}
|
|
534
713
|
} else {
|
|
535
|
-
this.
|
|
536
|
-
|
|
714
|
+
this.ensureNullVertex();
|
|
715
|
+
}
|
|
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");
|
|
537
731
|
}
|
|
732
|
+
return new Vertex(this, rootVertex);
|
|
538
733
|
}
|
|
539
734
|
getMoveOps() {
|
|
540
735
|
return this.moveOps;
|
|
@@ -546,13 +741,6 @@ var _RepTree = class _RepTree {
|
|
|
546
741
|
const vertex = this.state.getVertex(vertexId);
|
|
547
742
|
return vertex ? new Vertex(this, vertex) : void 0;
|
|
548
743
|
}
|
|
549
|
-
get rootVertex() {
|
|
550
|
-
const rootVertex = this.state.getVertex(this.rootVertexId);
|
|
551
|
-
if (!rootVertex) {
|
|
552
|
-
throw new Error("Root vertex not found");
|
|
553
|
-
}
|
|
554
|
-
return new Vertex(this, rootVertex);
|
|
555
|
-
}
|
|
556
744
|
getAllVertices() {
|
|
557
745
|
return this.state.getAllVertices().map((v) => new Vertex(this, v));
|
|
558
746
|
}
|
|
@@ -603,6 +791,17 @@ var _RepTree = class _RepTree {
|
|
|
603
791
|
setMaxDepth(maxDepth) {
|
|
604
792
|
this.maxDepth = maxDepth;
|
|
605
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
|
+
}
|
|
606
805
|
newVertex(parentId, props = null) {
|
|
607
806
|
const typedProps = props;
|
|
608
807
|
const vertexId = this.newVertexInternalWithUUID(parentId);
|
|
@@ -635,7 +834,7 @@ var _RepTree = class _RepTree {
|
|
|
635
834
|
this.applyMove(op);
|
|
636
835
|
}
|
|
637
836
|
deleteVertex(vertexId) {
|
|
638
|
-
this.moveVertex(vertexId, _RepTree.
|
|
837
|
+
this.moveVertex(vertexId, _RepTree.NULL_VERTEX_ID);
|
|
639
838
|
}
|
|
640
839
|
setTransientVertexProperty(vertexId, key, value) {
|
|
641
840
|
this.lamportClock++;
|
|
@@ -659,6 +858,9 @@ var _RepTree = class _RepTree {
|
|
|
659
858
|
path = path.replace(/^\/+/, "");
|
|
660
859
|
path = path.replace(/\/+$/, "");
|
|
661
860
|
const pathParts = path.split("/");
|
|
861
|
+
if (!this.rootVertexId) {
|
|
862
|
+
return void 0;
|
|
863
|
+
}
|
|
662
864
|
const root = this.state.getVertex(this.rootVertexId);
|
|
663
865
|
if (!root) {
|
|
664
866
|
throw new Error("The root vertex is not found");
|
|
@@ -680,12 +882,38 @@ var _RepTree = class _RepTree {
|
|
|
680
882
|
return void 0;
|
|
681
883
|
}
|
|
682
884
|
printTree() {
|
|
885
|
+
if (!this.rootVertexId) {
|
|
886
|
+
return "";
|
|
887
|
+
}
|
|
683
888
|
return this.state.printTree(this.rootVertexId);
|
|
684
889
|
}
|
|
685
890
|
merge(ops) {
|
|
686
891
|
this.applyOps(ops);
|
|
687
892
|
}
|
|
893
|
+
/** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
|
|
894
|
+
applyOpsOptimizedForLotsOfMoves(ops) {
|
|
895
|
+
const newMoveOps = ops.filter((op) => isMoveVertexOp(op) && !this.knownOps.has(op.id.toString()));
|
|
896
|
+
if (newMoveOps.length > 0) {
|
|
897
|
+
const allMoveOps = [...this.moveOps, ...newMoveOps];
|
|
898
|
+
allMoveOps.sort((a, b) => OpId.compare(a.id, b.id));
|
|
899
|
+
for (let i = 0, len = allMoveOps.length; i < len; i++) {
|
|
900
|
+
const op = allMoveOps[i];
|
|
901
|
+
this.applyMove(op);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const propertyOps = ops.filter((op) => isSetPropertyOp(op) && !this.knownOps.has(op.id.toString()));
|
|
905
|
+
for (let i = 0, len = propertyOps.length; i < len; i++) {
|
|
906
|
+
const op = propertyOps[i];
|
|
907
|
+
this.applyProperty(op);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
688
910
|
compareStructure(other) {
|
|
911
|
+
if (this.root?.id !== other.root?.id) {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
if (!this.rootVertexId) {
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
689
917
|
return _RepTree.compareVertices(this.rootVertexId, this, other);
|
|
690
918
|
}
|
|
691
919
|
compareMoveOps(other) {
|
|
@@ -795,8 +1023,8 @@ var _RepTree = class _RepTree {
|
|
|
795
1023
|
const vertexId = uuid();
|
|
796
1024
|
return this.newVertexInternal(vertexId, parentId);
|
|
797
1025
|
}
|
|
798
|
-
|
|
799
|
-
const vertexId = _RepTree.
|
|
1026
|
+
ensureNullVertex() {
|
|
1027
|
+
const vertexId = _RepTree.NULL_VERTEX_ID;
|
|
800
1028
|
if (this.state.getVertex(vertexId)) {
|
|
801
1029
|
return;
|
|
802
1030
|
}
|
|
@@ -808,6 +1036,19 @@ var _RepTree = class _RepTree {
|
|
|
808
1036
|
this.lamportClock = operation.id.counter;
|
|
809
1037
|
}
|
|
810
1038
|
}
|
|
1039
|
+
applyPendingMovesForParent(parentId) {
|
|
1040
|
+
if (!this.state.getVertex(parentId)) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const pendingMoves = this.pendingMovesWithMissingParent.get(parentId);
|
|
1044
|
+
if (!pendingMoves) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
this.pendingMovesWithMissingParent.delete(parentId);
|
|
1048
|
+
for (const pendingOp of pendingMoves) {
|
|
1049
|
+
this.applyMove(pendingOp);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
811
1052
|
applyMove(op) {
|
|
812
1053
|
if (op.parentId !== null && !this.state.getVertex(op.parentId)) {
|
|
813
1054
|
if (!this.pendingMovesWithMissingParent.has(op.parentId)) {
|
|
@@ -842,15 +1083,52 @@ var _RepTree = class _RepTree {
|
|
|
842
1083
|
}
|
|
843
1084
|
this.applyPendingMovesForParent(op.targetId);
|
|
844
1085
|
}
|
|
845
|
-
|
|
846
|
-
this.
|
|
847
|
-
|
|
848
|
-
|
|
1086
|
+
setPropertyAndItsOpId(op) {
|
|
1087
|
+
this.propertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
|
|
1088
|
+
this.state.setProperty(op.targetId, op.key, op.value);
|
|
1089
|
+
this.reportOpAsApplied(op);
|
|
1090
|
+
}
|
|
1091
|
+
setTransientPropertyAndItsOpId(op) {
|
|
1092
|
+
this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
|
|
1093
|
+
this.state.setTransientProperty(op.targetId, op.key, op.value);
|
|
1094
|
+
this.reportOpAsApplied(op);
|
|
1095
|
+
}
|
|
1096
|
+
applyProperty(op) {
|
|
1097
|
+
const targetVertex = this.state.getVertex(op.targetId);
|
|
1098
|
+
if (!targetVertex) {
|
|
1099
|
+
if (op.transient) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (!this.pendingPropertiesWithMissingVertex.has(op.targetId)) {
|
|
1103
|
+
this.pendingPropertiesWithMissingVertex.set(op.targetId, []);
|
|
1104
|
+
}
|
|
1105
|
+
this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.updateLamportClock(op);
|
|
1109
|
+
const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
|
|
1110
|
+
const prevProp = targetVertex.getProperty(op.key);
|
|
1111
|
+
const prevOpId = this.propertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
|
|
1112
|
+
if (!op.transient) {
|
|
1113
|
+
this.setPropertyOps.push(op);
|
|
1114
|
+
if (!prevProp || !prevOpId || op.id.isGreaterThan(prevOpId)) {
|
|
1115
|
+
this.setPropertyAndItsOpId(op);
|
|
1116
|
+
} else {
|
|
1117
|
+
this.knownOps.add(op.id.toString());
|
|
1118
|
+
}
|
|
1119
|
+
if (prevTransientOpId && op.id.isGreaterThan(prevTransientOpId)) {
|
|
1120
|
+
this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
|
|
1121
|
+
targetVertex.removeTransientProperty(op.key);
|
|
1122
|
+
}
|
|
1123
|
+
} else {
|
|
1124
|
+
if (!prevTransientOpId || op.id.isGreaterThan(prevTransientOpId)) {
|
|
1125
|
+
this.setTransientPropertyAndItsOpId(op);
|
|
1126
|
+
}
|
|
849
1127
|
}
|
|
850
1128
|
}
|
|
851
1129
|
applyOps(ops) {
|
|
852
1130
|
for (const op of ops) {
|
|
853
|
-
if (this.
|
|
1131
|
+
if (this.knownOps.has(op.id.toString())) {
|
|
854
1132
|
continue;
|
|
855
1133
|
}
|
|
856
1134
|
if (isMoveVertexOp(op)) {
|
|
@@ -860,34 +1138,13 @@ var _RepTree = class _RepTree {
|
|
|
860
1138
|
}
|
|
861
1139
|
}
|
|
862
1140
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const allMoveOps = [...this.moveOps, ...newMoveOps];
|
|
868
|
-
allMoveOps.sort((a, b) => OpId.compare(a.id, b.id));
|
|
869
|
-
for (let i = 0, len = allMoveOps.length; i < len; i++) {
|
|
870
|
-
const op = allMoveOps[i];
|
|
871
|
-
this.applyMove(op);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
const propertyOps = ops.filter((op) => isSetPropertyOp(op) && !this.appliedOps.has(op.id.toString()));
|
|
875
|
-
for (let i = 0, len = propertyOps.length; i < len; i++) {
|
|
876
|
-
const op = propertyOps[i];
|
|
877
|
-
this.applyProperty(op);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
applyPendingMovesForParent(parentId) {
|
|
881
|
-
if (!this.state.getVertex(parentId)) {
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
const pendingMoves = this.pendingMovesWithMissingParent.get(parentId);
|
|
885
|
-
if (!pendingMoves) {
|
|
886
|
-
return;
|
|
1141
|
+
reportOpAsApplied(op) {
|
|
1142
|
+
this.knownOps.add(op.id.toString());
|
|
1143
|
+
if (this._stateVectorEnabled) {
|
|
1144
|
+
this.stateVector.updateFromOp(op);
|
|
887
1145
|
}
|
|
888
|
-
this.
|
|
889
|
-
|
|
890
|
-
this.applyMove(pendingOp);
|
|
1146
|
+
for (const callback of this.opAppliedCallbacks) {
|
|
1147
|
+
callback(op);
|
|
891
1148
|
}
|
|
892
1149
|
}
|
|
893
1150
|
tryToMove(op) {
|
|
@@ -900,6 +1157,7 @@ var _RepTree = class _RepTree {
|
|
|
900
1157
|
this.state.moveVertex(op.targetId, op.parentId);
|
|
901
1158
|
if (!targetVertex) {
|
|
902
1159
|
const pendingProperties = this.pendingPropertiesWithMissingVertex.get(op.targetId) || [];
|
|
1160
|
+
this.pendingPropertiesWithMissingVertex.delete(op.targetId);
|
|
903
1161
|
for (const prop of pendingProperties) {
|
|
904
1162
|
this.setPropertyAndItsOpId(prop);
|
|
905
1163
|
}
|
|
@@ -917,55 +1175,72 @@ var _RepTree = class _RepTree {
|
|
|
917
1175
|
}
|
|
918
1176
|
this.state.moveVertex(op.targetId, prevParentId);
|
|
919
1177
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
this.
|
|
927
|
-
|
|
928
|
-
|
|
1178
|
+
// --- Range-Based State Vector Methods ---
|
|
1179
|
+
/**
|
|
1180
|
+
* Returns the current state vector.
|
|
1181
|
+
* Returns a readonly reference to the internal state vector.
|
|
1182
|
+
*/
|
|
1183
|
+
getStateVector() {
|
|
1184
|
+
if (!this._stateVectorEnabled) {
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
return this.stateVector.getState();
|
|
929
1188
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1189
|
+
/**
|
|
1190
|
+
* Determines which operations are needed to synchronize
|
|
1191
|
+
* with the provided state vector.
|
|
1192
|
+
*
|
|
1193
|
+
* @param theirStateVector The state vector from another peer
|
|
1194
|
+
* @returns Operations that should be sent to the other peer, sorted by OpId.
|
|
1195
|
+
*/
|
|
1196
|
+
getMissingOps(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);
|
|
1202
|
+
const missingOps = [];
|
|
1203
|
+
const allOps = [...this.moveOps, ...this.setPropertyOps];
|
|
1204
|
+
for (const op of allOps) {
|
|
1205
|
+
for (const range of missingRanges) {
|
|
1206
|
+
if (op.id.peerId === range.peerId && op.id.counter >= range.start && op.id.counter <= range.end) {
|
|
1207
|
+
missingOps.push(op);
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
938
1210
|
}
|
|
939
|
-
this.pendingPropertiesWithMissingVertex.get(op.targetId).push(op);
|
|
940
|
-
return;
|
|
941
1211
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1212
|
+
missingOps.sort((a, b) => OpId.compare(a.id, b.id));
|
|
1213
|
+
return missingOps;
|
|
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]);
|
|
955
1230
|
} else {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
}
|
|
1231
|
+
this._stateVectorEnabled = false;
|
|
1232
|
+
this.stateVector = new StateVector();
|
|
959
1233
|
}
|
|
960
1234
|
}
|
|
961
1235
|
};
|
|
962
|
-
_RepTree.
|
|
1236
|
+
_RepTree.NULL_VERTEX_ID = "0";
|
|
963
1237
|
_RepTree.DEFAULT_MAX_DEPTH = 1e5;
|
|
964
1238
|
var RepTree = _RepTree;
|
|
965
1239
|
// Annotate the CommonJS export names for ESM import in node:
|
|
966
1240
|
0 && (module.exports = {
|
|
967
1241
|
OpId,
|
|
968
1242
|
RepTree,
|
|
1243
|
+
StateVector,
|
|
969
1244
|
TreeState,
|
|
970
1245
|
Vertex,
|
|
971
1246
|
VertexState,
|