reptree 1.0.0 → 1.0.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 +95 -80
- package/dist/index.cjs +110 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -6
- package/dist/index.d.ts +30 -6
- package/dist/index.js +110 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -322,12 +322,17 @@ declare class RepTree {
|
|
|
322
322
|
printTree(): string;
|
|
323
323
|
merge(ops: ReadonlyArray<NodeOperation>): void;
|
|
324
324
|
private applyOps;
|
|
325
|
-
/** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
|
|
326
|
-
private applyOpsOptimizedForLotsOfMoves;
|
|
327
325
|
compareStructure(other: RepTree): boolean;
|
|
328
326
|
compareMoveOps(other: RepTree): boolean;
|
|
329
|
-
/** Checks
|
|
327
|
+
/** Checks whether moving `targetId` under `parentId` would create a cycle. */
|
|
328
|
+
wouldMoveCreateCycle(move: Pick<MoveNode, 'targetId' | 'parentId'>): boolean;
|
|
329
|
+
/**
|
|
330
|
+
* Checks if the given `ancestorId` is an ancestor of `childId` in the tree.
|
|
331
|
+
*
|
|
332
|
+
* @deprecated Use `wouldMoveCreateCycle` for move validation.
|
|
333
|
+
*/
|
|
330
334
|
isAncestor(childId: string, ancestorId: string | null): boolean;
|
|
335
|
+
private hasAncestor;
|
|
331
336
|
observeNode(nodeId: string, callback: (updatedNode: Node) => void): () => void;
|
|
332
337
|
observeNodeMove(callback: (movedNode: Node, isNew: boolean) => void): () => void;
|
|
333
338
|
observe(nodeId: string, callback: (events: NodeChangeEvent[]) => void): () => void;
|
|
@@ -347,12 +352,14 @@ declare class RepTree {
|
|
|
347
352
|
private applyLLWProperty;
|
|
348
353
|
private applyOperation;
|
|
349
354
|
private markOpSeen;
|
|
355
|
+
private reportMoveOpAsApplied;
|
|
356
|
+
private reportPropertyOpAsApplied;
|
|
350
357
|
private reportOpAsApplied;
|
|
351
358
|
private tryToMove;
|
|
352
359
|
private undoMove;
|
|
353
360
|
/**
|
|
354
361
|
* Returns the current state vectors for move and property streams.
|
|
355
|
-
* Returns
|
|
362
|
+
* Returns copies of the internal state vectors.
|
|
356
363
|
*/
|
|
357
364
|
getStateVectors(): {
|
|
358
365
|
move: Readonly<StateVectors["move"]>;
|
|
@@ -382,8 +389,9 @@ declare class RepTree {
|
|
|
382
389
|
parse: (data: unknown) => T;
|
|
383
390
|
}): T;
|
|
384
391
|
private getPropertyOps;
|
|
385
|
-
private
|
|
392
|
+
private recordRetainedPropertyOpInStateVector;
|
|
386
393
|
private getOpKey;
|
|
394
|
+
private getPropertyKey;
|
|
387
395
|
private filterOpsByRanges;
|
|
388
396
|
}
|
|
389
397
|
|
|
@@ -438,9 +446,25 @@ declare class StateVector {
|
|
|
438
446
|
* @param op The operation that was just applied
|
|
439
447
|
*/
|
|
440
448
|
updateFromOp(op: NodeOperation): void;
|
|
449
|
+
/**
|
|
450
|
+
* Removes a single operation ID from the state vector.
|
|
451
|
+
* Assumes ranges are sorted and non-overlapping.
|
|
452
|
+
*
|
|
453
|
+
* @param peerId The peer ID of the operation
|
|
454
|
+
* @param counter The counter value of the operation
|
|
455
|
+
* @returns true if a counter was removed, false if it was absent
|
|
456
|
+
*/
|
|
457
|
+
remove(peerId: string, counter: number): boolean;
|
|
458
|
+
/**
|
|
459
|
+
* Removes the operation ID from the state vector.
|
|
460
|
+
*
|
|
461
|
+
* @param op The operation to remove
|
|
462
|
+
* @returns true if the operation ID was removed, false if it was absent
|
|
463
|
+
*/
|
|
464
|
+
removeFromOp(op: NodeOperation): boolean;
|
|
441
465
|
/**
|
|
442
466
|
* Returns the current state vector.
|
|
443
|
-
* Returns a
|
|
467
|
+
* Returns a deep copy so callers cannot mutate internal state.
|
|
444
468
|
*/
|
|
445
469
|
getState(): Readonly<Record<string, number[][]>>;
|
|
446
470
|
/**
|
package/dist/index.js
CHANGED
|
@@ -750,12 +750,63 @@ var StateVector = class _StateVector {
|
|
|
750
750
|
updateFromOp(op) {
|
|
751
751
|
this.update(op.id.peerId, op.id.counter);
|
|
752
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Removes a single operation ID from the state vector.
|
|
755
|
+
* Assumes ranges are sorted and non-overlapping.
|
|
756
|
+
*
|
|
757
|
+
* @param peerId The peer ID of the operation
|
|
758
|
+
* @param counter The counter value of the operation
|
|
759
|
+
* @returns true if a counter was removed, false if it was absent
|
|
760
|
+
*/
|
|
761
|
+
remove(peerId, counter) {
|
|
762
|
+
const ranges = this.ranges[peerId];
|
|
763
|
+
if (!ranges) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
767
|
+
const range = ranges[i];
|
|
768
|
+
const [start, end] = range;
|
|
769
|
+
if (counter < start) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (counter > end) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (start === end) {
|
|
776
|
+
ranges.splice(i, 1);
|
|
777
|
+
} else if (counter === start) {
|
|
778
|
+
range[0] = start + 1;
|
|
779
|
+
} else if (counter === end) {
|
|
780
|
+
range[1] = end - 1;
|
|
781
|
+
} else {
|
|
782
|
+
ranges.splice(i, 1, [start, counter - 1], [counter + 1, end]);
|
|
783
|
+
}
|
|
784
|
+
if (ranges.length === 0) {
|
|
785
|
+
delete this.ranges[peerId];
|
|
786
|
+
}
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Removes the operation ID from the state vector.
|
|
793
|
+
*
|
|
794
|
+
* @param op The operation to remove
|
|
795
|
+
* @returns true if the operation ID was removed, false if it was absent
|
|
796
|
+
*/
|
|
797
|
+
removeFromOp(op) {
|
|
798
|
+
return this.remove(op.id.peerId, op.id.counter);
|
|
799
|
+
}
|
|
753
800
|
/**
|
|
754
801
|
* Returns the current state vector.
|
|
755
|
-
* Returns a
|
|
802
|
+
* Returns a deep copy so callers cannot mutate internal state.
|
|
756
803
|
*/
|
|
757
804
|
getState() {
|
|
758
|
-
|
|
805
|
+
const state = {};
|
|
806
|
+
for (const [peerId, peerRanges] of Object.entries(this.ranges)) {
|
|
807
|
+
state[peerId] = peerRanges.map((range) => [...range]);
|
|
808
|
+
}
|
|
809
|
+
return state;
|
|
759
810
|
}
|
|
760
811
|
/**
|
|
761
812
|
* Calculates which operation ranges we have that the other state vector is missing
|
|
@@ -766,9 +817,8 @@ var StateVector = class _StateVector {
|
|
|
766
817
|
*/
|
|
767
818
|
diff(other) {
|
|
768
819
|
const missingRanges = [];
|
|
769
|
-
const theirState = other.getState();
|
|
770
820
|
for (const [peerId, ourRanges] of Object.entries(this.ranges)) {
|
|
771
|
-
const theirRanges =
|
|
821
|
+
const theirRanges = other.ranges[peerId] || [];
|
|
772
822
|
const missing = subtractRanges(ourRanges, theirRanges);
|
|
773
823
|
for (const [start, end] of missing) {
|
|
774
824
|
if (start <= end) {
|
|
@@ -1138,23 +1188,6 @@ var _RepTree = class _RepTree {
|
|
|
1138
1188
|
this.applyOperation(op);
|
|
1139
1189
|
}
|
|
1140
1190
|
}
|
|
1141
|
-
/** Applies operations in an optimized way, sorting move ops by OpId to avoid undo-do-redo cycles */
|
|
1142
|
-
applyOpsOptimizedForLotsOfMoves(ops) {
|
|
1143
|
-
const newMoveOps = ops.filter((op) => isMoveNodeOp(op) && !this.knownOps.has(this.getOpKey(op)));
|
|
1144
|
-
if (newMoveOps.length > 0) {
|
|
1145
|
-
const allMoveOps = [...this.moveOps, ...newMoveOps];
|
|
1146
|
-
allMoveOps.sort((a, b) => compareOpId(a.id, b.id));
|
|
1147
|
-
for (let i = 0, len = allMoveOps.length; i < len; i++) {
|
|
1148
|
-
const op = allMoveOps[i];
|
|
1149
|
-
this.applyMove(op);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
const propertyOps = ops.filter((op) => isAnyPropertyOp(op) && !this.knownOps.has(this.getOpKey(op)));
|
|
1153
|
-
for (let i = 0, len = propertyOps.length; i < len; i++) {
|
|
1154
|
-
const op = propertyOps[i];
|
|
1155
|
-
this.applyProperty(op);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
1191
|
compareStructure(other) {
|
|
1159
1192
|
if (this.root?.id !== other.root?.id) {
|
|
1160
1193
|
return false;
|
|
@@ -1177,16 +1210,29 @@ var _RepTree = class _RepTree {
|
|
|
1177
1210
|
}
|
|
1178
1211
|
return true;
|
|
1179
1212
|
}
|
|
1180
|
-
/** Checks
|
|
1213
|
+
/** Checks whether moving `targetId` under `parentId` would create a cycle. */
|
|
1214
|
+
wouldMoveCreateCycle(move) {
|
|
1215
|
+
if (move.targetId === move.parentId) return true;
|
|
1216
|
+
if (move.parentId === null) return false;
|
|
1217
|
+
return this.hasAncestor(move.parentId, move.targetId);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Checks if the given `ancestorId` is an ancestor of `childId` in the tree.
|
|
1221
|
+
*
|
|
1222
|
+
* @deprecated Use `wouldMoveCreateCycle` for move validation.
|
|
1223
|
+
*/
|
|
1181
1224
|
isAncestor(childId, ancestorId) {
|
|
1182
|
-
|
|
1225
|
+
return this.hasAncestor(childId, ancestorId);
|
|
1226
|
+
}
|
|
1227
|
+
hasAncestor(nodeId, ancestorId) {
|
|
1228
|
+
let targetId = nodeId;
|
|
1183
1229
|
let node;
|
|
1184
1230
|
const visitedNodes = /* @__PURE__ */ new Set();
|
|
1185
1231
|
while (node = this.state.getNode(targetId)) {
|
|
1186
1232
|
if (node.parentId === ancestorId) return true;
|
|
1187
1233
|
if (!node.parentId) return false;
|
|
1188
1234
|
if (visitedNodes.has(targetId)) {
|
|
1189
|
-
console.error(`
|
|
1235
|
+
console.error(`hasAncestor: cycle detected in the tree structure.`);
|
|
1190
1236
|
return false;
|
|
1191
1237
|
}
|
|
1192
1238
|
visitedNodes.add(targetId);
|
|
@@ -1307,14 +1353,14 @@ var _RepTree = class _RepTree {
|
|
|
1307
1353
|
this.pendingMovesWithMissingParent.set(op.parentId, []);
|
|
1308
1354
|
}
|
|
1309
1355
|
this.pendingMovesWithMissingParent.get(op.parentId).push(op);
|
|
1310
|
-
this.markOpSeen(op
|
|
1356
|
+
this.markOpSeen(op);
|
|
1311
1357
|
return;
|
|
1312
1358
|
}
|
|
1313
1359
|
this.updateMoveClock(op);
|
|
1314
1360
|
const lastOp = this.moveOps.length > 0 ? this.moveOps[this.moveOps.length - 1] : null;
|
|
1315
1361
|
if (lastOp === null || isOpIdGreaterThan(op.id, lastOp.id)) {
|
|
1316
1362
|
this.moveOps.push(op);
|
|
1317
|
-
this.
|
|
1363
|
+
this.reportMoveOpAsApplied(op);
|
|
1318
1364
|
this.tryToMove(op);
|
|
1319
1365
|
} else {
|
|
1320
1366
|
let targetIndex = this.moveOps.length;
|
|
@@ -1328,7 +1374,7 @@ var _RepTree = class _RepTree {
|
|
|
1328
1374
|
}
|
|
1329
1375
|
}
|
|
1330
1376
|
this.moveOps.splice(targetIndex + 1, 0, op);
|
|
1331
|
-
this.
|
|
1377
|
+
this.reportMoveOpAsApplied(op);
|
|
1332
1378
|
this.tryToMove(op);
|
|
1333
1379
|
for (let i = targetIndex + 2; i < this.moveOps.length; i++) {
|
|
1334
1380
|
this.tryToMove(this.moveOps[i]);
|
|
@@ -1336,16 +1382,16 @@ var _RepTree = class _RepTree {
|
|
|
1336
1382
|
}
|
|
1337
1383
|
this.applyPendingMovesForParent(op.targetId);
|
|
1338
1384
|
}
|
|
1339
|
-
setLLWPropertyAndItsOpId(op) {
|
|
1340
|
-
this.propertyOpsByKey.set(
|
|
1385
|
+
setLLWPropertyAndItsOpId(op, previousOp) {
|
|
1386
|
+
this.propertyOpsByKey.set(this.getPropertyKey(op), op);
|
|
1341
1387
|
this.state.setProperty(op.targetId, op.key, op.value);
|
|
1342
|
-
this.
|
|
1343
|
-
this.
|
|
1388
|
+
this.recordRetainedPropertyOpInStateVector(op, previousOp);
|
|
1389
|
+
this.reportPropertyOpAsApplied(op);
|
|
1344
1390
|
}
|
|
1345
1391
|
setTransientPropertyAndItsOpId(op) {
|
|
1346
|
-
this.transientPropertiesAndTheirOpIds.set(
|
|
1392
|
+
this.transientPropertiesAndTheirOpIds.set(this.getPropertyKey(op), op.id);
|
|
1347
1393
|
this.state.setTransientProperty(op.targetId, op.key, op.value);
|
|
1348
|
-
this.
|
|
1394
|
+
this.reportPropertyOpAsApplied(op);
|
|
1349
1395
|
}
|
|
1350
1396
|
applyProperty(op) {
|
|
1351
1397
|
const targetNode = this.state.getNode(op.targetId);
|
|
@@ -1357,30 +1403,31 @@ var _RepTree = class _RepTree {
|
|
|
1357
1403
|
this.pendingPropertiesWithMissingNode.set(op.targetId, []);
|
|
1358
1404
|
}
|
|
1359
1405
|
this.pendingPropertiesWithMissingNode.get(op.targetId).push(op);
|
|
1360
|
-
this.markOpSeen(op
|
|
1406
|
+
this.markOpSeen(op);
|
|
1361
1407
|
return;
|
|
1362
1408
|
}
|
|
1363
1409
|
this.updatePropClock(op);
|
|
1364
1410
|
this.applyLLWProperty(op, targetNode);
|
|
1365
1411
|
}
|
|
1366
1412
|
applyLLWProperty(op, targetNode) {
|
|
1367
|
-
const
|
|
1368
|
-
const
|
|
1413
|
+
const propertyKey = this.getPropertyKey(op);
|
|
1414
|
+
const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(propertyKey);
|
|
1415
|
+
const previousOp = this.propertyOpsByKey.get(propertyKey);
|
|
1369
1416
|
if (!op.transient) {
|
|
1370
|
-
if (!
|
|
1371
|
-
this.setLLWPropertyAndItsOpId(op);
|
|
1417
|
+
if (!previousOp || isOpIdGreaterThan(op.id, previousOp.id)) {
|
|
1418
|
+
this.setLLWPropertyAndItsOpId(op, previousOp);
|
|
1372
1419
|
} else {
|
|
1373
|
-
this.markOpSeen(op
|
|
1420
|
+
this.markOpSeen(op);
|
|
1374
1421
|
}
|
|
1375
1422
|
if (prevTransientOpId && isOpIdGreaterThan(op.id, prevTransientOpId)) {
|
|
1376
|
-
this.transientPropertiesAndTheirOpIds.delete(
|
|
1423
|
+
this.transientPropertiesAndTheirOpIds.delete(propertyKey);
|
|
1377
1424
|
targetNode.removeTransientProperty(op.key);
|
|
1378
1425
|
}
|
|
1379
1426
|
} else {
|
|
1380
1427
|
if (!prevTransientOpId || isOpIdGreaterThan(op.id, prevTransientOpId)) {
|
|
1381
1428
|
this.setTransientPropertyAndItsOpId(op);
|
|
1382
1429
|
} else {
|
|
1383
|
-
this.markOpSeen(op
|
|
1430
|
+
this.markOpSeen(op);
|
|
1384
1431
|
}
|
|
1385
1432
|
}
|
|
1386
1433
|
}
|
|
@@ -1391,18 +1438,21 @@ var _RepTree = class _RepTree {
|
|
|
1391
1438
|
this.applyProperty(op);
|
|
1392
1439
|
}
|
|
1393
1440
|
}
|
|
1394
|
-
markOpSeen(op
|
|
1441
|
+
markOpSeen(op) {
|
|
1395
1442
|
this.knownOps.add(this.getOpKey(op));
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
}
|
|
1443
|
+
}
|
|
1444
|
+
reportMoveOpAsApplied(op) {
|
|
1445
|
+
this.markOpSeen(op);
|
|
1446
|
+
if (this._stateVectorEnabled) {
|
|
1447
|
+
this.moveStateVector.updateFromOp(op);
|
|
1402
1448
|
}
|
|
1449
|
+
this.reportOpAsApplied(op);
|
|
1450
|
+
}
|
|
1451
|
+
reportPropertyOpAsApplied(op) {
|
|
1452
|
+
this.markOpSeen(op);
|
|
1453
|
+
this.reportOpAsApplied(op);
|
|
1403
1454
|
}
|
|
1404
|
-
reportOpAsApplied(op
|
|
1405
|
-
this.markOpSeen(op, includeInStateVector);
|
|
1455
|
+
reportOpAsApplied(op) {
|
|
1406
1456
|
for (const callback of this.opAppliedCallbacks) {
|
|
1407
1457
|
callback(op);
|
|
1408
1458
|
}
|
|
@@ -1412,8 +1462,7 @@ var _RepTree = class _RepTree {
|
|
|
1412
1462
|
if (targetNode) {
|
|
1413
1463
|
this.parentIdBeforeMove.set(op.id, targetNode.parentId);
|
|
1414
1464
|
}
|
|
1415
|
-
if (
|
|
1416
|
-
if (op.parentId && this.isAncestor(op.parentId, op.targetId)) return;
|
|
1465
|
+
if (this.wouldMoveCreateCycle(op)) return;
|
|
1417
1466
|
this.state.moveNode(op.targetId, op.parentId);
|
|
1418
1467
|
if (!targetNode) {
|
|
1419
1468
|
const pendingProperties = this.pendingPropertiesWithMissingNode.get(op.targetId) || [];
|
|
@@ -1438,7 +1487,7 @@ var _RepTree = class _RepTree {
|
|
|
1438
1487
|
// --- Range-Based State Vector Methods ---
|
|
1439
1488
|
/**
|
|
1440
1489
|
* Returns the current state vectors for move and property streams.
|
|
1441
|
-
* Returns
|
|
1490
|
+
* Returns copies of the internal state vectors.
|
|
1442
1491
|
*/
|
|
1443
1492
|
getStateVectors() {
|
|
1444
1493
|
if (!this._stateVectorEnabled) {
|
|
@@ -1506,16 +1555,22 @@ var _RepTree = class _RepTree {
|
|
|
1506
1555
|
getPropertyOps() {
|
|
1507
1556
|
return Array.from(this.propertyOpsByKey.values());
|
|
1508
1557
|
}
|
|
1509
|
-
|
|
1558
|
+
recordRetainedPropertyOpInStateVector(op, previousOp) {
|
|
1510
1559
|
if (!this._stateVectorEnabled) {
|
|
1511
1560
|
return;
|
|
1512
1561
|
}
|
|
1513
|
-
|
|
1562
|
+
if (previousOp) {
|
|
1563
|
+
this.propStateVector.removeFromOp(previousOp);
|
|
1564
|
+
}
|
|
1565
|
+
this.propStateVector.updateFromOp(op);
|
|
1514
1566
|
}
|
|
1515
1567
|
getOpKey(op) {
|
|
1516
1568
|
const stream = isMoveNodeOp(op) ? "move" : "prop";
|
|
1517
1569
|
return `${stream}:${opIdToString(op.id)}`;
|
|
1518
1570
|
}
|
|
1571
|
+
getPropertyKey(op) {
|
|
1572
|
+
return `${op.key}@${op.targetId}`;
|
|
1573
|
+
}
|
|
1519
1574
|
filterOpsByRanges(ops, ranges) {
|
|
1520
1575
|
const missingOps = [];
|
|
1521
1576
|
for (const op of ops) {
|