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 +4 -6
- package/dist/index.cjs +252 -120
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -22
- package/dist/index.d.ts +75 -22
- package/dist/index.js +251 -120
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
* @param peerId - The peer ID of the current client
|
|
144
|
-
* @param ops - The operations to replicate an existing tree, if
|
|
144
|
+
* @param peerId - The peer ID of the current client. Should be unique across all peers.
|
|
145
|
+
* @param ops - The operations to replicate an existing tree, if not provided - an empty tree will be created without a root vertex
|
|
145
146
|
*/
|
|
146
|
-
constructor(peerId: string, ops?: ReadonlyArray<VertexOperation>
|
|
147
|
+
constructor(peerId: string, ops?: ReadonlyArray<VertexOperation>);
|
|
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
|
@@ -324,9 +324,21 @@ var TreeState = class {
|
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
const children = this.getChildrenIds(vertexId);
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
const
|
|
327
|
+
const sortedChildren = [...children].sort((a, b) => {
|
|
328
|
+
const vertexA = this.getVertex(a);
|
|
329
|
+
const vertexB = this.getVertex(b);
|
|
330
|
+
const nameA = vertexA?.getProperty("_n");
|
|
331
|
+
const nameB = vertexB?.getProperty("_n");
|
|
332
|
+
if (nameA && nameB) {
|
|
333
|
+
return nameA.localeCompare(nameB);
|
|
334
|
+
}
|
|
335
|
+
if (nameA) return -1;
|
|
336
|
+
if (nameB) return 1;
|
|
337
|
+
return a.localeCompare(b);
|
|
338
|
+
});
|
|
339
|
+
for (let i = 0; i < sortedChildren.length; i++) {
|
|
340
|
+
const childId = sortedChildren[i];
|
|
341
|
+
const isLastChild = i === sortedChildren.length - 1;
|
|
330
342
|
result += this.printTree(childId, indent + (isLast ? " " : "\u2502 "), isLastChild);
|
|
331
343
|
}
|
|
332
344
|
return result;
|
|
@@ -459,7 +471,7 @@ var Vertex = class {
|
|
|
459
471
|
}
|
|
460
472
|
};
|
|
461
473
|
|
|
462
|
-
// src/
|
|
474
|
+
// src/StateVector.ts
|
|
463
475
|
function subtractRanges(rangesA, rangesB) {
|
|
464
476
|
if (rangesB.length === 0) return rangesA.map((r) => [...r]);
|
|
465
477
|
if (rangesA.length === 0) return [];
|
|
@@ -492,12 +504,165 @@ function subtractRanges(rangesA, rangesB) {
|
|
|
492
504
|
}
|
|
493
505
|
return result;
|
|
494
506
|
}
|
|
507
|
+
var StateVector = class _StateVector {
|
|
508
|
+
/**
|
|
509
|
+
* Creates a new StateVector.
|
|
510
|
+
* @param initialState Optional initial state to copy from
|
|
511
|
+
*/
|
|
512
|
+
constructor(initialState = {}) {
|
|
513
|
+
this.ranges = {};
|
|
514
|
+
for (const [peerId, peerRanges] of Object.entries(initialState)) {
|
|
515
|
+
this.ranges[peerId] = peerRanges.map((range) => [...range]);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Updates the state vector with a newly applied operation.
|
|
520
|
+
* Assumes ranges are sorted and non-overlapping.
|
|
521
|
+
*
|
|
522
|
+
* @param peerId The peer ID of the operation
|
|
523
|
+
* @param counter The counter value of the operation
|
|
524
|
+
*/
|
|
525
|
+
update(peerId, counter) {
|
|
526
|
+
if (!this.ranges[peerId]) {
|
|
527
|
+
this.ranges[peerId] = [];
|
|
528
|
+
}
|
|
529
|
+
const ranges = this.ranges[peerId];
|
|
530
|
+
if (ranges.length === 0) {
|
|
531
|
+
ranges.push([counter, counter]);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
let rangeExtendedOrMerged = false;
|
|
535
|
+
let insertIndex = -1;
|
|
536
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
537
|
+
const range = ranges[i];
|
|
538
|
+
if (counter >= range[0] && counter <= range[1]) {
|
|
539
|
+
rangeExtendedOrMerged = true;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
if (counter === range[0] - 1) {
|
|
543
|
+
range[0] = counter;
|
|
544
|
+
rangeExtendedOrMerged = true;
|
|
545
|
+
if (i > 0 && range[0] === ranges[i - 1][1] + 1) {
|
|
546
|
+
ranges[i - 1][1] = range[1];
|
|
547
|
+
ranges.splice(i, 1);
|
|
548
|
+
}
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
if (counter === range[1] + 1) {
|
|
552
|
+
range[1] = counter;
|
|
553
|
+
rangeExtendedOrMerged = true;
|
|
554
|
+
if (i < ranges.length - 1 && range[1] + 1 === ranges[i + 1][0]) {
|
|
555
|
+
range[1] = ranges[i + 1][1];
|
|
556
|
+
ranges.splice(i + 1, 1);
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
if (counter < range[0] && insertIndex === -1) {
|
|
561
|
+
insertIndex = i;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!rangeExtendedOrMerged) {
|
|
565
|
+
if (insertIndex === -1) {
|
|
566
|
+
insertIndex = ranges.length;
|
|
567
|
+
}
|
|
568
|
+
ranges.splice(insertIndex, 0, [counter, counter]);
|
|
569
|
+
if (insertIndex > 0 && ranges[insertIndex][0] === ranges[insertIndex - 1][1] + 1) {
|
|
570
|
+
ranges[insertIndex - 1][1] = ranges[insertIndex][1];
|
|
571
|
+
ranges.splice(insertIndex, 1);
|
|
572
|
+
insertIndex--;
|
|
573
|
+
}
|
|
574
|
+
if (insertIndex < ranges.length - 1 && ranges[insertIndex][1] + 1 === ranges[insertIndex + 1][0]) {
|
|
575
|
+
ranges[insertIndex][1] = ranges[insertIndex + 1][1];
|
|
576
|
+
ranges.splice(insertIndex + 1, 1);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Updates the state vector with a newly applied operation.
|
|
582
|
+
*
|
|
583
|
+
* @param op The operation that was just applied
|
|
584
|
+
*/
|
|
585
|
+
updateFromOp(op) {
|
|
586
|
+
this.update(op.id.peerId, op.id.counter);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Returns the current state vector.
|
|
590
|
+
* Returns a readonly reference to the internal state.
|
|
591
|
+
*/
|
|
592
|
+
getState() {
|
|
593
|
+
return this.ranges;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Calculates which operation ranges we have that the other state vector is missing
|
|
597
|
+
* by comparing state vectors.
|
|
598
|
+
*
|
|
599
|
+
* @param other The other state vector to compare against
|
|
600
|
+
* @returns Array of operation ID ranges that we have but they don't
|
|
601
|
+
*/
|
|
602
|
+
diff(other) {
|
|
603
|
+
const missingRanges = [];
|
|
604
|
+
const theirState = other.getState();
|
|
605
|
+
for (const [peerId, ourRanges] of Object.entries(this.ranges)) {
|
|
606
|
+
const theirRanges = theirState[peerId] || [];
|
|
607
|
+
const missing = subtractRanges(ourRanges, theirRanges);
|
|
608
|
+
for (const [start, end] of missing) {
|
|
609
|
+
if (start <= end) {
|
|
610
|
+
missingRanges.push({
|
|
611
|
+
peerId,
|
|
612
|
+
start,
|
|
613
|
+
end
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return missingRanges;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Checks if the state vector contains the given operation ID
|
|
622
|
+
*
|
|
623
|
+
* @param opId The operation ID to check
|
|
624
|
+
* @returns true if the operation is in the state vector, false otherwise
|
|
625
|
+
*/
|
|
626
|
+
contains(opId) {
|
|
627
|
+
const peerId = opId.peerId;
|
|
628
|
+
const counter = opId.counter;
|
|
629
|
+
if (!this.ranges[peerId]) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
for (const [start, end] of this.ranges[peerId]) {
|
|
633
|
+
if (counter >= start && counter <= end) {
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Creates a copy of this state vector
|
|
641
|
+
*/
|
|
642
|
+
clone() {
|
|
643
|
+
return new _StateVector(this.ranges);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Builds a state vector from an array of operations
|
|
647
|
+
* @param operations The operations to build the state vector from
|
|
648
|
+
* @returns A new StateVector instance
|
|
649
|
+
*/
|
|
650
|
+
static fromOperations(operations) {
|
|
651
|
+
const stateVector = new _StateVector();
|
|
652
|
+
for (const op of operations) {
|
|
653
|
+
stateVector.updateFromOp(op);
|
|
654
|
+
}
|
|
655
|
+
return stateVector;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// src/RepTree.ts
|
|
495
660
|
var _RepTree = class _RepTree {
|
|
496
661
|
/**
|
|
497
|
-
* @param peerId - The peer ID of the current client
|
|
498
|
-
* @param ops - The operations to replicate an existing tree, if
|
|
662
|
+
* @param peerId - The peer ID of the current client. Should be unique across all peers.
|
|
663
|
+
* @param ops - The operations to replicate an existing tree, if not provided - an empty tree will be created without a root vertex
|
|
499
664
|
*/
|
|
500
|
-
constructor(peerId, ops
|
|
665
|
+
constructor(peerId, ops) {
|
|
501
666
|
this.lamportClock = 0;
|
|
502
667
|
this.moveOps = [];
|
|
503
668
|
this.setPropertyOps = [];
|
|
@@ -510,30 +675,37 @@ var _RepTree = class _RepTree {
|
|
|
510
675
|
this.parentIdBeforeMove = /* @__PURE__ */ new Map();
|
|
511
676
|
this.opAppliedCallbacks = [];
|
|
512
677
|
this.maxDepth = _RepTree.DEFAULT_MAX_DEPTH;
|
|
513
|
-
|
|
514
|
-
this.stateVector = {};
|
|
678
|
+
this._stateVectorEnabled = true;
|
|
515
679
|
this.peerId = peerId;
|
|
516
680
|
this.state = new TreeState();
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
}
|
|
681
|
+
this.stateVector = new StateVector();
|
|
682
|
+
if (ops && ops.length > 0) {
|
|
530
683
|
this.applyOps(ops);
|
|
531
|
-
this.
|
|
684
|
+
const root = this.root;
|
|
685
|
+
if (!root) {
|
|
686
|
+
throw new Error("There has to be a root vertex in the operations");
|
|
687
|
+
}
|
|
532
688
|
} else {
|
|
533
|
-
this.rootVertexId = this.newVertexInternalWithUUID(null);
|
|
534
689
|
this.ensureNullVertex();
|
|
535
690
|
}
|
|
536
691
|
}
|
|
692
|
+
get root() {
|
|
693
|
+
if (!this.rootVertexId) {
|
|
694
|
+
const vertices = this.state.getAllVertices();
|
|
695
|
+
for (const vertex of vertices) {
|
|
696
|
+
if (vertex.parentId === null && vertex.id !== _RepTree.NULL_VERTEX_ID) {
|
|
697
|
+
this.rootVertexId = vertex.id;
|
|
698
|
+
return new Vertex(this, vertex);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return void 0;
|
|
702
|
+
}
|
|
703
|
+
const rootVertex = this.state.getVertex(this.rootVertexId);
|
|
704
|
+
if (!rootVertex) {
|
|
705
|
+
throw new Error("Root vertex not found");
|
|
706
|
+
}
|
|
707
|
+
return new Vertex(this, rootVertex);
|
|
708
|
+
}
|
|
537
709
|
getMoveOps() {
|
|
538
710
|
return this.moveOps;
|
|
539
711
|
}
|
|
@@ -544,13 +716,6 @@ var _RepTree = class _RepTree {
|
|
|
544
716
|
const vertex = this.state.getVertex(vertexId);
|
|
545
717
|
return vertex ? new Vertex(this, vertex) : void 0;
|
|
546
718
|
}
|
|
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
719
|
getAllVertices() {
|
|
555
720
|
return this.state.getAllVertices().map((v) => new Vertex(this, v));
|
|
556
721
|
}
|
|
@@ -601,6 +766,17 @@ var _RepTree = class _RepTree {
|
|
|
601
766
|
setMaxDepth(maxDepth) {
|
|
602
767
|
this.maxDepth = maxDepth;
|
|
603
768
|
}
|
|
769
|
+
createRoot() {
|
|
770
|
+
if (this.rootVertexId) {
|
|
771
|
+
throw new Error("Root vertex already exists");
|
|
772
|
+
}
|
|
773
|
+
this.rootVertexId = this.newVertexInternalWithUUID(null);
|
|
774
|
+
const rootVertex = this.state.getVertex(this.rootVertexId);
|
|
775
|
+
if (!rootVertex) {
|
|
776
|
+
throw new Error("Root vertex not found");
|
|
777
|
+
}
|
|
778
|
+
return new Vertex(this, rootVertex);
|
|
779
|
+
}
|
|
604
780
|
newVertex(parentId, props = null) {
|
|
605
781
|
const typedProps = props;
|
|
606
782
|
const vertexId = this.newVertexInternalWithUUID(parentId);
|
|
@@ -657,6 +833,9 @@ var _RepTree = class _RepTree {
|
|
|
657
833
|
path = path.replace(/^\/+/, "");
|
|
658
834
|
path = path.replace(/\/+$/, "");
|
|
659
835
|
const pathParts = path.split("/");
|
|
836
|
+
if (!this.rootVertexId) {
|
|
837
|
+
return void 0;
|
|
838
|
+
}
|
|
660
839
|
const root = this.state.getVertex(this.rootVertexId);
|
|
661
840
|
if (!root) {
|
|
662
841
|
throw new Error("The root vertex is not found");
|
|
@@ -678,6 +857,9 @@ var _RepTree = class _RepTree {
|
|
|
678
857
|
return void 0;
|
|
679
858
|
}
|
|
680
859
|
printTree() {
|
|
860
|
+
if (!this.rootVertexId) {
|
|
861
|
+
return "";
|
|
862
|
+
}
|
|
681
863
|
return this.state.printTree(this.rootVertexId);
|
|
682
864
|
}
|
|
683
865
|
merge(ops) {
|
|
@@ -701,6 +883,12 @@ var _RepTree = class _RepTree {
|
|
|
701
883
|
}
|
|
702
884
|
}
|
|
703
885
|
compareStructure(other) {
|
|
886
|
+
if (this.root?.id !== other.root?.id) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
if (!this.rootVertexId) {
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
704
892
|
return _RepTree.compareVertices(this.rootVertexId, this, other);
|
|
705
893
|
}
|
|
706
894
|
compareMoveOps(other) {
|
|
@@ -927,7 +1115,9 @@ var _RepTree = class _RepTree {
|
|
|
927
1115
|
}
|
|
928
1116
|
reportOpAsApplied(op) {
|
|
929
1117
|
this.knownOps.add(op.id.toString());
|
|
930
|
-
this.
|
|
1118
|
+
if (this._stateVectorEnabled) {
|
|
1119
|
+
this.stateVector.updateFromOp(op);
|
|
1120
|
+
}
|
|
931
1121
|
for (const callback of this.opAppliedCallbacks) {
|
|
932
1122
|
callback(op);
|
|
933
1123
|
}
|
|
@@ -961,99 +1151,15 @@ var _RepTree = class _RepTree {
|
|
|
961
1151
|
this.state.moveVertex(op.targetId, prevParentId);
|
|
962
1152
|
}
|
|
963
1153
|
// --- 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
1154
|
/**
|
|
1028
1155
|
* Returns the current state vector.
|
|
1029
1156
|
* Returns a readonly reference to the internal state vector.
|
|
1030
1157
|
*/
|
|
1031
1158
|
getStateVector() {
|
|
1032
|
-
|
|
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
|
-
}
|
|
1159
|
+
if (!this._stateVectorEnabled) {
|
|
1160
|
+
return null;
|
|
1055
1161
|
}
|
|
1056
|
-
return
|
|
1162
|
+
return this.stateVector.getState();
|
|
1057
1163
|
}
|
|
1058
1164
|
/**
|
|
1059
1165
|
* Determines which operations are needed to synchronize
|
|
@@ -1063,7 +1169,11 @@ var _RepTree = class _RepTree {
|
|
|
1063
1169
|
* @returns Operations that should be sent to the other peer, sorted by OpId.
|
|
1064
1170
|
*/
|
|
1065
1171
|
getMissingOps(theirStateVector) {
|
|
1066
|
-
|
|
1172
|
+
if (!this._stateVectorEnabled) {
|
|
1173
|
+
return [...this.moveOps, ...this.setPropertyOps];
|
|
1174
|
+
}
|
|
1175
|
+
const otherStateVector = new StateVector(theirStateVector);
|
|
1176
|
+
const missingRanges = this.stateVector.diff(otherStateVector);
|
|
1067
1177
|
const missingOps = [];
|
|
1068
1178
|
const allOps = [...this.moveOps, ...this.setPropertyOps];
|
|
1069
1179
|
for (const op of allOps) {
|
|
@@ -1077,6 +1187,26 @@ var _RepTree = class _RepTree {
|
|
|
1077
1187
|
missingOps.sort((a, b) => OpId.compare(a.id, b.id));
|
|
1078
1188
|
return missingOps;
|
|
1079
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Gets or sets whether state vector tracking is enabled
|
|
1192
|
+
*/
|
|
1193
|
+
get stateVectorEnabled() {
|
|
1194
|
+
return this._stateVectorEnabled;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Sets the state vector enabled status
|
|
1198
|
+
* When enabled, rebuilds the state vector from existing operations if needed
|
|
1199
|
+
*/
|
|
1200
|
+
set stateVectorEnabled(value) {
|
|
1201
|
+
if (value === this._stateVectorEnabled) return;
|
|
1202
|
+
if (value) {
|
|
1203
|
+
this._stateVectorEnabled = true;
|
|
1204
|
+
this.stateVector = StateVector.fromOperations([...this.moveOps, ...this.setPropertyOps]);
|
|
1205
|
+
} else {
|
|
1206
|
+
this._stateVectorEnabled = false;
|
|
1207
|
+
this.stateVector = new StateVector();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1080
1210
|
};
|
|
1081
1211
|
_RepTree.NULL_VERTEX_ID = "0";
|
|
1082
1212
|
_RepTree.DEFAULT_MAX_DEPTH = 1e5;
|
|
@@ -1084,6 +1214,7 @@ var RepTree = _RepTree;
|
|
|
1084
1214
|
export {
|
|
1085
1215
|
OpId,
|
|
1086
1216
|
RepTree,
|
|
1217
|
+
StateVector,
|
|
1087
1218
|
TreeState,
|
|
1088
1219
|
Vertex,
|
|
1089
1220
|
VertexState,
|