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/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 if the given `ancestorId` is an ancestor of `childId` in the tree */
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 readonly references to the internal state vectors.
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 refreshPropStateVector;
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 readonly reference to the internal state.
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 readonly reference to the internal state.
802
+ * Returns a deep copy so callers cannot mutate internal state.
756
803
  */
757
804
  getState() {
758
- return this.ranges;
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 = theirState[peerId] || [];
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 if the given `ancestorId` is an ancestor of `childId` in the tree */
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
- let targetId = childId;
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(`isAncestor: cycle detected in the tree structure.`);
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, true);
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.reportOpAsApplied(op);
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.reportOpAsApplied(op);
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(`${op.key}@${op.targetId}`, op);
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.reportOpAsApplied(op, false);
1343
- this.refreshPropStateVector();
1388
+ this.recordRetainedPropertyOpInStateVector(op, previousOp);
1389
+ this.reportPropertyOpAsApplied(op);
1344
1390
  }
1345
1391
  setTransientPropertyAndItsOpId(op) {
1346
- this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
1392
+ this.transientPropertiesAndTheirOpIds.set(this.getPropertyKey(op), op.id);
1347
1393
  this.state.setTransientProperty(op.targetId, op.key, op.value);
1348
- this.reportOpAsApplied(op, false);
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, false);
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 prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
1368
- const prevOpId = this.propertyOpsByKey.get(`${op.key}@${op.targetId}`)?.id;
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 (!prevOpId || isOpIdGreaterThan(op.id, prevOpId)) {
1371
- this.setLLWPropertyAndItsOpId(op);
1417
+ if (!previousOp || isOpIdGreaterThan(op.id, previousOp.id)) {
1418
+ this.setLLWPropertyAndItsOpId(op, previousOp);
1372
1419
  } else {
1373
- this.markOpSeen(op, false);
1420
+ this.markOpSeen(op);
1374
1421
  }
1375
1422
  if (prevTransientOpId && isOpIdGreaterThan(op.id, prevTransientOpId)) {
1376
- this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
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, false);
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, includeInStateVector) {
1441
+ markOpSeen(op) {
1395
1442
  this.knownOps.add(this.getOpKey(op));
1396
- if (includeInStateVector && this._stateVectorEnabled) {
1397
- if (isMoveNodeOp(op)) {
1398
- this.moveStateVector.updateFromOp(op);
1399
- } else if (isAnyPropertyOp(op)) {
1400
- this.propStateVector.updateFromOp(op);
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, includeInStateVector = true) {
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 (op.targetId === op.parentId) return;
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 readonly references to the internal state vectors.
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
- refreshPropStateVector() {
1558
+ recordRetainedPropertyOpInStateVector(op, previousOp) {
1510
1559
  if (!this._stateVectorEnabled) {
1511
1560
  return;
1512
1561
  }
1513
- this.propStateVector = StateVector.fromOperations(this.getPropertyOps());
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) {