@fluidframework/merge-tree 0.59.4001 → 1.1.0-75972

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.
Files changed (80) hide show
  1. package/.eslintrc.js +1 -1
  2. package/README.md +1 -1
  3. package/REFERENCEPOSITIONS.md +199 -0
  4. package/dist/client.d.ts +30 -4
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/client.js +89 -47
  7. package/dist/client.js.map +1 -1
  8. package/dist/collections.d.ts +5 -4
  9. package/dist/collections.d.ts.map +1 -1
  10. package/dist/collections.js +17 -18
  11. package/dist/collections.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/localReference.d.ts +11 -3
  17. package/dist/localReference.d.ts.map +1 -1
  18. package/dist/localReference.js +25 -8
  19. package/dist/localReference.js.map +1 -1
  20. package/dist/mergeTree.d.ts +23 -3
  21. package/dist/mergeTree.d.ts.map +1 -1
  22. package/dist/mergeTree.js +136 -48
  23. package/dist/mergeTree.js.map +1 -1
  24. package/dist/mergeTreeDeltaCallback.d.ts +8 -10
  25. package/dist/mergeTreeDeltaCallback.d.ts.map +1 -1
  26. package/dist/mergeTreeDeltaCallback.js +6 -10
  27. package/dist/mergeTreeDeltaCallback.js.map +1 -1
  28. package/dist/opBuilder.js +6 -5
  29. package/dist/opBuilder.js.map +1 -1
  30. package/dist/ops.d.ts +12 -10
  31. package/dist/ops.d.ts.map +1 -1
  32. package/dist/ops.js +7 -7
  33. package/dist/ops.js.map +1 -1
  34. package/dist/referencePositions.d.ts +1 -1
  35. package/dist/referencePositions.d.ts.map +1 -1
  36. package/dist/referencePositions.js +3 -2
  37. package/dist/referencePositions.js.map +1 -1
  38. package/lib/client.d.ts +30 -4
  39. package/lib/client.d.ts.map +1 -1
  40. package/lib/client.js +89 -47
  41. package/lib/client.js.map +1 -1
  42. package/lib/collections.d.ts +5 -4
  43. package/lib/collections.d.ts.map +1 -1
  44. package/lib/collections.js +17 -18
  45. package/lib/collections.js.map +1 -1
  46. package/lib/index.d.ts +1 -1
  47. package/lib/index.d.ts.map +1 -1
  48. package/lib/index.js +1 -1
  49. package/lib/index.js.map +1 -1
  50. package/lib/localReference.d.ts +11 -3
  51. package/lib/localReference.d.ts.map +1 -1
  52. package/lib/localReference.js +23 -7
  53. package/lib/localReference.js.map +1 -1
  54. package/lib/mergeTree.d.ts +23 -3
  55. package/lib/mergeTree.d.ts.map +1 -1
  56. package/lib/mergeTree.js +137 -49
  57. package/lib/mergeTree.js.map +1 -1
  58. package/lib/mergeTreeDeltaCallback.d.ts +8 -10
  59. package/lib/mergeTreeDeltaCallback.d.ts.map +1 -1
  60. package/lib/mergeTreeDeltaCallback.js +6 -10
  61. package/lib/mergeTreeDeltaCallback.js.map +1 -1
  62. package/lib/opBuilder.js +6 -5
  63. package/lib/opBuilder.js.map +1 -1
  64. package/lib/ops.d.ts +12 -10
  65. package/lib/ops.d.ts.map +1 -1
  66. package/lib/ops.js +7 -7
  67. package/lib/ops.js.map +1 -1
  68. package/lib/referencePositions.d.ts +1 -1
  69. package/lib/referencePositions.d.ts.map +1 -1
  70. package/lib/referencePositions.js +3 -2
  71. package/lib/referencePositions.js.map +1 -1
  72. package/package.json +17 -94
  73. package/src/client.ts +87 -27
  74. package/src/collections.ts +5 -4
  75. package/src/index.ts +1 -1
  76. package/src/localReference.ts +33 -8
  77. package/src/mergeTree.ts +136 -43
  78. package/src/mergeTreeDeltaCallback.ts +8 -10
  79. package/src/ops.ts +13 -10
  80. package/src/referencePositions.ts +3 -2
package/src/client.ts CHANGED
@@ -16,7 +16,7 @@ import { LoggingError } from "@fluidframework/telemetry-utils";
16
16
  import { IIntegerRange } from "./base";
17
17
  import { RedBlackTree } from "./collections";
18
18
  import { UnassignedSequenceNumber, UniversalSequenceNumber } from "./constants";
