@fluidframework/merge-tree 2.23.0 → 2.30.0

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 (53) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/mergeTree.d.ts +2 -0
  3. package/dist/mergeTree.d.ts.map +1 -1
  4. package/dist/mergeTree.js +15 -10
  5. package/dist/mergeTree.js.map +1 -1
  6. package/dist/perspective.d.ts +92 -54
  7. package/dist/perspective.d.ts.map +1 -1
  8. package/dist/perspective.js +145 -84
  9. package/dist/perspective.js.map +1 -1
  10. package/dist/stamps.d.ts +90 -0
  11. package/dist/stamps.d.ts.map +1 -0
  12. package/dist/stamps.js +90 -0
  13. package/dist/stamps.js.map +1 -0
  14. package/dist/test/perspective.spec.d.ts +6 -0
  15. package/dist/test/perspective.spec.d.ts.map +1 -0
  16. package/dist/test/perspective.spec.js +119 -0
  17. package/dist/test/perspective.spec.js.map +1 -0
  18. package/dist/test/stamps.spec.d.ts +6 -0
  19. package/dist/test/stamps.spec.d.ts.map +1 -0
  20. package/dist/test/stamps.spec.js +130 -0
  21. package/dist/test/stamps.spec.js.map +1 -0
  22. package/dist/test/testClientLogger.d.ts +9 -0
  23. package/dist/test/testClientLogger.d.ts.map +1 -1
  24. package/dist/test/testClientLogger.js +64 -45
  25. package/dist/test/testClientLogger.js.map +1 -1
  26. package/lib/mergeTree.d.ts +2 -0
  27. package/lib/mergeTree.d.ts.map +1 -1
  28. package/lib/mergeTree.js +16 -11
  29. package/lib/mergeTree.js.map +1 -1
  30. package/lib/perspective.d.ts +92 -54
  31. package/lib/perspective.d.ts.map +1 -1
  32. package/lib/perspective.js +119 -80
  33. package/lib/perspective.js.map +1 -1
  34. package/lib/stamps.d.ts +90 -0
  35. package/lib/stamps.d.ts.map +1 -0
  36. package/lib/stamps.js +77 -0
  37. package/lib/stamps.js.map +1 -0
  38. package/lib/test/perspective.spec.d.ts +6 -0
  39. package/lib/test/perspective.spec.d.ts.map +1 -0
  40. package/lib/test/perspective.spec.js +117 -0
  41. package/lib/test/perspective.spec.js.map +1 -0
  42. package/lib/test/stamps.spec.d.ts +6 -0
  43. package/lib/test/stamps.spec.d.ts.map +1 -0
  44. package/lib/test/stamps.spec.js +105 -0
  45. package/lib/test/stamps.spec.js.map +1 -0
  46. package/lib/test/testClientLogger.d.ts +9 -0
  47. package/lib/test/testClientLogger.d.ts.map +1 -1
  48. package/lib/test/testClientLogger.js +65 -46
  49. package/lib/test/testClientLogger.js.map +1 -1
  50. package/package.json +17 -17
  51. package/src/mergeTree.ts +32 -11
  52. package/src/perspective.ts +184 -108
  53. package/src/stamps.ts +164 -0
@@ -4,86 +4,124 @@
4
4
  */
5
5
  import { type MergeTree } from "./mergeTree.js";
6
6
  import { type ISegmentLeaf } from "./mergeTreeNodes.js";
7
- import { type IInsertionInfo, type IMoveInfo, type IRemovalInfo, type SegmentWithInfo } from "./segmentInfos.js";
7
+ import type { OperationStamp, InsertOperationStamp, RemoveOperationStamp } from "./stamps.js";
8
8
  /**
9
- * Provides a view of a MergeTree from the perspective of a specific client at a specific sequence number.
9
+ * A perspective which includes some subset of operations known to the local client.
10
+ *
11
+ * This helps the local client reason about the state of other clients when they issued an operation.
10
12
  */
