reptree 1.0.1 → 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,8 +322,6 @@ 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
327
  /** Checks whether moving `targetId` under `parentId` would create a cycle. */
@@ -354,12 +352,14 @@ declare class RepTree {
354
352
  private applyLLWProperty;
355
353
  private applyOperation;
356
354
  private markOpSeen;
355
+ private reportMoveOpAsApplied;
356
+ private reportPropertyOpAsApplied;
357
357
  private reportOpAsApplied;
358
358
  private tryToMove;
359
359
  private undoMove;
360
360
  /**
361
361
  * Returns the current state vectors for move and property streams.
362
- * Returns readonly references to the internal state vectors.
362
+ * Returns copies of the internal state vectors.
363
363
  */
364
364
  getStateVectors(): {
365
365
  move: Readonly<StateVectors["move"]>;
@@ -389,8 +389,9 @@ declare class RepTree {
389
389
  parse: (data: unknown) => T;
390
390
  }): T;
391
391
  private getPropertyOps;
392
- private refreshPropStateVector;
392
+ private recordRetainedPropertyOpInStateVector;
393
393
  private getOpKey;
394
+ private getPropertyKey;
394
395
  private filterOpsByRanges;
395
396
  }
396
397
 
@@ -445,9 +446,25 @@ declare class StateVector {
445
446
  * @param op The operation that was just applied
446
447
  */
447
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;
448
465
  /**
449
466
  * Returns the current state vector.
450
- * Returns a readonly reference to the internal state.
467
+ * Returns a deep copy so callers cannot mutate internal state.
451
468
  */
452
469
  getState(): Readonly<Record<string, number[][]>>;
453
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;
@@ -1320,14 +1353,14 @@ var _RepTree = class _RepTree {
1320
1353
  this.pendingMovesWithMissingParent.set(op.parentId, []);
1321
1354
  }
1322
1355
  this.pendingMovesWithMissingParent.get(op.parentId).push(op);
1323
- this.markOpSeen(op, true);
1356
+ this.markOpSeen(op);
1324
1357
  return;
1325
1358
  }
1326
1359
  this.updateMoveClock(op);
1327
1360
  const lastOp = this.moveOps.length > 0 ? this.moveOps[this.moveOps.length - 1] : null;
1328
1361
  if (lastOp === null || isOpIdGreaterThan(op.id, lastOp.id)) {
1329
1362
  this.moveOps.push(op);
1330
- this.reportOpAsApplied(op);
1363
+ this.reportMoveOpAsApplied(op);
1331
1364
  this.tryToMove(op);
1332
1365
  } else {
1333
1366
  let targetIndex = this.moveOps.length;
@@ -1341,7 +1374,7 @@ var _RepTree = class _RepTree {
1341
1374
  }
1342
1375
  }
1343
1376
  this.moveOps.splice(targetIndex + 1, 0, op);
1344
- this.reportOpAsApplied(op);
1377
+ this.reportMoveOpAsApplied(op);
1345
1378
  this.tryToMove(op);
1346
1379
  for (let i = targetIndex + 2; i < this.moveOps.length; i++) {
1347
1380
  this.tryToMove(this.moveOps[i]);
@@ -1349,16 +1382,16 @@ var _RepTree = class _RepTree {
1349
1382
  }
1350
1383
  this.applyPendingMovesForParent(op.targetId);
1351
1384
  }
1352
- setLLWPropertyAndItsOpId(op) {
1353
- this.propertyOpsByKey.set(`${op.key}@${op.targetId}`, op);
1385
+ setLLWPropertyAndItsOpId(op, previousOp) {
1386
+ this.propertyOpsByKey.set(this.getPropertyKey(op), op);
1354
1387
  this.state.setProperty(op.targetId, op.key, op.value);
1355
- this.reportOpAsApplied(op, false);
1356
- this.refreshPropStateVector();
1388
+ this.recordRetainedPropertyOpInStateVector(op, previousOp);
1389
+ this.reportPropertyOpAsApplied(op);
1357
1390
  }
1358
1391
  setTransientPropertyAndItsOpId(op) {
1359
- this.transientPropertiesAndTheirOpIds.set(`${op.key}@${op.targetId}`, op.id);
1392
+ this.transientPropertiesAndTheirOpIds.set(this.getPropertyKey(op), op.id);
1360
1393
  this.state.setTransientProperty(op.targetId, op.key, op.value);
1361
- this.reportOpAsApplied(op, false);
1394
+ this.reportPropertyOpAsApplied(op);
1362
1395
  }
1363
1396
  applyProperty(op) {
1364
1397
  const targetNode = this.state.getNode(op.targetId);
@@ -1370,30 +1403,31 @@ var _RepTree = class _RepTree {
1370
1403
  this.pendingPropertiesWithMissingNode.set(op.targetId, []);
1371
1404
  }
1372
1405
  this.pendingPropertiesWithMissingNode.get(op.targetId).push(op);
1373
- this.markOpSeen(op, false);
1406
+ this.markOpSeen(op);
1374
1407
  return;
1375
1408
  }
1376
1409
  this.updatePropClock(op);
1377
1410
  this.applyLLWProperty(op, targetNode);
1378
1411
  }