19
- import { LocalReference } from "./localReference";
19
+ import { LocalReference, LocalReferencePosition } from "./localReference";
20
20
  import {
21
21
  CollaborationWindow,
22
22
  compareStrings,
@@ -53,7 +53,7 @@ import { SnapshotLegacy } from "./snapshotlegacy";
53
53
  import { SnapshotLoader } from "./snapshotLoader";
54
54
  import { MergeTreeTextHelper } from "./textSegment";
55
55
  import { SnapshotV1 } from "./snapshotV1";
56
- import { ReferencePosition, RangeStackMap, DetachedReferencePosition } from "./referencePositions";
56
+ import { ReferencePosition, RangeStackMap } from "./referencePositions";
57
57
  import {
58
58
  IMergeTreeClientSequenceArgs,
59
59
  IMergeTreeDeltaOpArgs,
@@ -333,21 +333,17 @@ export class Client {
333
333
  }
334
334
 
335
335
  public createLocalReferencePosition(
336
- segment: ISegment, offset: number, refType: ReferenceType, properties: PropertySet | undefined,
337
- ): ReferencePosition {
336
+ segment: ISegment, offset: number | undefined, refType: ReferenceType, properties: PropertySet | undefined,
337
+ ): LocalReferencePosition {
338
338
  return this.mergeTree.createLocalReferencePosition(segment, offset, refType, properties, this);
339
339
  }
340
340
 
341
- public removeLocalReferencePosition(lref: ReferencePosition) {
341
+ public removeLocalReferencePosition(lref: LocalReferencePosition) {
342
342
  return this.mergeTree.removeLocalReferencePosition(lref);
343
343
  }
344
344
 
345
345
  public localReferencePositionToPosition(lref: ReferencePosition) {
346
- const segment = lref.getSegment();
347
- if (segment === undefined) {
348
- return DetachedReferencePosition;
349
- }
350
- return this.getPosition(segment) + lref.getOffset();
346
+ return this.mergeTree.referencePositionToLocalPosition(lref);
351
347
  }
352
348
 
353
349
  /**
@@ -575,13 +571,14 @@ export class Client {
575
571
 
576
572
  /**
577
573
  * Gets the client args from the op if remote, otherwise uses the local clients info
578
- * @param opArgs - The op arg to get the client sequence args for
574
+ * @param sequencedMessage - The sequencedMessage to get the client sequence args for
579
575
  */
580
- private getClientSequenceArgs(opArgs: IMergeTreeDeltaOpArgs): IMergeTreeClientSequenceArgs {
576
+ private getClientSequenceArgsForMessage(sequencedMessage: ISequencedDocumentMessage | undefined):
577
+ IMergeTreeClientSequenceArgs {
581
578
  // If there this no sequenced message, then the op is local
582
579
  // and unacked, so use this clients sequenced args
583
580
  //
584
- if (!opArgs.sequencedMessage) {
581
+ if (!sequencedMessage) {
585
582
  const segWindow = this.getCollabWindow();
586
583
  return {
587
584
  clientId: segWindow.clientId,
@@ -590,13 +587,21 @@ export class Client {
590
587
  };
591
588
  } else {
592
589
  return {
593
- clientId: this.getShortClientId(opArgs.sequencedMessage.clientId),
594
- referenceSequenceNumber: opArgs.sequencedMessage.referenceSequenceNumber,
595
- sequenceNumber: opArgs.sequencedMessage.sequenceNumber,
590
+ clientId: this.getOrAddShortClientId(sequencedMessage.clientId),
591
+ referenceSequenceNumber: sequencedMessage.referenceSequenceNumber,
592
+ sequenceNumber: sequencedMessage.sequenceNumber,
596
593
  };
597
594
  }
598
595
  }
599
596
 
597
+ /**
598
+ * Gets the client args from the op if remote, otherwise uses the local clients info
599
+ * @param opArgs - The op arg to get the client sequence args for
600
+ */
601
+ private getClientSequenceArgs(opArgs: IMergeTreeDeltaOpArgs): IMergeTreeClientSequenceArgs {
602
+ return this.getClientSequenceArgsForMessage(opArgs.sequencedMessage);
603
+ }
604
+
600
605
  private ackPendingSegment(opArgs: IMergeTreeDeltaOpArgs) {
601
606
  const ackOp = (deltaOpArgs: IMergeTreeDeltaOpArgs) => {
602
607
  let trace: Trace | undefined;
@@ -704,6 +709,61 @@ export class Client {
704
709
  return segmentPosition;
705
710
  }
706
711
 
712
+ /**
713
+ * Rebases a (local) position from the perspective `{ seq: seqNumberFrom, localSeq }` to the perspective
714
+ * of the current sequence number. This is desirable when rebasing operations for reconnection.
715
+ *
716
+ * If the position refers to a segment/offset that was removed by some operation between `seqNumberFrom` and
717
+ * the current sequence number, the returned position will align with the position of a reference given
718
+ * `SlideOnRemove` semantics.
719
+ */
720
+ public rebasePosition(
721
+ pos: number,
722
+ seqNumberFrom: number,
723
+ localSeq: number,
724
+ ): number {
725
+ assert(localSeq <= this.mergeTree.collabWindow.localSeq, 0x300 /* localSeq greater than collab window */);
726
+ let segment: ISegment | undefined;
727
+ let posAccumulated = 0;
728
+ let offset = pos;
729
+ const isInsertedInView = (seg: ISegment) =>
730
+ (seg.seq !== undefined && seg.seq !== UnassignedSequenceNumber && seg.seq <= seqNumberFrom)
731
+ || (seg.localSeq !== undefined && seg.localSeq <= localSeq);
732
+
733
+ const isRemovedFromView = ({ removedSeq, localRemovedSeq }: ISegment) =>
734
+ (removedSeq !== undefined && removedSeq !== UnassignedSequenceNumber && removedSeq <= seqNumberFrom)
735
+ || (localRemovedSeq !== undefined && localRemovedSeq <= localSeq);
736
+
737
+ this.mergeTree.walkAllSegments(this.mergeTree.root, (seg) => {
738
+ assert(seg.seq !== undefined || seg.localSeq !== undefined,
739
+ 0x301 /* Either seq or localSeq should be defined */);
740
+ segment = seg;
741
+
742
+ if (isInsertedInView(seg) && !isRemovedFromView(seg)) {
743
+ posAccumulated += seg.cachedLength;
744
+ if (offset >= seg.cachedLength) {
745
+ offset -= seg.cachedLength;
746
+ }
747
+ }
748
+
749
+ // Keep going while we've yet to reach the segment at the desired position
750
+ return posAccumulated <= pos;
751
+ });
752
+
753
+ assert(segment !== undefined, 0x302 /* No segment found */);
754
+ const seqNumberTo = this.getCollabWindow().currentSeq;
755
+ if ((segment.removedSeq !== undefined &&
756
+ segment.removedSeq !== UnassignedSequenceNumber &&
757
+ segment.removedSeq <= seqNumberTo)
758
+ || (segment.localRemovedSeq !== undefined && segment.localRemovedSeq <= localSeq)) {
759
+ // Segment that the position was in has been removed: null out offset.
760
+ offset = 0;
761
+ }
762
+
763
+ assert(0 <= offset && offset < segment.cachedLength, 0x303 /* Invalid offset */);
764
+ return this.findReconnectionPosition(segment, localSeq) + offset;
765
+ }
766
+
707
767
  private resetPendingDeltaToOps(
708
768
  resetOp: IMergeTreeDeltaOp,
709
769
  segmentGroup: SegmentGroup): IMergeTreeDeltaOp[] {
@@ -1021,17 +1081,17 @@ export class Client {
1021
1081
  }
1022
1082
 
1023
1083
  getContainingSegment<T extends ISegment>(pos: number, op?: ISequencedDocumentMessage) {
1024
- let seq: number;
1025
- let clientId: number;
1026
- if (op) {
1027
- clientId = this.getOrAddShortClientId(op.clientId);
1028
- seq = op.referenceSequenceNumber;
1029
- } else {
1030
- const segWindow = this.mergeTree.getCollabWindow();
1031
- seq = segWindow.currentSeq;
1032
- clientId = segWindow.clientId;
1033
- }
1034
- return this.mergeTree.getContainingSegment<T>(pos, seq, clientId);
1084
+ const args = this.getClientSequenceArgsForMessage(op);
1085
+ return this.mergeTree.getContainingSegment<T>(pos, args.referenceSequenceNumber, args.clientId);
1086
+ }
1087
+
1088
+ /**
1089
+ * Returns the position to slide a reference to if a slide is required.
1090
+ * @param segoff - The segment and offset to slide from
1091
+ * @returns - segment and offset to slide the reference to
1092
+ */
1093
+ getSlideToSegment(segoff: { segment: ISegment | undefined; offset: number | undefined; }) {
1094
+ return this.mergeTree._getSlideToSegment(segoff);
1035
1095
  }
1036
1096
 
1037
1097
  getPropertiesAtPosition(pos: number) {
@@ -245,10 +245,11 @@ export class Heap<T> {
245
245
  /* eslint-enable no-bitwise */
246
246
  }
247
247
 
248
- export const enum RBColor {
249
- RED,
250
- BLACK,
251
- }
248
+ export const RBColor = {
249
+ RED: 0,
250
+ BLACK: 1,
251
+ } as const;
252
+ export type RBColor = typeof RBColor[keyof typeof RBColor];
252
253
 
253
254
  export interface RBNode<TKey, TData> {
254
255
  key: TKey;
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ export * from "./base";
7
7
  export * from "./client";
8
8
  export * from "./collections";
9
9
  export * from "./constants";
10
- export * from "./localReference";
10
+ export { LocalReference, LocalReferencePosition, LocalReferenceCollection } from "./localReference";
11
11
  export * from "./mergeTree";
12
12
  export * from "./mergeTreeDeltaCallback";
13
13
  export * from "./mergeTreeTracking";
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { assert } from "@fluidframework/common-utils";
7
+ import { UsageError } from "@fluidframework/container-utils";
7
8
  import { Client } from "./client";
8
9
  import { List, ListMakeHead, ListRemoveEntry } from "./collections";
9
10
  import {
@@ -26,9 +27,33 @@ import {
26
27
  } from "./referencePositions";
27
28
 
28
29
  /**
29
- * @deprecated - Use ReferencePosition
30
+ * @internal
30
31
  */
31
- export class LocalReference implements ReferencePosition {
32
+ export function _validateReferenceType(refType: ReferenceType) {
33
+ let exclusiveCount = 0;
34
+ if (refTypeIncludesFlag(refType, ReferenceType.Transient)) {
35
+ ++exclusiveCount;
36
+ }
37
+ if (refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove)) {
38
+ ++exclusiveCount;
39
+ }
40
+ if (refTypeIncludesFlag(refType, ReferenceType.StayOnRemove)) {
41
+ ++exclusiveCount;
42
+ }
43
+ if (exclusiveCount > 1) {
44
+ throw new UsageError(
45
+ "Reference types can only be one of Transient, SlideOnRemove, and StayOnRemove");
46
+ }
47
+ }
48
+
49
+ export interface LocalReferencePosition extends ReferencePosition {
50
+ callbacks?: Partial<Record<"beforeSlide" | "afterSlide", () => void>>;
51
+ }
52
+
53
+ /**
54
+ * @deprecated - Use LocalReferencePosition
55
+ */
56
+ export class LocalReference implements LocalReferencePosition {
32
57
  /**
33
58
  * @deprecated - use DetachedReferencePosition
34
59
  */
@@ -44,6 +69,8 @@ export class LocalReference implements ReferencePosition {
44
69
  */
45
70
  public segment: ISegment | undefined;
46
71
 
72
+ public callbacks?: Partial<Record<"beforeSlide" | "afterSlide", () => void>> | undefined;
73
+
47
74
  /**
48
75
  * @deprecated - use createReferencePosition
49
76
  */
@@ -57,6 +84,7 @@ export class LocalReference implements ReferencePosition {
57
84
  public refType = ReferenceType.Simple,
58
85
  properties?: PropertySet,
59
86
  ) {
87
+ _validateReferenceType(refType);
60
88
  this.segment = initSegment;
61
89
  this.properties = properties;
62
90
  }
@@ -144,9 +172,6 @@ export class LocalReference implements ReferencePosition {
144
172
  }
145
173
 
146
174
  public getOffset() {
147
- if (this.segment?.removedSeq) {
148
- return 0;
149
- }
150
175
  return this.offset;
151
176
  }
152
177
 
@@ -286,7 +311,7 @@ export class LocalReferenceCollection {
286
311
  * @internal - this method should only be called by mergeTree
287
312
  */
288
313
  public createLocalRef(
289
- offset: number,
314
+ offset: number | undefined,
290
315
  refType: ReferenceType,
291
316
  properties: PropertySet | undefined,
292
317
  client: Client): ReferencePosition {
@@ -312,8 +337,8 @@ export class LocalReferenceCollection {
312
337
  !refTypeIncludesFlag(lref, ReferenceType.Transient),
313
338
  0x2df /* "transient references cannot be bound to segments" */);
314
339
  assertLocalReferences(lref);
315
- const refsAtOffset = this.refsByOffset[lref.getOffset()] =
316
- this.refsByOffset[lref.getOffset()]
340
+ const refsAtOffset = this.refsByOffset[lref.offset] =
341
+ this.refsByOffset[lref.offset]
317
342
  ?? { at: ListMakeHead() };
318
343
  const atRefs = refsAtOffset.at =
319
344
  refsAtOffset.at
package/src/mergeTree.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  /* eslint-disable @typescript-eslint/prefer-optional-chain, no-bitwise */
10
10
 
11
11
  import { assert } from "@fluidframework/common-utils";
12
+ import { UsageError } from "@fluidframework/container-utils";
12
13
  import {
13
14
  Comparer,
14
15
  Heap,
@@ -23,7 +24,7 @@ import {
23
24
  UnassignedSequenceNumber,
24
25
  UniversalSequenceNumber,
25
26
  } from "./constants";
26
- import { LocalReference, LocalReferenceCollection } from "./localReference";
27
+ import { LocalReference, LocalReferenceCollection, LocalReferencePosition } from "./localReference";
27
28
  import {
28
29
  IMergeTreeDeltaOpArgs,
29
30
  IMergeTreeSegmentDelta,
@@ -109,6 +110,15 @@ export function toRemovalInfo(maybe: Partial<IRemovalInfo> | undefined): IRemova
109
110
  0x2bf /* "both removedClientIds and removedSeq should be set or not set" */);
110
111
  }
111
112
 
113
+ function isRemoved(segment: ISegment): boolean {
114
+ return toRemovalInfo(segment) !== undefined;
115
+ }
116
+
117
+ function isRemovedAndAcked(segment: ISegment): boolean {
118
+ const removalInfo = toRemovalInfo(segment);
119
+ return removalInfo !== undefined && removalInfo.removedSeq !== UnassignedSequenceNumber;
120
+ }
121
+
112
122
  /**
113
123
  * A segment representing a portion of the merge tree.
114
124
  */
@@ -554,7 +564,6 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
554
564
  return true;
555
565
 
556
566
  case MergeTreeDeltaType.REMOVE:
557
-
558
567
  const removalInfo: IRemovalInfo | undefined = toRemovalInfo(this);
559
568
  assert(removalInfo !== undefined, 0x046 /* "On remove ack, missing removal info!" */);
560
569
  this.localRemovedSeq = undefined;
@@ -562,7 +571,6 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
562
571
  removalInfo.removedSeq = opArgs.sequencedMessage!.sequenceNumber;
563
572
  return true;
564
573
  }
565
-
566
574
  return false;
567
575
 
568
576
  default:
@@ -1414,6 +1422,110 @@ export class MergeTree {
1414
1422
  return { segment, offset };
1415
1423
  }
1416
1424
 
1425
+ /**
1426
+ * @internal must only be used by client
1427
+ * @param segoff - The segment and offset to slide from
1428
+ * @returns The segment and offset to slide to
1429
+ */
1430
+ public _getSlideToSegment(segoff: { segment: ISegment | undefined; offset: number | undefined; }) {
1431
+ if (!segoff.segment || !isRemovedAndAcked(segoff.segment)) {
1432
+ return segoff;
1433
+ }
1434
+ let slideToSegment: ISegment | undefined;
1435
+ const goFurtherToFindSlideToSegment = (seg) => {
1436
+ if (seg.seq !== UnassignedSequenceNumber && !isRemovedAndAcked(seg)) {
1437
+ slideToSegment = seg;
1438
+ return false;
1439
+ }
1440
+ return true;
1441
+ };
1442
+ // Slide to the next farthest valid segment in the tree.
1443
+ this.rightExcursion(segoff.segment, goFurtherToFindSlideToSegment);
1444
+ if (slideToSegment) {
1445
+ return { segment: slideToSegment, offset: 0 };
1446
+ }
1447
+ // If no such segment is found, slide to the last valid segment.
1448
+ this.leftExcursion(segoff.segment, goFurtherToFindSlideToSegment);
1449
+
1450
+ // Workaround TypeScript issue (https://github.com/microsoft/TypeScript/issues/9998)
1451
+ slideToSegment = slideToSegment as ISegment | undefined;
1452
+
1453
+ if (slideToSegment) {
1454
+ // If slid nearer then offset should be at the end of the segment
1455
+ return { segment: slideToSegment, offset: slideToSegment.cachedLength - 1 };
1456
+ }
1457
+
1458
+ return { segment: undefined, offset: 0 };
1459
+ }
1460
+
1461
+ /**
1462
+ * This method should only be called when the current client sequence number is
1463
+ * max(remove segment sequence number, add reference sequence number).
1464
+ * Otherwise eventual consistency is not guaranteed.
1465
+ * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md`
1466
+ */
1467
+ private slideReferences(segment: ISegment, refsToSlide: LocalReference[]) {
1468
+ assert(
1469
+ isRemovedAndAcked(segment),
1470
+ 0x2f1 /* slideReferences from a segment which has not been removed and acked */);
1471
+ assert(!!segment.localRefs, 0x2f2 /* Ref not in the segment localRefs */);
1472
+ const newSegoff = this._getSlideToSegment({ segment, offset: 0 });
1473
+ const newSegment = newSegoff.segment;
1474
+ if (newSegment && !newSegment.localRefs) {
1475
+ newSegment.localRefs = new LocalReferenceCollection(newSegment);
1476
+ }
1477
+ for (const ref of refsToSlide) {
1478
+ ref.callbacks?.beforeSlide?.();
1479
+ const removedRef = segment.localRefs.removeLocalRef(ref);
1480
+ assert(ref === removedRef, 0x2f3 /* Ref not in the segment localRefs */);
1481
+ if (!newSegment) {
1482
+ // No valid segments (all nodes removed or not yet created)
1483
+ ref.segment = undefined;
1484
+ ref.offset = 0;
1485
+ } else {
1486
+ ref.segment = newSegment;
1487
+ ref.offset = newSegoff.offset ?? 0;
1488
+ assert(!!newSegment.localRefs, 0x2f4 /* localRefs must be allocated */);
1489
+ newSegment.localRefs.addLocalRef(ref);
1490
+ }
1491
+ ref.callbacks?.afterSlide?.();
1492
+ }
1493
+ // TODO is it required to update the path lengths?
1494
+ if (newSegment) {
1495
+ this.blockUpdatePathLengths(newSegment.parent, TreeMaintenanceSequenceNumber,
1496
+ LocalClientId);
1497
+ }
1498
+ }
1499
+
1500
+ private updateSegmentRefsAfterMarkRemoved(segment: ISegment, pending: boolean) {
1501
+ if (!segment.localRefs || segment.localRefs.empty) {
1502
+ return;
1503
+ }
1504
+ const refsToSlide: LocalReference[] = [];
1505
+ const refsToStay: LocalReference[] = [];
1506
+ for (const lref of segment.localRefs) {
1507
+ if (refTypeIncludesFlag(lref, ReferenceType.StayOnRemove)) {
1508
+ refsToStay.push(lref);
1509
+ } else if (refTypeIncludesFlag(lref, ReferenceType.SlideOnRemove)) {
1510
+ if (pending) {
1511
+ refsToStay.push(lref);
1512
+ } else {
1513
+ refsToSlide.push(lref);
1514
+ }
1515
+ }
1516
+ }
1517
+ // Rethink implementation of keeping and sliding refs once other reference
1518
+ // changes are complete. This works but is fragile and possibly slow.
1519
+ if (!pending) {
1520
+ this.slideReferences(segment, refsToSlide);
1521
+ }
1522
+ segment.localRefs.clear();
1523
+ for (const lref of refsToStay) {
1524
+ lref.segment = segment;
1525
+ segment.localRefs.addLocalRef(lref);
1526
+ }
1527
+ }
1528
+
1417
1529
  private blockLength(node: IMergeBlock, refSeq: number, clientId: number) {
1418
1530
  if ((this.collabWindow.collaborating) && (clientId !== this.collabWindow.clientId)) {
1419
1531
  return node.partialLengths!.getPartialLength(refSeq, clientId);
@@ -1676,7 +1788,12 @@ export class MergeTree {
1676
1788
  if (pendingSegmentGroup !== undefined) {
1677
1789
  const deltaSegments: IMergeTreeSegmentDelta[] = [];
1678
1790
  pendingSegmentGroup.segments.map((pendingSegment) => {
1679
- overwrite = !pendingSegment.ack(pendingSegmentGroup, opArgs, this) || overwrite;
1791
+ const overlappingRemove = !pendingSegment.ack(pendingSegmentGroup, opArgs, this);
1792
+ overwrite = overlappingRemove || overwrite;
1793
+
1794
+ if (!overlappingRemove && opArgs.op.type === MergeTreeDeltaType.REMOVE) {
1795
+ this.updateSegmentRefsAfterMarkRemoved(pendingSegment, false);
1796
+ }
1680
1797
  if (MergeTree.options.zamboniSegments) {
1681
1798
  this.addToLRUSet(pendingSegment, seq);
1682
1799
  }
@@ -2338,7 +2455,7 @@ export class MergeTree {
2338
2455
  this.ensureIntervalBoundary(end, refSeq, clientId);
2339
2456
  let segmentGroup: SegmentGroup;
2340
2457
  const removedSegments: IMergeTreeSegmentDelta[] = [];
2341
- const savedLocalRefs: LocalReferenceCollection[] = [];
2458
+ const segmentsWithRefs: ISegment[] = [];
2342
2459
  const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
2343
2460
  const markRemoved = (segment: ISegment, pos: number, _start: number, _end: number) => {
2344
2461
  const existingRemovalInfo = toRemovalInfo(segment);
@@ -2362,10 +2479,9 @@ export class MergeTree {
2362
2479
  segment.localRemovedSeq = localSeq;
2363
2480
 
2364
2481
  removedSegments.push({ segment });
2365
- if (segment.localRefs && !segment.localRefs.empty) {
2366
- savedLocalRefs.push(segment.localRefs);
2367
- }
2368
- segment.localRefs = undefined;
2482
+ }
2483
+ if (segment.localRefs && !segment.localRefs.empty) {
2484
+ segmentsWithRefs.push(segment);
2369
2485
  }
2370
2486
 
2371
2487
  // Save segment so can assign removed sequence number when acked by server
@@ -2389,37 +2505,9 @@ export class MergeTree {
2389
2505
  return true;
2390
2506
  };
2391
2507
  this.mapRange({ leaf: markRemoved, post: afterMarkRemoved }, refSeq, clientId, undefined, start, end);
2392
- if (savedLocalRefs.length > 0) {
2393
- const length = this.getLength(refSeq, clientId);
2394
- let refSegment: ISegment | undefined;
2395
- if (start < length) {
2396
- const afterSegOff = this.getContainingSegment(start, refSeq, clientId);
2397
- refSegment = afterSegOff.segment;
2398
- assert(!!refSegment, 0x052 /* "Missing reference segment!" */);
2399
- if (!refSegment.localRefs) {
2400
- refSegment.localRefs = new LocalReferenceCollection(refSegment);
2401
- }
2402
- refSegment.localRefs.addBeforeTombstones(...savedLocalRefs);
2403
- } else if (length > 0) {
2404
- const beforeSegOff = this.getContainingSegment(length - 1, refSeq, clientId);
2405
- refSegment = beforeSegOff.segment;
2406
- assert(!!refSegment, 0x053 /* "Missing reference segment!" */);
2407
- if (!refSegment.localRefs) {
2408
- refSegment.localRefs = new LocalReferenceCollection(refSegment);
2409
- }
2410
- refSegment.localRefs.addAfterTombstones(...savedLocalRefs);
2411
- } else {
2412
- // TODO: The tree is empty, so there isn't anywhere to put these
2413
- // they should be preserved somehow
2414
- for (const refsCollection of savedLocalRefs) {
2415
- refsCollection.clear();
2416
- }
2417
- }
2418
-
2419
- if (refSegment) {
2420
- this.blockUpdatePathLengths(refSegment.parent, TreeMaintenanceSequenceNumber,
2421
- LocalClientId);
2422
- }
2508
+ const pending = this.collabWindow.collaborating && clientId === this.collabWindow.clientId;
2509
+ for (const segment of segmentsWithRefs) {
2510
+ this.updateSegmentRefsAfterMarkRemoved(segment, pending);
2423
2511
  }
2424
2512
 
2425
2513
  // opArgs == undefined => test code
@@ -2445,7 +2533,7 @@ export class MergeTree {
2445
2533
  }
2446
2534
  }
2447
2535
 
2448
- public removeLocalReferencePosition(lref: ReferencePosition): ReferencePosition | undefined {
2536
+ public removeLocalReferencePosition(lref: LocalReferencePosition): LocalReferencePosition | undefined {
2449
2537
  const segment = lref.getSegment();
2450
2538
  if (segment) {
2451
2539
  const removedRefs = segment?.localRefs?.removeLocalRef(lref);
@@ -2457,9 +2545,14 @@ export class MergeTree {
2457
2545
  }
2458
2546
  }
2459
2547
  public createLocalReferencePosition(
2460
- segment: ISegment, offset: number, refType: ReferenceType, properties: PropertySet | undefined,
2548
+ segment: ISegment, offset: number | undefined, refType: ReferenceType, properties: PropertySet | undefined,
2461
2549
  client: Client,
2462
- ): ReferencePosition {
2550
+ ): LocalReferencePosition {
2551
+ if (isRemoved(segment)) {
2552
+ if (!refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove)) {
2553
+ throw new UsageError("Can only create SlideOnRemove local reference position on a removed segment");
2554
+ }
2555
+ }
2463
2556
  const localRefs = segment.localRefs ?? new LocalReferenceCollection(segment);
2464
2557
  segment.localRefs = localRefs;
2465
2558
 
@@ -13,28 +13,26 @@ import { PropertySet } from "./properties";
13
13
  import { ISegment } from "./mergeTree";
14
14
 
15
15
  export type MergeTreeDeltaOperationType =
16
- MergeTreeDeltaType.ANNOTATE | MergeTreeDeltaType.INSERT | MergeTreeDeltaType.REMOVE;
16
+ typeof MergeTreeDeltaType.ANNOTATE | typeof MergeTreeDeltaType.INSERT | typeof MergeTreeDeltaType.REMOVE;
17
17
 
18
18
  // Note: Assigned negative integers to avoid clashing with MergeTreeDeltaType
19
- export const enum MergeTreeMaintenanceType {
20
- APPEND = -1,
21
- SPLIT = -2,
19
+ export const MergeTreeMaintenanceType = {
20
+ APPEND: -1,
21
+ SPLIT: -2,
22
22
  /**
23
23
  * Notification that a segment has been unlinked from the MergeTree. This occurs during
24
24
  * Zamboni when:
25
25
  *
26
- * a) The minSeq has moved past the segment's removeSeq, in which case the segment
27
- * can no longer be referenced by incoming remote ops, and...
28
- *
29
26
  * b) The segment's tracking collection is empty (e.g., not being tracked for undo/redo).
30
27
  */
31
- UNLINK = -3,
28
+ UNLINK: -3,
32
29
  /**
33
30
  * Notification that a local change has been acknowledged by the server.
34
31
  * This means that it has made the round trip to the server and has had a sequence number assigned.
35
32
  */
36
- ACKNOWLEDGED = -4,
37
- }
33
+ ACKNOWLEDGED: -4,
34
+ } as const;
35
+ export type MergeTreeMaintenanceType = typeof MergeTreeMaintenanceType[keyof typeof MergeTreeMaintenanceType];
38
36
 
39
37
  export type MergeTreeDeltaOperationTypes = MergeTreeDeltaOperationType | MergeTreeMaintenanceType;
40
38
 
package/src/ops.ts CHANGED
@@ -11,6 +11,7 @@ export enum ReferenceType {
11
11
  RangeBegin = 0x10,
12
12
  RangeEnd = 0x20,
13
13
  SlideOnRemove = 0x40,
14
+ StayOnRemove = 0x80,
14
15
  Transient = 0x100,
15
16
  }
16
17
 
@@ -19,12 +20,14 @@ export interface IMarkerDef {
19
20
  }
20
21
 
21
22
  // Note: Assigned positive integers to avoid clashing with MergeTreeMaintenanceType
22
- export const enum MergeTreeDeltaType {
23
- INSERT = 0,
24
- REMOVE = 1,
25
- ANNOTATE = 2,
26
- GROUP = 3,
27
- }
23
+ export const MergeTreeDeltaType = {
24
+ INSERT: 0,
25
+ REMOVE: 1,
26
+ ANNOTATE: 2,
27
+ GROUP: 3,
28
+ } as const;
29
+
30
+ export type MergeTreeDeltaType = typeof MergeTreeDeltaType[keyof typeof MergeTreeDeltaType];
28
31
 
29
32
  export interface IMergeTreeDelta {
30
33
  /**
@@ -54,7 +57,7 @@ export interface IRelativePosition {
54
57
  }
55
58
 
56
59
  export interface IMergeTreeInsertMsg extends IMergeTreeDelta {
57
- type: MergeTreeDeltaType.INSERT;
60
+ type: typeof MergeTreeDeltaType.INSERT;
58
61
  pos1?: number;
59
62
  relativePos1?: IRelativePosition;
60
63
  pos2?: number;
@@ -63,7 +66,7 @@ export interface IMergeTreeInsertMsg extends IMergeTreeDelta {
63
66
  }
64
67
 
65
68
  export interface IMergeTreeRemoveMsg extends IMergeTreeDelta {
66
- type: MergeTreeDeltaType.REMOVE;
69
+ type: typeof MergeTreeDeltaType.REMOVE;
67
70
  pos1?: number;
68
71
  relativePos1?: IRelativePosition;
69
72
  pos2?: number;
@@ -78,7 +81,7 @@ export interface ICombiningOp {
78
81
  }
79
82
 
80
83
  export interface IMergeTreeAnnotateMsg extends IMergeTreeDelta {
81
- type: MergeTreeDeltaType.ANNOTATE;
84
+ type: typeof MergeTreeDeltaType.ANNOTATE;
82
85
  pos1?: number;
83
86
  relativePos1?: IRelativePosition;
84
87
  pos2?: number;
@@ -88,7 +91,7 @@ export interface IMergeTreeAnnotateMsg extends IMergeTreeDelta {
88
91
  }
89
92
 
90
93
  export interface IMergeTreeGroupMsg extends IMergeTreeDelta {
91
- type: MergeTreeDeltaType.GROUP;
94
+ type: typeof MergeTreeDeltaType.GROUP;
92
95
  ops: IMergeTreeDeltaOp[];
93
96
  }
94
97
 
@@ -11,9 +11,10 @@ import { PropertySet, MapLike } from "./properties";
11
11
  export const reservedTileLabelsKey = "referenceTileLabels";
12
12
  export const reservedRangeLabelsKey = "referenceRangeLabels";
13
13
 
14
- export function refTypeIncludesFlag(refPos: ReferencePosition, flags: ReferenceType): boolean {
14
+ export function refTypeIncludesFlag(refPosOrType: ReferencePosition | ReferenceType, flags: ReferenceType): boolean {
15
+ const refType = typeof refPosOrType === "number" ? refPosOrType : refPosOrType.refType;
15
16
  // eslint-disable-next-line no-bitwise
16
- return (refPos.refType & flags) !== 0;
17
+ return (refType & flags) !== 0;
17
18
  }
18
19
 
19
20
  export const refGetTileLabels = (refPos: ReferencePosition): string[] | undefined =>