11
13
  export interface Perspective {
12
- nextSegment(segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;
13
- previousSegment(segment: ISegmentLeaf): ISegmentLeaf;
14
- }
15
- /**
16
- * Represents a point in time inside the collaboration window.
17
- */
18
- export interface SeqTime {
19
- refSeq: number;
20
- localSeq?: number;
21
- }
22
- /**
23
- * Implementation of {@link Perspective}.
24
- * @privateRemarks
25
- * TODO:AB#29765: This class does not support non-local-client perspectives, but should.
26
- */
27
- export declare class PerspectiveImpl implements Perspective {
28
- private readonly _mergeTree;
29
- private readonly _seqTime;
30
14
  /**
31
- * @param _mergeTree - The {@link MergeTree} to view.
32
- * @param _seqTime - The latest sequence number and local sequence number to consider.
15
+ * The sequence number last seen from this perspective. Same concept as `ISequencedDocumentMessage.referenceSequenceNumber`.
16
+ * @privateRemarks
17
+ * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends
18
+ * on the (refSeq, clientId, localSeq?) representation of perspectives.
19
+ */
20
+ readonly refSeq: number;
21
+ /**
22
+ * The client id for this perspective.
23
+ * @privateRemarks
24
+ * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends
25
+ * on the (refSeq, clientId, localSeq?) representation of perspectives.
26
+ */
27
+ readonly clientId: number;
28
+ /**
29
+ * When this is a local perspective, the local sequence number last seen from this perspective.
30
+ *
31
+ * Perspectives with defined `localSeq` values are useful in reconnection flows, where the local client may need to resend some
32
+ * of its ops after rederiving their new equivalents.
33
+ * @privateRemarks
34
+ * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends
35
+ * on the (refSeq, clientId, localSeq?) representation of perspectives.
36
+ */
37
+ readonly localSeq?: number;
38
+ /**
39
+ * @returns Whether the segment is present (visible) from this perspective
40
+ */
41
+ isSegmentPresent(segment: ISegmentLeaf): boolean;
42
+ /**
43
+ * @returns Whether this perspective has seen the given operation.
33
44
  */
34
- constructor(_mergeTree: MergeTree, _seqTime: SeqTime);
45
+ hasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;
46
+ nextSegment(mergeTree: MergeTree, segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;
47
+ previousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf;
48
+ }
49
+ declare abstract class PerspectiveBase {
50
+ abstract hasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;
35
51
  /**
36
52
  * Returns the immediately adjacent segment in the specified direction from this perspective.
37
53
  * There may actually be multiple segments between the given segment and the returned segment,
38
- * but they were either inserted after this perspective, or have been removed or moved before this perspective.
54
+ * but they were either inserted after this perspective, or have been removed before this perspective.
39
55
  *
40
56
  * @param segment - The segment to start from.
41
57
  * @param forward - The direction to search.
42
58
  * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
43
59
  */
44
- nextSegment(segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;
60
+ nextSegment(mergeTree: MergeTree, segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;
45
61
  /**
46
62
  * Finds the segment prior to the given segment.
47
63
  * @param segment - The segment to start from.
48
64
  * @returns the previous segment, or the start of the tree if there is no previous segment.
49
65
  * @remarks This is a convenient equivalent to calling `nextSegment(segment, false)`.
50
66
  */
51
- previousSegment(segment: ISegmentLeaf): ISegmentLeaf;
67
+ previousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf;
68
+ isSegmentPresent(seg: ISegmentLeaf): boolean;
52
69
  }
53
70
  /**
54
- * Determines if the given segment was removed before the given perspective.
55
- * @param seg - The segment to check.
56
- * @param seq - The latest sequence number to consider.
57
- * @param localSeq - The latest local sequence number to consider.
58
- * @returns true iff this segment was removed in the given perspective.
59
- * @privateRemarks
60
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
71
+ * A perspective which includes edits at or before some reference sequence number alongside all edits from some particular client.
72
+ *
73
+ * @remarks
74
+ * This works for both the local client as well as remote clients since refSeq-based checks disallow unacked edits, but the clientId check
75
+ * catches unacked edits from the local client.
61
76
  */
62
- export declare function wasRemovedBefore(seg: SegmentWithInfo<IInsertionInfo & IRemovalInfo>, { refSeq, localSeq }: SeqTime): boolean;
77
+ export declare class PriorPerspective extends PerspectiveBase implements Perspective {
78
+ readonly refSeq: number;
79
+ readonly clientId: number;
80
+ constructor(refSeq: number, clientId: number);
81
+ hasOccurred(stamp: OperationStamp): boolean;
82
+ }
63
83
  /**
64
- * Determines if the given segment was moved before the given perspective.
65
- * @param seg - The segment to check.
66
- * @param refSeq - The latest sequence number to consider.
67
- * @param localSeq - The latest local sequence number to consider.
68
- * @returns true iff this segment was moved (aka obliterated) in the given perspective.
69
- * @privateRemarks
70
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
84
+ * A perspective which includes edits which were either:
85
+ * - acked and at or before some reference sequence number
86
+ * - unacked, but at or before some local sequence number
87
+ *
88
+ * This is a useful perspective when the local client is in the process of reconnecting, since it must
89
+ * rederive positions for unacked ops while only considering a portion of its own edits as having been applied.
71
90
  */
72
- export declare function wasMovedBefore(seg: SegmentWithInfo<IInsertionInfo & IMoveInfo>, { refSeq, localSeq }: SeqTime): boolean;
91
+ export declare class LocalReconnectingPerspective extends PerspectiveBase implements Perspective {
92
+ readonly refSeq: number;
93
+ readonly clientId: number;
94
+ readonly localSeq: number;
95
+ constructor(refSeq: number, clientId: number, localSeq: number);
96
+ hasOccurred(stamp: OperationStamp): boolean;
97
+ }
73
98
  /**
74
- * See {@link wasRemovedBefore} and {@link wasMovedBefore}.
75
- * @privateRemarks
76
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
99
+ * A perspective which includes all known edits.
100
+ *
101
+ * This is the perspective that the application sees.
102
+ * @remarks
103
+ * This can be represented using {@link PriorPerspective} with a refSeq of `Number.MAX_SAFE_INTEGER`, but having an explicit
104
+ * variant of this perspective renders extra refSeq checks unnecessary and is a bit easier to read.
77
105
  */
78
- export declare function wasRemovedOrMovedBefore(seg: ISegmentLeaf, seqTime: SeqTime): boolean;
106
+ export declare class LocalDefaultPerspective extends PerspectiveBase implements Perspective {
107
+ readonly clientId: number;
108
+ readonly refSeq: number;
109
+ constructor(clientId: number);
110
+ hasOccurred(_stamp: OperationStamp): boolean;
111
+ }
79
112
  /**
80
- * Determines if the given segment is present in the given perspective.
81
- * @param seg - The segment to check.
82
- * @param seqTime - The latest sequence number and local sequence number to consider.
83
- * @returns true iff this segment was inserted before the given perspective,
84
- * and it was not removed or moved in the given perspective.
85
- * @privateRemarks
86
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
113
+ * A perspective dictating whether segments are 'visible' to a remote obliterate operation.
114
+ *
115
+ * NOTE: Beware that partial lengths doesn't support this perspective, in the sense that consulting partial lengths' for the length of a block
116
+ * can give different results than summing the lengths of present segments in that block.
117
+ * This ends up not affecting the current obliterate implementation (which has some special casing in the mapRange calls it uses),
118
+ * but use with caution.
87
119
  */
88
- export declare function isSegmentPresent(seg: ISegmentLeaf, seqTime: SeqTime): boolean;
120
+ export declare class RemoteObliteratePerspective extends PerspectiveBase implements Perspective {
121
+ readonly clientId: number;
122
+ readonly refSeq: number;
123
+ constructor(clientId: number);
124
+ hasOccurred(stamp: InsertOperationStamp | RemoveOperationStamp): boolean;
125
+ }
126
+ export {};
89
127
  //# sourceMappingURL=perspective.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"perspective.d.ts","sourceRoot":"","sources":["../src/perspective.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAIN,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,MAAM,mBAAmB,CAAC;AAE3B;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;IACpE,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,WAAW;IAMjD,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAN1B;;;OAGG;gBAEe,UAAU,EAAE,SAAS,EACrB,QAAQ,EAAE,OAAO;IAGnC;;;;;;;;OAQG;IACI,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,OAAc,GAAG,YAAY;IAYhF;;;;;OAKG;IACI,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY;CAG3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,eAAe,CAAC,cAAc,GAAG,YAAY,CAAC,EACnD,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,OAAO,GAC3B,OAAO,CAST;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC7B,GAAG,EAAE,eAAe,CAAC,cAAc,GAAG,SAAS,CAAC,EAChD,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,OAAO,GAC3B,OAAO,CAST;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAMpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAsB7E"}
1
+ {"version":3,"file":"perspective.d.ts","sourceRoot":"","sources":["../src/perspective.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAE9F;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;IAEjD;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,oBAAoB,GAAG,oBAAoB,GAAG,OAAO,CAAC;IAEzE,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;IAC1F,eAAe,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY,CAAC;CAC3E;AAED,uBAAe,eAAe;IAC7B,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,oBAAoB,GAAG,oBAAoB,GAAG,OAAO;IAEjF;;;;;;;;OAQG;IACI,WAAW,CACjB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,OAAc,GACrB,YAAY;IAYf;;;;;OAKG;IACI,eAAe,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY;IAI1E,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO;CAsDnD;AAED;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,eAAgB,YAAW,WAAW;aAE1D,MAAM,EAAE,MAAM;aACd,QAAQ,EAAE,MAAM;gBADhB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM;IAK1B,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO;CAKlD;AAED;;;;;;;GAOG;AACH,qBAAa,4BAA6B,SAAQ,eAAgB,YAAW,WAAW;aAEtE,MAAM,EAAE,MAAM;aACd,QAAQ,EAAE,MAAM;aAChB,QAAQ,EAAE,MAAM;gBAFhB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;IAK1B,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO;CAMlD;AAED;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,eAAgB,YAAW,WAAW;aAG/C,QAAQ,EAAE,MAAM;IAFnD,SAAgB,MAAM,SAA2B;gBAEd,QAAQ,EAAE,MAAM;IAI5C,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO;CAGnD;AAED;;;;;;;GAOG;AACH,qBAAa,2BAA4B,SAAQ,eAAgB,YAAW,WAAW;aAG1D,QAAQ,EAAE,MAAM;IAF5C,SAAgB,MAAM,SAA2B;gBAErB,QAAQ,EAAE,MAAM;IAIrC,WAAW,CAAC,KAAK,EAAE,oBAAoB,GAAG,oBAAoB,GAAG,OAAO;CAY/E"}
@@ -2,43 +2,31 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { UnassignedSequenceNumber } from "./constants.js";
5
+ import { LocalClientId, UnassignedSequenceNumber } from "./constants.js";
6
6
  import { LeafAction, backwardExcursion, forwardExcursion } from "./mergeTreeNodeWalk.js";
7
7
  import { seqLTE } from "./mergeTreeNodes.js";
8
- import { isInserted, isMoved, isRemoved, } from "./segmentInfos.js";
9
- /**
10
- * Implementation of {@link Perspective}.
11
- * @privateRemarks
12
- * TODO:AB#29765: This class does not support non-local-client perspectives, but should.
13
- */
14
- export class PerspectiveImpl {
15
- /**
16
- * @param _mergeTree - The {@link MergeTree} to view.
17
- * @param _seqTime - The latest sequence number and local sequence number to consider.
18
- */
19
- constructor(_mergeTree, _seqTime) {
20
- this._mergeTree = _mergeTree;
21
- this._seqTime = _seqTime;
22
- }
8
+ import { isInserted, toMoveInfo, toRemovalInfo } from "./segmentInfos.js";
9
+ import * as opstampUtils from "./stamps.js";
10
+ class PerspectiveBase {
23
11
  /**
24
12
  * Returns the immediately adjacent segment in the specified direction from this perspective.
25
13
  * There may actually be multiple segments between the given segment and the returned segment,
26
- * but they were either inserted after this perspective, or have been removed or moved before this perspective.
14
+ * but they were either inserted after this perspective, or have been removed before this perspective.
27
15
  *
28
16
  * @param segment - The segment to start from.
29
17
  * @param forward - The direction to search.
30
18
  * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
31
19
  */
32
- nextSegment(segment, forward = true) {
20
+ nextSegment(mergeTree, segment, forward = true) {
33
21
  let next;
34
22
  const action = (seg) => {
35
- if (isSegmentPresent(seg, this._seqTime)) {
23
+ if (this.isSegmentPresent(seg)) {
36
24
  next = seg;
37
25
  return LeafAction.Exit;
38
26
  }
39
27
  };
40
28
  (forward ? forwardExcursion : backwardExcursion)(segment, action);
41
- return next ?? (forward ? this._mergeTree.endOfTree : this._mergeTree.startOfTree);
29
+ return next ?? (forward ? mergeTree.endOfTree : mergeTree.startOfTree);
42
30
  }
43
31
  /**
44
32
  * Finds the segment prior to the given segment.
@@ -46,83 +34,134 @@ export class PerspectiveImpl {
46
34
  * @returns the previous segment, or the start of the tree if there is no previous segment.
47
35
  * @remarks This is a convenient equivalent to calling `nextSegment(segment, false)`.
48
36
  */
49
- previousSegment(segment) {
50
- return this.nextSegment(segment, false);
37
+ previousSegment(mergeTree, segment) {
38
+ return this.nextSegment(mergeTree, segment, false);
39
+ }
40
+ isSegmentPresent(seg) {
41
+ const insert = {
42
+ type: "insert",
43
+ clientId: seg.clientId,
44
+ seq: seg.seq,
45
+ localSeq: seg.localSeq,
46
+ };
47
+ if (isInserted(seg) && !this.hasOccurred(insert)) {
48
+ return false;
49
+ }
50
+ const removes = [];
51
+ const removalInfo = toRemovalInfo(seg);
52
+ if (removalInfo !== undefined) {
53
+ removes.push(...removalInfo.removedClientIds.map((clientId) => (clientId === LocalClientId || clientId === 0) &&
54
+ removalInfo.localRemovedSeq !== undefined
55
+ ? {
56
+ type: "setRemove",
57
+ seq: UnassignedSequenceNumber,
58
+ clientId,
59
+ localSeq: removalInfo.localRemovedSeq,
60
+ }
61
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
62
+ { type: "setRemove", seq: removalInfo.removedSeq, clientId }));
63
+ }
64
+ const moveInfo = toMoveInfo(seg);
65
+ if (moveInfo !== undefined) {
66
+ removes.push(...moveInfo.movedClientIds.map((clientId, index) => (clientId === LocalClientId || clientId === 0) &&
67
+ moveInfo.localMovedSeq !== undefined
68
+ ? {
69
+ type: "sliceRemove",
70
+ seq: UnassignedSequenceNumber,
71
+ clientId,
72
+ localSeq: moveInfo.localMovedSeq,
73
+ }
74
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
75
+ { type: "setRemove", seq: moveInfo.movedSeqs[index], clientId }));
76
+ }
77
+ if (removes.some((remove) => this.hasOccurred(remove))) {
78
+ return false;
79
+ }
80
+ return true;
51
81
  }
52
82
  }
53
83
  /**
54
- * Determines if the given segment was removed before the given perspective.
55
- * @param seg - The segment to check.
56
- * @param seq - The latest sequence number to consider.
57
- * @param localSeq - The latest local sequence number to consider.
58
- * @returns true iff this segment was removed in the given perspective.
59
- * @privateRemarks
60
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
84
+ * A perspective which includes edits at or before some reference sequence number alongside all edits from some particular client.
85
+ *
86
+ * @remarks
87
+ * This works for both the local client as well as remote clients since refSeq-based checks disallow unacked edits, but the clientId check
88
+ * catches unacked edits from the local client.
61
89
  */
62
- export function wasRemovedBefore(seg, { refSeq, localSeq }) {
63
- if (seg.removedSeq === UnassignedSequenceNumber &&
64
- localSeq !== undefined &&
65
- seg.localRemovedSeq !== undefined) {
66
- return seg.localRemovedSeq <= localSeq;
90
+ export class PriorPerspective extends PerspectiveBase {
91
+ constructor(refSeq, clientId) {
92
+ super();
93
+ this.refSeq = refSeq;
94
+ this.clientId = clientId;
95
+ }
96
+ hasOccurred(stamp) {
97
+ const predatesViaRefSeq = seqLTE(stamp.seq, this.refSeq);
98
+ const predatesViaSameClient = stamp.clientId === this.clientId;
99
+ return predatesViaRefSeq || predatesViaSameClient;
67
100
  }
68
- return seg.removedSeq !== undefined && seqLTE(seg.removedSeq, refSeq);
69
101
  }
70
102
  /**
71
- * Determines if the given segment was moved before the given perspective.
72
- * @param seg - The segment to check.
73
- * @param refSeq - The latest sequence number to consider.
74
- * @param localSeq - The latest local sequence number to consider.
75
- * @returns true iff this segment was moved (aka obliterated) in the given perspective.
76
- * @privateRemarks
77
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
103
+ * A perspective which includes edits which were either:
104
+ * - acked and at or before some reference sequence number
105
+ * - unacked, but at or before some local sequence number
106
+ *
107
+ * This is a useful perspective when the local client is in the process of reconnecting, since it must
108
+ * rederive positions for unacked ops while only considering a portion of its own edits as having been applied.
78
109
  */
79
- export function wasMovedBefore(seg, { refSeq, localSeq }) {
80
- if (seg.movedSeq === UnassignedSequenceNumber &&
81
- localSeq !== undefined &&
82
- seg.localMovedSeq !== undefined) {
83
- return seg.localMovedSeq <= localSeq;
110
+ export class LocalReconnectingPerspective extends PerspectiveBase {
111
+ constructor(refSeq, clientId, localSeq) {
112
+ super();
113
+ this.refSeq = refSeq;
114
+ this.clientId = clientId;
115
+ this.localSeq = localSeq;
116
+ }
117
+ hasOccurred(stamp) {
118
+ const predatesViaRefSeq = seqLTE(stamp.seq, this.refSeq);
119
+ const predatesViaLocalSeq = stamp.localSeq !== undefined && stamp.localSeq <= this.localSeq;
120
+ return predatesViaRefSeq || predatesViaLocalSeq;
84
121
  }
85
- return seg.movedSeq !== undefined && seqLTE(seg.movedSeq, refSeq);
86
122
  }
87
123
  /**
88
- * See {@link wasRemovedBefore} and {@link wasMovedBefore}.
89
- * @privateRemarks
90
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
124
+ * A perspective which includes all known edits.
125
+ *
126
+ * This is the perspective that the application sees.
127
+ * @remarks
128
+ * This can be represented using {@link PriorPerspective} with a refSeq of `Number.MAX_SAFE_INTEGER`, but having an explicit
129
+ * variant of this perspective renders extra refSeq checks unnecessary and is a bit easier to read.
91
130
  */
92
- export function wasRemovedOrMovedBefore(seg, seqTime) {
93
- return (isInserted(seg) &&
94
- ((isRemoved(seg) && wasRemovedBefore(seg, seqTime)) ||
95
- (isMoved(seg) && wasMovedBefore(seg, seqTime))));
131
+ export class LocalDefaultPerspective extends PerspectiveBase {
132
+ constructor(clientId) {
133
+ super();
134
+ this.clientId = clientId;
135
+ this.refSeq = Number.MAX_SAFE_INTEGER;
136
+ }
137
+ hasOccurred(_stamp) {
138
+ return true;
139
+ }
96
140
  }
97
141
  /**
98
- * Determines if the given segment is present in the given perspective.
99
- * @param seg - The segment to check.
100
- * @param seqTime - The latest sequence number and local sequence number to consider.
101
- * @returns true iff this segment was inserted before the given perspective,
102
- * and it was not removed or moved in the given perspective.
103
- * @privateRemarks
104
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
142
+ * A perspective dictating whether segments are 'visible' to a remote obliterate operation.
143
+ *
144
+ * NOTE: Beware that partial lengths doesn't support this perspective, in the sense that consulting partial lengths' for the length of a block
145
+ * can give different results than summing the lengths of present segments in that block.
146
+ * This ends up not affecting the current obliterate implementation (which has some special casing in the mapRange calls it uses),
147
+ * but use with caution.
105
148
  */
106
- export function isSegmentPresent(seg, seqTime) {
107
- const { refSeq, localSeq } = seqTime;
108
- // If seg.seq is undefined, then this segment has existed since minSeq.
109
- // It may have been moved or removed since.
110
- if (isInserted(seg)) {
111
- if (seg.seq !== UnassignedSequenceNumber) {
112
- if (!seqLTE(seg.seq, refSeq)) {
113
- return false;
114
- }
115
- }
116
- else if (seg.localSeq !== undefined && // seg.seq === UnassignedSequenceNumber
117
- // If the current perspective does not include local sequence numbers,
118
- // then this segment does not exist yet.
119
- (localSeq === undefined || seg.localSeq > localSeq)) {
149
+ export class RemoteObliteratePerspective extends PerspectiveBase {
150
+ constructor(clientId) {
151
+ super();
152
+ this.clientId = clientId;
153
+ this.refSeq = Number.MAX_SAFE_INTEGER;
154
+ }
155
+ hasOccurred(stamp) {
156
+ // Local-only removals are not visible to an obliterate operation, since this means the local removal was concurrent
157
+ // to a remote obliterate and we may need to mark the segment appropriately to reflect this overlapping remove.
158
+ // Every other type of operation is visible: obliterates do not affect segments that have already been removed and acked,
159
+ // and they always affect segments within their range that have not been removed, even if those segments were inserted
160
+ // after the obliterate's refSeq.
161
+ if (stamp.type !== "insert" && opstampUtils.isLocal(stamp)) {
120
162
  return false;
121
163
  }
164
+ return true;
122
165
  }
123
- if (wasRemovedOrMovedBefore(seg, seqTime)) {
124
- return false;
125
- }
126
- return true;
127
166
  }
128
167
  //# sourceMappingURL=perspective.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"perspective.js","sourceRoot":"","sources":["../src/perspective.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { UnassignedSequenceNumber } from \"./constants.js\";\nimport { type MergeTree } from \"./mergeTree.js\";\nimport { LeafAction, backwardExcursion, forwardExcursion } from \"./mergeTreeNodeWalk.js\";\nimport { seqLTE, type ISegmentLeaf } from \"./mergeTreeNodes.js\";\nimport {\n\tisInserted,\n\tisMoved,\n\tisRemoved,\n\ttype IInsertionInfo,\n\ttype IMoveInfo,\n\ttype IRemovalInfo,\n\ttype SegmentWithInfo,\n} from \"./segmentInfos.js\";\n\n/**\n * Provides a view of a MergeTree from the perspective of a specific client at a specific sequence number.\n */\nexport interface Perspective {\n\tnextSegment(segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;\n\tpreviousSegment(segment: ISegmentLeaf): ISegmentLeaf;\n}\n\n/**\n * Represents a point in time inside the collaboration window.\n */\nexport interface SeqTime {\n\trefSeq: number;\n\tlocalSeq?: number;\n}\n\n/**\n * Implementation of {@link Perspective}.\n * @privateRemarks\n * TODO:AB#29765: This class does not support non-local-client perspectives, but should.\n */\nexport class PerspectiveImpl implements Perspective {\n\t/**\n\t * @param _mergeTree - The {@link MergeTree} to view.\n\t * @param _seqTime - The latest sequence number and local sequence number to consider.\n\t */\n\tpublic constructor(\n\t\tprivate readonly _mergeTree: MergeTree,\n\t\tprivate readonly _seqTime: SeqTime,\n\t) {}\n\n\t/**\n\t * Returns the immediately adjacent segment in the specified direction from this perspective.\n\t * There may actually be multiple segments between the given segment and the returned segment,\n\t * but they were either inserted after this perspective, or have been removed or moved before this perspective.\n\t *\n\t * @param segment - The segment to start from.\n\t * @param forward - The direction to search.\n\t * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.\n\t */\n\tpublic nextSegment(segment: ISegmentLeaf, forward: boolean = true): ISegmentLeaf {\n\t\tlet next: ISegmentLeaf | undefined;\n\t\tconst action = (seg: ISegmentLeaf): boolean | undefined => {\n\t\t\tif (isSegmentPresent(seg, this._seqTime)) {\n\t\t\t\tnext = seg;\n\t\t\t\treturn LeafAction.Exit;\n\t\t\t}\n\t\t};\n\t\t(forward ? forwardExcursion : backwardExcursion)(segment, action);\n\t\treturn next ?? (forward ? this._mergeTree.endOfTree : this._mergeTree.startOfTree);\n\t}\n\n\t/**\n\t * Finds the segment prior to the given segment.\n\t * @param segment - The segment to start from.\n\t * @returns the previous segment, or the start of the tree if there is no previous segment.\n\t * @remarks This is a convenient equivalent to calling `nextSegment(segment, false)`.\n\t */\n\tpublic previousSegment(segment: ISegmentLeaf): ISegmentLeaf {\n\t\treturn this.nextSegment(segment, false);\n\t}\n}\n\n/**\n * Determines if the given segment was removed before the given perspective.\n * @param seg - The segment to check.\n * @param seq - The latest sequence number to consider.\n * @param localSeq - The latest local sequence number to consider.\n * @returns true iff this segment was removed in the given perspective.\n * @privateRemarks\n * TODO:AB#29765: This function does not support non-local-client perspectives, but should.\n */\nexport function wasRemovedBefore(\n\tseg: SegmentWithInfo<IInsertionInfo & IRemovalInfo>,\n\t{ refSeq, localSeq }: SeqTime,\n): boolean {\n\tif (\n\t\tseg.removedSeq === UnassignedSequenceNumber &&\n\t\tlocalSeq !== undefined &&\n\t\tseg.localRemovedSeq !== undefined\n\t) {\n\t\treturn seg.localRemovedSeq <= localSeq;\n\t}\n\treturn seg.removedSeq !== undefined && seqLTE(seg.removedSeq, refSeq);\n}\n\n/**\n * Determines if the given segment was moved before the given perspective.\n * @param seg - The segment to check.\n * @param refSeq - The latest sequence number to consider.\n * @param localSeq - The latest local sequence number to consider.\n * @returns true iff this segment was moved (aka obliterated) in the given perspective.\n * @privateRemarks\n * TODO:AB#29765: This function does not support non-local-client perspectives, but should.\n */\nexport function wasMovedBefore(\n\tseg: SegmentWithInfo<IInsertionInfo & IMoveInfo>,\n\t{ refSeq, localSeq }: SeqTime,\n): boolean {\n\tif (\n\t\tseg.movedSeq === UnassignedSequenceNumber &&\n\t\tlocalSeq !== undefined &&\n\t\tseg.localMovedSeq !== undefined\n\t) {\n\t\treturn seg.localMovedSeq <= localSeq;\n\t}\n\treturn seg.movedSeq !== undefined && seqLTE(seg.movedSeq, refSeq);\n}\n\n/**\n * See {@link wasRemovedBefore} and {@link wasMovedBefore}.\n * @privateRemarks\n * TODO:AB#29765: This function does not support non-local-client perspectives, but should.\n */\nexport function wasRemovedOrMovedBefore(seg: ISegmentLeaf, seqTime: SeqTime): boolean {\n\treturn (\n\t\tisInserted(seg) &&\n\t\t((isRemoved(seg) && wasRemovedBefore(seg, seqTime)) ||\n\t\t\t(isMoved(seg) && wasMovedBefore(seg, seqTime)))\n\t);\n}\n\n/**\n * Determines if the given segment is present in the given perspective.\n * @param seg - The segment to check.\n * @param seqTime - The latest sequence number and local sequence number to consider.\n * @returns true iff this segment was inserted before the given perspective,\n * and it was not removed or moved in the given perspective.\n * @privateRemarks\n * TODO:AB#29765: This function does not support non-local-client perspectives, but should.\n */\nexport function isSegmentPresent(seg: ISegmentLeaf, seqTime: SeqTime): boolean {\n\tconst { refSeq, localSeq } = seqTime;\n\t// If seg.seq is undefined, then this segment has existed since minSeq.\n\t// It may have been moved or removed since.\n\tif (isInserted(seg)) {\n\t\tif (seg.seq !== UnassignedSequenceNumber) {\n\t\t\tif (!seqLTE(seg.seq, refSeq)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (\n\t\t\tseg.localSeq !== undefined && // seg.seq === UnassignedSequenceNumber\n\t\t\t// If the current perspective does not include local sequence numbers,\n\t\t\t// then this segment does not exist yet.\n\t\t\t(localSeq === undefined || seg.localSeq > localSeq)\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tif (wasRemovedOrMovedBefore(seg, seqTime)) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"perspective.js","sourceRoot":"","sources":["../src/perspective.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { LocalClientId, UnassignedSequenceNumber } from \"./constants.js\";\nimport { type MergeTree } from \"./mergeTree.js\";\nimport { LeafAction, backwardExcursion, forwardExcursion } from \"./mergeTreeNodeWalk.js\";\nimport { seqLTE, type ISegmentLeaf } from \"./mergeTreeNodes.js\";\nimport { isInserted, toMoveInfo, toRemovalInfo } from \"./segmentInfos.js\";\nimport * as opstampUtils from \"./stamps.js\";\nimport type { OperationStamp, InsertOperationStamp, RemoveOperationStamp } from \"./stamps.js\";\n\n/**\n * A perspective which includes some subset of operations known to the local client.\n *\n * This helps the local client reason about the state of other clients when they issued an operation.\n */\nexport interface Perspective {\n\t/**\n\t * The sequence number last seen from this perspective. Same concept as `ISequencedDocumentMessage.referenceSequenceNumber`.\n\t * @privateRemarks\n\t * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends\n\t * on the (refSeq, clientId, localSeq?) representation of perspectives.\n\t */\n\treadonly refSeq: number;\n\n\t/**\n\t * The client id for this perspective.\n\t * @privateRemarks\n\t * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends\n\t * on the (refSeq, clientId, localSeq?) representation of perspectives.\n\t */\n\treadonly clientId: number;\n\n\t/**\n\t * When this is a local perspective, the local sequence number last seen from this perspective.\n\t *\n\t * Perspectives with defined `localSeq` values are useful in reconnection flows, where the local client may need to resend some\n\t * of its ops after rederiving their new equivalents.\n\t * @privateRemarks\n\t * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends\n\t * on the (refSeq, clientId, localSeq?) representation of perspectives.\n\t */\n\treadonly localSeq?: number;\n\n\t/**\n\t * @returns Whether the segment is present (visible) from this perspective\n\t */\n\tisSegmentPresent(segment: ISegmentLeaf): boolean;\n\n\t/**\n\t * @returns Whether this perspective has seen the given operation.\n\t */\n\thasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;\n\n\tnextSegment(mergeTree: MergeTree, segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;\n\tpreviousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf;\n}\n\nabstract class PerspectiveBase {\n\tabstract hasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;\n\n\t/**\n\t * Returns the immediately adjacent segment in the specified direction from this perspective.\n\t * There may actually be multiple segments between the given segment and the returned segment,\n\t * but they were either inserted after this perspective, or have been removed before this perspective.\n\t *\n\t * @param segment - The segment to start from.\n\t * @param forward - The direction to search.\n\t * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.\n\t */\n\tpublic nextSegment(\n\t\tmergeTree: MergeTree,\n\t\tsegment: ISegmentLeaf,\n\t\tforward: boolean = true,\n\t): ISegmentLeaf {\n\t\tlet next: ISegmentLeaf | undefined;\n\t\tconst action = (seg: ISegmentLeaf): boolean | undefined => {\n\t\t\tif (this.isSegmentPresent(seg)) {\n\t\t\t\tnext = seg;\n\t\t\t\treturn LeafAction.Exit;\n\t\t\t}\n\t\t};\n\t\t(forward ? forwardExcursion : backwardExcursion)(segment, action);\n\t\treturn next ?? (forward ? mergeTree.endOfTree : mergeTree.startOfTree);\n\t}\n\n\t/**\n\t * Finds the segment prior to the given segment.\n\t * @param segment - The segment to start from.\n\t * @returns the previous segment, or the start of the tree if there is no previous segment.\n\t * @remarks This is a convenient equivalent to calling `nextSegment(segment, false)`.\n\t */\n\tpublic previousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf {\n\t\treturn this.nextSegment(mergeTree, segment, false);\n\t}\n\n\tpublic isSegmentPresent(seg: ISegmentLeaf): boolean {\n\t\tconst insert: InsertOperationStamp = {\n\t\t\ttype: \"insert\",\n\t\t\tclientId: seg.clientId,\n\t\t\tseq: seg.seq,\n\t\t\tlocalSeq: seg.localSeq,\n\t\t};\n\t\tif (isInserted(seg) && !this.hasOccurred(insert)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst removes: RemoveOperationStamp[] = [];\n\t\tconst removalInfo = toRemovalInfo(seg);\n\t\tif (removalInfo !== undefined) {\n\t\t\tremoves.push(\n\t\t\t\t...removalInfo.removedClientIds.map((clientId) =>\n\t\t\t\t\t(clientId === LocalClientId || clientId === 0) &&\n\t\t\t\t\tremovalInfo.localRemovedSeq !== undefined\n\t\t\t\t\t\t? ({\n\t\t\t\t\t\t\t\ttype: \"setRemove\",\n\t\t\t\t\t\t\t\tseq: UnassignedSequenceNumber,\n\t\t\t\t\t\t\t\tclientId,\n\t\t\t\t\t\t\t\tlocalSeq: removalInfo.localRemovedSeq,\n\t\t\t\t\t\t\t} as const)\n\t\t\t\t\t\t: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n\t\t\t\t\t\t\t({ type: \"setRemove\", seq: removalInfo.removedSeq, clientId } as const),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tconst moveInfo = toMoveInfo(seg);\n\t\tif (moveInfo !== undefined) {\n\t\t\tremoves.push(\n\t\t\t\t...moveInfo.movedClientIds.map((clientId, index) =>\n\t\t\t\t\t(clientId === LocalClientId || clientId === 0) &&\n\t\t\t\t\tmoveInfo.localMovedSeq !== undefined\n\t\t\t\t\t\t? ({\n\t\t\t\t\t\t\t\ttype: \"sliceRemove\",\n\t\t\t\t\t\t\t\tseq: UnassignedSequenceNumber,\n\t\t\t\t\t\t\t\tclientId,\n\t\t\t\t\t\t\t\tlocalSeq: moveInfo.localMovedSeq,\n\t\t\t\t\t\t\t} as const)\n\t\t\t\t\t\t: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n\t\t\t\t\t\t\t({ type: \"setRemove\", seq: moveInfo.movedSeqs[index]!, clientId } as const),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tif (removes.some((remove) => this.hasOccurred(remove))) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n\n/**\n * A perspective which includes edits at or before some reference sequence number alongside all edits from some particular client.\n *\n * @remarks\n * This works for both the local client as well as remote clients since refSeq-based checks disallow unacked edits, but the clientId check\n * catches unacked edits from the local client.\n */\nexport class PriorPerspective extends PerspectiveBase implements Perspective {\n\tpublic constructor(\n\t\tpublic readonly refSeq: number,\n\t\tpublic readonly clientId: number,\n\t) {\n\t\tsuper();\n\t}\n\n\tpublic hasOccurred(stamp: OperationStamp): boolean {\n\t\tconst predatesViaRefSeq = seqLTE(stamp.seq, this.refSeq);\n\t\tconst predatesViaSameClient = stamp.clientId === this.clientId;\n\t\treturn predatesViaRefSeq || predatesViaSameClient;\n\t}\n}\n\n/**\n * A perspective which includes edits which were either:\n * - acked and at or before some reference sequence number\n * - unacked, but at or before some local sequence number\n *\n * This is a useful perspective when the local client is in the process of reconnecting, since it must\n * rederive positions for unacked ops while only considering a portion of its own edits as having been applied.\n */\nexport class LocalReconnectingPerspective extends PerspectiveBase implements Perspective {\n\tpublic constructor(\n\t\tpublic readonly refSeq: number,\n\t\tpublic readonly clientId: number,\n\t\tpublic readonly localSeq: number,\n\t) {\n\t\tsuper();\n\t}\n\n\tpublic hasOccurred(stamp: OperationStamp): boolean {\n\t\tconst predatesViaRefSeq = seqLTE(stamp.seq, this.refSeq);\n\t\tconst predatesViaLocalSeq =\n\t\t\tstamp.localSeq !== undefined && stamp.localSeq <= this.localSeq;\n\t\treturn predatesViaRefSeq || predatesViaLocalSeq;\n\t}\n}\n\n/**\n * A perspective which includes all known edits.\n *\n * This is the perspective that the application sees.\n * @remarks\n * This can be represented using {@link PriorPerspective} with a refSeq of `Number.MAX_SAFE_INTEGER`, but having an explicit\n * variant of this perspective renders extra refSeq checks unnecessary and is a bit easier to read.\n */\nexport class LocalDefaultPerspective extends PerspectiveBase implements Perspective {\n\tpublic readonly refSeq = Number.MAX_SAFE_INTEGER;\n\n\tpublic constructor(public readonly clientId: number) {\n\t\tsuper();\n\t}\n\n\tpublic hasOccurred(_stamp: OperationStamp): boolean {\n\t\treturn true;\n\t}\n}\n\n/**\n * A perspective dictating whether segments are 'visible' to a remote obliterate operation.\n *\n * NOTE: Beware that partial lengths doesn't support this perspective, in the sense that consulting partial lengths' for the length of a block\n * can give different results than summing the lengths of present segments in that block.\n * This ends up not affecting the current obliterate implementation (which has some special casing in the mapRange calls it uses),\n * but use with caution.\n */\nexport class RemoteObliteratePerspective extends PerspectiveBase implements Perspective {\n\tpublic readonly refSeq = Number.MAX_SAFE_INTEGER;\n\n\tconstructor(public readonly clientId: number) {\n\t\tsuper();\n\t}\n\n\tpublic hasOccurred(stamp: InsertOperationStamp | RemoveOperationStamp): boolean {\n\t\t// Local-only removals are not visible to an obliterate operation, since this means the local removal was concurrent\n\t\t// to a remote obliterate and we may need to mark the segment appropriately to reflect this overlapping remove.\n\t\t// Every other type of operation is visible: obliterates do not affect segments that have already been removed and acked,\n\t\t// and they always affect segments within their range that have not been removed, even if those segments were inserted\n\t\t// after the obliterate's refSeq.\n\t\tif (stamp.type !== \"insert\" && opstampUtils.isLocal(stamp)) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
@@ -0,0 +1,90 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ /**
6
+ * A stamp that identifies provenance of an operation performed on the MergeTree.
7
+ *
8
+ * Stamps identify a point in time (`seq`/`localSeq`) as well as the source (`clientId`) for the operation.
9
+ * This provides enough information to linearize all known applied operations: acked operations happen before
10
+ * local+unacked ones, with acked operations ordered by their sequence numbers and local+unacked operations
11
+ * ordered by their localSeq.
12
+ *
13
+ * By including `clientId`, it also provides enough information to resolve whether segments are visible
14
+ * from alternative perspectives: a remote client will have seen all of its own previous operations as well as
15
+ * those at or below the op's reference sequence number.
16
+ *
17
+ * @remarks - As the `readonly` identifies suggest, these stamps should be treated as immutable.
18
+ * New operations applied to a merge-tree should create new stamps rather than modify existing ones (e.g. when
19
+ * a change's ack happens).
20
+ * @internal
21
+ */
22
+ export interface OperationStamp {
23
+ /**
24
+ * The sequence number at which this operation was applied.
25
+ */
26
+ readonly seq: number;
27
+ /**
28
+ * Short clientId for the client that performed this operation.
29
+ */
30
+ readonly clientId: number;
31
+ /**
32
+ * Local seq at which this operation was applied.
33
+ * This is defined if and only if the operation is pending an ack, i.e. `seq` is UnassignedSequenceNumber.
34
+ *
35
+ * @privateRemarks
36
+ * See {@link CollaborationWindow.localSeq} for more information on the semantics of localSeq.
37
+ */
38
+ readonly localSeq?: number;
39
+ }
40
+ /**
41
+ * {@link OperationStamp} for an 'insert' operation.
42
+ */
43
+ export interface InsertOperationStamp extends OperationStamp {
44
+ readonly type: "insert";
45
+ }
46
+ /**
47
+ * {@link OperationStamp} for a 'set remove' operation. This aligns with the `markRangeRemoved` API in MergeTree.
48
+ *
49
+ * @remarks The terminology here comes from the fact that the removal should affect only the *set* of nodes that were
50
+ * specified at the time the local client issued the remove, and not any nodes that were inserted concurrently.
51
+ *
52
+ * Not using "remove" and "obliterate" here allows us to unambiguously use the term "remove" elsewhere in code to mean
53
+ * "removed from the tree, either by MergeTree.obliterateRange or MergeTree.removeRange". This is convenient as the vast majority
54
+ * of merge-tree code only cares about segment visibility and not the specific operation that caused a segment to be removed.
55
+ */
56
+ export interface SetRemoveOperationStamp extends OperationStamp {
57
+ readonly type: "setRemove";
58
+ }
59
+ /**
60
+ * {@link OperationStamp} for a 'set remove' operation. This aligns with the `obliterateRange` API in MergeTree.
61
+ *
62
+ * @remarks The terminology here comes from the fact that the removal should affect the *slice* of nodes between the
63
+ * start and end point specified by the local client, which includes any nodes that were inserted concurrently.
64
+ *
65
+ * Not using "remove" and "obliterate" here allows us to unambiguously use the term "remove" elsewhere in code to mean
66
+ * "removed from the tree, either by MergeTree.obliterateRange or MergeTree.removeRange". This is convenient as the vast majority
67
+ * of merge-tree code only cares about segment visibility and not the specific operation that caused a segment to be removed.
68
+ */
69
+ export interface SliceRemoveOperationStamp extends OperationStamp {
70
+ readonly type: "sliceRemove";
71
+ }
72
+ export type RemoveOperationStamp = SetRemoveOperationStamp | SliceRemoveOperationStamp;
73
+ export declare function lessThan(a: OperationStamp, b: OperationStamp): boolean;
74
+ export declare function gte(a: OperationStamp, b: OperationStamp): boolean;
75
+ export declare function greaterThan(a: OperationStamp, b: OperationStamp): boolean;
76
+ export declare function lte(a: OperationStamp, b: OperationStamp): boolean;
77
+ export declare function equal(a: OperationStamp, b: OperationStamp): boolean;
78
+ export declare function isLocal(a: OperationStamp): boolean;
79
+ export declare function isAcked(a: OperationStamp): boolean;
80
+ /**
81
+ * Inserts a stamp into a sorted list of stamps in the correct (sorted) position.
82
+ *
83
+ * Beware that this uses Array.splice, thus requires asymptotics considerations.
84
+ * If inserting a variable number of timestamp, consider just pushing them and sorting the list
85
+ * after using {@link compare} instead.
86
+ */
87
+ export declare function spliceIntoList(list: OperationStamp[], stamp: OperationStamp): void;
88
+ export declare function hasAnyAckedOperation(list: OperationStamp[]): boolean;
89
+ export declare function compare(a: OperationStamp, b: OperationStamp): number;
90
+ //# sourceMappingURL=stamps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stamps.d.ts","sourceRoot":"","sources":["../src/stamps.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,cAAc;IAC9B;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,cAAc;IAC3D,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC9D,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,yBAA0B,SAAQ,cAAc;IAChE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC7B;AAED,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,GAAG,yBAAyB,CAAC;AAEvF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAWtE;AAED,wBAAgB,GAAG,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAEjE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAWzE;AAED,wBAAgB,GAAG,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAEjE;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,OAAO,CAEnE;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAElD;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAclF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,OAAO,CAEpE;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,GAAG,MAAM,CAQpE"}