1379
1412
  applyLLWProperty(op, targetNode) {
1380
- const prevTransientOpId = this.transientPropertiesAndTheirOpIds.get(`${op.key}@${op.targetId}`);
1381
- 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);
1382
1416
  if (!op.transient) {
1383
- if (!prevOpId || isOpIdGreaterThan(op.id, prevOpId)) {
1384
- this.setLLWPropertyAndItsOpId(op);
1417
+ if (!previousOp || isOpIdGreaterThan(op.id, previousOp.id)) {
1418
+ this.setLLWPropertyAndItsOpId(op, previousOp);
1385
1419
  } else {
1386
- this.markOpSeen(op, false);
1420
+ this.markOpSeen(op);
1387
1421
  }
1388
1422
  if (prevTransientOpId && isOpIdGreaterThan(op.id, prevTransientOpId)) {
1389
- this.transientPropertiesAndTheirOpIds.delete(`${op.key}@${op.targetId}`);
1423
+ this.transientPropertiesAndTheirOpIds.delete(propertyKey);
1390
1424
  targetNode.removeTransientProperty(op.key);
1391
1425
  }
1392
1426
  } else {
1393
1427
  if (!prevTransientOpId || isOpIdGreaterThan(op.id, prevTransientOpId)) {
1394
1428
  this.setTransientPropertyAndItsOpId(op);
1395
1429
  } else {
1396
- this.markOpSeen(op, false);
1430
+ this.markOpSeen(op);
1397
1431
  }
1398
1432
  }
1399
1433
  }
@@ -1404,18 +1438,21 @@ var _RepTree = class _RepTree {
1404
1438
  this.applyProperty(op);
1405
1439
  }
1406
1440
  }
1407
- markOpSeen(op, includeInStateVector) {
1441
+ markOpSeen(op) {
1408
1442
  this.knownOps.add(this.getOpKey(op));
1409
- if (includeInStateVector && this._stateVectorEnabled) {
1410
- if (isMoveNodeOp(op)) {
1411
- this.moveStateVector.updateFromOp(op);
1412
- } else if (isAnyPropertyOp(op)) {
1413
- this.propStateVector.updateFromOp(op);
1414
- }
1443
+ }
1444
+ reportMoveOpAsApplied(op) {
1445
+ this.markOpSeen(op);
1446
+ if (this._stateVectorEnabled) {
1447
+ this.moveStateVector.updateFromOp(op);
1415
1448
  }
1449
+ this.reportOpAsApplied(op);
1450
+ }
1451
+ reportPropertyOpAsApplied(op) {
1452
+ this.markOpSeen(op);
1453
+ this.reportOpAsApplied(op);
1416
1454
  }
1417
- reportOpAsApplied(op, includeInStateVector = true) {
1418
- this.markOpSeen(op, includeInStateVector);
1455
+ reportOpAsApplied(op) {
1419
1456
  for (const callback of this.opAppliedCallbacks) {
1420
1457
  callback(op);
1421
1458
  }
@@ -1450,7 +1487,7 @@ var _RepTree = class _RepTree {
1450
1487
  // --- Range-Based State Vector Methods ---
1451
1488
  /**
1452
1489
  * Returns the current state vectors for move and property streams.
1453
- * Returns readonly references to the internal state vectors.
1490
+ * Returns copies of the internal state vectors.
1454
1491
  */
1455
1492
  getStateVectors() {
1456
1493
  if (!this._stateVectorEnabled) {
@@ -1518,16 +1555,22 @@ var _RepTree = class _RepTree {
1518
1555
  getPropertyOps() {
1519
1556
  return Array.from(this.propertyOpsByKey.values());
1520
1557
  }
1521
- refreshPropStateVector() {
1558
+ recordRetainedPropertyOpInStateVector(op, previousOp) {
1522
1559
  if (!this._stateVectorEnabled) {
1523
1560
  return;
1524
1561
  }
1525
- this.propStateVector = StateVector.fromOperations(this.getPropertyOps());
1562
+ if (previousOp) {
1563
+ this.propStateVector.removeFromOp(previousOp);
1564
+ }
1565
+ this.propStateVector.updateFromOp(op);
1526
1566
  }
1527
1567
  getOpKey(op) {
1528
1568
  const stream = isMoveNodeOp(op) ? "move" : "prop";
1529
1569
  return `${stream}:${opIdToString(op.id)}`;
1530
1570
  }
1571
+ getPropertyKey(op) {
1572
+ return `${op.key}@${op.targetId}`;
1573
+ }
1531
1574
  filterOpsByRanges(ops, ranges) {
1532
1575
  const missingOps = [];
1533
1576
  for (const op of ops) {