@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
@@ -3,70 +3,87 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { UnassignedSequenceNumber } from "./constants.js";
6
+ import { LocalClientId, UnassignedSequenceNumber } from "./constants.js";
7
7
  import { type MergeTree } from "./mergeTree.js";
8
8
  import { LeafAction, backwardExcursion, forwardExcursion } from "./mergeTreeNodeWalk.js";
9
9
  import { seqLTE, type ISegmentLeaf } from "./mergeTreeNodes.js";
10
- import {
11
- isInserted,
12
- isMoved,
13
- isRemoved,
14
- type IInsertionInfo,
15
- type IMoveInfo,
16
- type IRemovalInfo,
17
- type SegmentWithInfo,
18
- } from "./segmentInfos.js";
10
+ import { isInserted, toMoveInfo, toRemovalInfo } from "./segmentInfos.js";
11
+ import * as opstampUtils from "./stamps.js";
12
+ import type { OperationStamp, InsertOperationStamp, RemoveOperationStamp } from "./stamps.js";
19
13
 
20
14
  /**
21
- * Provides a view of a MergeTree from the perspective of a specific client at a specific sequence number.
15
+ * A perspective which includes some subset of operations known to the local client.
16
+ *
17
+ * This helps the local client reason about the state of other clients when they issued an operation.
22
18
  */
23
19
  export interface Perspective {
24
- nextSegment(segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;
25
- previousSegment(segment: ISegmentLeaf): ISegmentLeaf;
26
- }
20
+ /**
21
+ * The sequence number last seen from this perspective. Same concept as `ISequencedDocumentMessage.referenceSequenceNumber`.
22
+ * @privateRemarks
23
+ * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends
24
+ * on the (refSeq, clientId, localSeq?) representation of perspectives.
25
+ */
26
+ readonly refSeq: number;
27
27
 
28
- /**
29
- * Represents a point in time inside the collaboration window.
30
- */
31
- export interface SeqTime {
32
- refSeq: number;
33
- localSeq?: number;
34
- }
28
+ /**
29
+ * The client id for this perspective.
30
+ * @privateRemarks
31
+ * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends
32
+ * on the (refSeq, clientId, localSeq?) representation of perspectives.
33
+ */
34
+ readonly clientId: number;
35
35
 
36
- /**
37
- * Implementation of {@link Perspective}.
38
- * @privateRemarks
39
- * TODO:AB#29765: This class does not support non-local-client perspectives, but should.
40
- */
41
- export class PerspectiveImpl implements Perspective {
42
36
  /**
43
- * @param _mergeTree - The {@link MergeTree} to view.
44
- * @param _seqTime - The latest sequence number and local sequence number to consider.
37
+ * When this is a local perspective, the local sequence number last seen from this perspective.
38
+ *
39
+ * Perspectives with defined `localSeq` values are useful in reconnection flows, where the local client may need to resend some
40
+ * of its ops after rederiving their new equivalents.
41
+ * @privateRemarks
42
+ * This currently allows inter-operation between MergeTree methods and the partial lengths implementation, which still depends
43
+ * on the (refSeq, clientId, localSeq?) representation of perspectives.
45
44
  */
46
- public constructor(
47
- private readonly _mergeTree: MergeTree,
48
- private readonly _seqTime: SeqTime,
49
- ) {}
45
+ readonly localSeq?: number;
46
+
47
+ /**
48
+ * @returns Whether the segment is present (visible) from this perspective
49
+ */
50
+ isSegmentPresent(segment: ISegmentLeaf): boolean;
51
+
52
+ /**
53
+ * @returns Whether this perspective has seen the given operation.
54
+ */
55
+ hasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;
56
+
57
+ nextSegment(mergeTree: MergeTree, segment: ISegmentLeaf, forward?: boolean): ISegmentLeaf;
58
+ previousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf;
59
+ }
60
+
61
+ abstract class PerspectiveBase {
62
+ abstract hasOccurred(stamp: RemoveOperationStamp | InsertOperationStamp): boolean;
50
63
 
51
64
  /**
52
65
  * Returns the immediately adjacent segment in the specified direction from this perspective.
53
66
  * There may actually be multiple segments between the given segment and the returned segment,
54
- * but they were either inserted after this perspective, or have been removed or moved before this perspective.
67
+ * but they were either inserted after this perspective, or have been removed before this perspective.
55
68
  *
56
69
  * @param segment - The segment to start from.
57
70
  * @param forward - The direction to search.
58
71
  * @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
59
72
  */
60
- public nextSegment(segment: ISegmentLeaf, forward: boolean = true): ISegmentLeaf {
73
+ public nextSegment(
74
+ mergeTree: MergeTree,
75
+ segment: ISegmentLeaf,
76
+ forward: boolean = true,
77
+ ): ISegmentLeaf {
61
78
  let next: ISegmentLeaf | undefined;
62
79
  const action = (seg: ISegmentLeaf): boolean | undefined => {
63
- if (isSegmentPresent(seg, this._seqTime)) {
80
+ if (this.isSegmentPresent(seg)) {
64
81
  next = seg;
65
82
  return LeafAction.Exit;
66
83
  }
67
84
  };
68
85
  (forward ? forwardExcursion : backwardExcursion)(segment, action);
69
- return next ?? (forward ? this._mergeTree.endOfTree : this._mergeTree.startOfTree);
86
+ return next ?? (forward ? mergeTree.endOfTree : mergeTree.startOfTree);
70
87
  }
71
88
 
72
89
  /**
@@ -75,99 +92,158 @@ export class PerspectiveImpl implements Perspective {
75
92
  * @returns the previous segment, or the start of the tree if there is no previous segment.
76
93
  * @remarks This is a convenient equivalent to calling `nextSegment(segment, false)`.
77
94
  */
78
- public previousSegment(segment: ISegmentLeaf): ISegmentLeaf {
79
- return this.nextSegment(segment, false);
95
+ public previousSegment(mergeTree: MergeTree, segment: ISegmentLeaf): ISegmentLeaf {
96
+ return this.nextSegment(mergeTree, segment, false);
97
+ }
98
+
99
+ public isSegmentPresent(seg: ISegmentLeaf): boolean {
100
+ const insert: InsertOperationStamp = {
101
+ type: "insert",
102
+ clientId: seg.clientId,
103
+ seq: seg.seq,
104
+ localSeq: seg.localSeq,
105
+ };
106
+ if (isInserted(seg) && !this.hasOccurred(insert)) {
107
+ return false;
108
+ }
109
+
110
+ const removes: RemoveOperationStamp[] = [];
111
+ const removalInfo = toRemovalInfo(seg);
112
+ if (removalInfo !== undefined) {
113
+ removes.push(
114
+ ...removalInfo.removedClientIds.map((clientId) =>
115
+ (clientId === LocalClientId || clientId === 0) &&
116
+ removalInfo.localRemovedSeq !== undefined
117
+ ? ({
118
+ type: "setRemove",
119
+ seq: UnassignedSequenceNumber,
120
+ clientId,
121
+ localSeq: removalInfo.localRemovedSeq,
122
+ } as const)
123
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
124
+ ({ type: "setRemove", seq: removalInfo.removedSeq, clientId } as const),
125
+ ),
126
+ );
127
+ }
128
+
129
+ const moveInfo = toMoveInfo(seg);
130
+ if (moveInfo !== undefined) {
131
+ removes.push(
132
+ ...moveInfo.movedClientIds.map((clientId, index) =>
133
+ (clientId === LocalClientId || clientId === 0) &&
134
+ moveInfo.localMovedSeq !== undefined
135
+ ? ({
136
+ type: "sliceRemove",
137
+ seq: UnassignedSequenceNumber,
138
+ clientId,
139
+ localSeq: moveInfo.localMovedSeq,
140
+ } as const)
141
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
142
+ ({ type: "setRemove", seq: moveInfo.movedSeqs[index]!, clientId } as const),
143
+ ),
144
+ );
145
+ }
146
+
147
+ if (removes.some((remove) => this.hasOccurred(remove))) {
148
+ return false;
149
+ }
150
+
151
+ return true;
80
152
  }
81
153
  }
82
154
 
83
155
  /**
84
- * Determines if the given segment was removed before the given perspective.
85
- * @param seg - The segment to check.
86
- * @param seq - The latest sequence number to consider.
87
- * @param localSeq - The latest local sequence number to consider.
88
- * @returns true iff this segment was removed in the given perspective.
89
- * @privateRemarks
90
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
156
+ * A perspective which includes edits at or before some reference sequence number alongside all edits from some particular client.
157
+ *
158
+ * @remarks
159
+ * This works for both the local client as well as remote clients since refSeq-based checks disallow unacked edits, but the clientId check
160
+ * catches unacked edits from the local client.
91
161
  */
92
- export function wasRemovedBefore(
93
- seg: SegmentWithInfo<IInsertionInfo & IRemovalInfo>,
94
- { refSeq, localSeq }: SeqTime,
95
- ): boolean {
96
- if (
97
- seg.removedSeq === UnassignedSequenceNumber &&
98
- localSeq !== undefined &&
99
- seg.localRemovedSeq !== undefined
162
+ export class PriorPerspective extends PerspectiveBase implements Perspective {
163
+ public constructor(
164
+ public readonly refSeq: number,
165
+ public readonly clientId: number,
100
166
  ) {
101
- return seg.localRemovedSeq <= localSeq;
167
+ super();
168
+ }
169
+
170
+ public hasOccurred(stamp: OperationStamp): boolean {
171
+ const predatesViaRefSeq = seqLTE(stamp.seq, this.refSeq);
172
+ const predatesViaSameClient = stamp.clientId === this.clientId;
173
+ return predatesViaRefSeq || predatesViaSameClient;
102
174
  }
103
- return seg.removedSeq !== undefined && seqLTE(seg.removedSeq, refSeq);
104
175
  }
105
176
 
106
177
  /**
107
- * Determines if the given segment was moved before the given perspective.
108
- * @param seg - The segment to check.
109
- * @param refSeq - The latest sequence number to consider.
110
- * @param localSeq - The latest local sequence number to consider.
111
- * @returns true iff this segment was moved (aka obliterated) in the given perspective.
112
- * @privateRemarks
113
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
178
+ * A perspective which includes edits which were either:
179
+ * - acked and at or before some reference sequence number
180
+ * - unacked, but at or before some local sequence number
181
+ *
182
+ * This is a useful perspective when the local client is in the process of reconnecting, since it must
183
+ * rederive positions for unacked ops while only considering a portion of its own edits as having been applied.
114
184
  */
115
- export function wasMovedBefore(
116
- seg: SegmentWithInfo<IInsertionInfo & IMoveInfo>,
117
- { refSeq, localSeq }: SeqTime,
118
- ): boolean {
119
- if (
120
- seg.movedSeq === UnassignedSequenceNumber &&
121
- localSeq !== undefined &&
122
- seg.localMovedSeq !== undefined
185
+ export class LocalReconnectingPerspective extends PerspectiveBase implements Perspective {
186
+ public constructor(
187
+ public readonly refSeq: number,
188
+ public readonly clientId: number,
189
+ public readonly localSeq: number,
123
190
  ) {
124
- return seg.localMovedSeq <= localSeq;
191
+ super();
192
+ }
193
+
194
+ public hasOccurred(stamp: OperationStamp): boolean {
195
+ const predatesViaRefSeq = seqLTE(stamp.seq, this.refSeq);
196
+ const predatesViaLocalSeq =
197
+ stamp.localSeq !== undefined && stamp.localSeq <= this.localSeq;
198
+ return predatesViaRefSeq || predatesViaLocalSeq;
125
199
  }
126
- return seg.movedSeq !== undefined && seqLTE(seg.movedSeq, refSeq);
127
200
  }
128
201
 
129
202
  /**
130
- * See {@link wasRemovedBefore} and {@link wasMovedBefore}.
131
- * @privateRemarks
132
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
203
+ * A perspective which includes all known edits.
204
+ *
205
+ * This is the perspective that the application sees.
206
+ * @remarks
207
+ * This can be represented using {@link PriorPerspective} with a refSeq of `Number.MAX_SAFE_INTEGER`, but having an explicit
208
+ * variant of this perspective renders extra refSeq checks unnecessary and is a bit easier to read.
133
209
  */
134
- export function wasRemovedOrMovedBefore(seg: ISegmentLeaf, seqTime: SeqTime): boolean {
135
- return (
136
- isInserted(seg) &&
137
- ((isRemoved(seg) && wasRemovedBefore(seg, seqTime)) ||
138
- (isMoved(seg) && wasMovedBefore(seg, seqTime)))
139
- );
210
+ export class LocalDefaultPerspective extends PerspectiveBase implements Perspective {
211
+ public readonly refSeq = Number.MAX_SAFE_INTEGER;
212
+
213
+ public constructor(public readonly clientId: number) {
214
+ super();
215
+ }
216
+
217
+ public hasOccurred(_stamp: OperationStamp): boolean {
218
+ return true;
219
+ }
140
220
  }
141
221
 
142
222
  /**
143
- * Determines if the given segment is present in the given perspective.
144
- * @param seg - The segment to check.
145
- * @param seqTime - The latest sequence number and local sequence number to consider.
146
- * @returns true iff this segment was inserted before the given perspective,
147
- * and it was not removed or moved in the given perspective.
148
- * @privateRemarks
149
- * TODO:AB#29765: This function does not support non-local-client perspectives, but should.
223
+ * A perspective dictating whether segments are 'visible' to a remote obliterate operation.
224
+ *
225
+ * NOTE: Beware that partial lengths doesn't support this perspective, in the sense that consulting partial lengths' for the length of a block
226
+ * can give different results than summing the lengths of present segments in that block.
227
+ * This ends up not affecting the current obliterate implementation (which has some special casing in the mapRange calls it uses),
228
+ * but use with caution.
150
229
  */
151
- export function isSegmentPresent(seg: ISegmentLeaf, seqTime: SeqTime): boolean {
152
- const { refSeq, localSeq } = seqTime;
153
- // If seg.seq is undefined, then this segment has existed since minSeq.
154
- // It may have been moved or removed since.
155
- if (isInserted(seg)) {
156
- if (seg.seq !== UnassignedSequenceNumber) {
157
- if (!seqLTE(seg.seq, refSeq)) {
158
- return false;
159
- }
160
- } else if (
161
- seg.localSeq !== undefined && // seg.seq === UnassignedSequenceNumber
162
- // If the current perspective does not include local sequence numbers,
163
- // then this segment does not exist yet.
164
- (localSeq === undefined || seg.localSeq > localSeq)
165
- ) {
230
+ export class RemoteObliteratePerspective extends PerspectiveBase implements Perspective {
231
+ public readonly refSeq = Number.MAX_SAFE_INTEGER;
232
+
233
+ constructor(public readonly clientId: number) {
234
+ super();
235
+ }
236
+
237
+ public hasOccurred(stamp: InsertOperationStamp | RemoveOperationStamp): boolean {
238
+ // Local-only removals are not visible to an obliterate operation, since this means the local removal was concurrent
239
+ // to a remote obliterate and we may need to mark the segment appropriately to reflect this overlapping remove.
240
+ // Every other type of operation is visible: obliterates do not affect segments that have already been removed and acked,
241
+ // and they always affect segments within their range that have not been removed, even if those segments were inserted
242
+ // after the obliterate's refSeq.
243
+ if (stamp.type !== "insert" && opstampUtils.isLocal(stamp)) {
166
244
  return false;
167
245
  }
246
+
247
+ return true;
168
248
  }
169
- if (wasRemovedOrMovedBefore(seg, seqTime)) {
170
- return false;
171
- }
172
- return true;
173
249
  }
package/src/stamps.ts ADDED
@@ -0,0 +1,164 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { UnassignedSequenceNumber } from "./constants.js";
7
+
8
+ /**
9
+ * A stamp that identifies provenance of an operation performed on the MergeTree.
10
+ *
11
+ * Stamps identify a point in time (`seq`/`localSeq`) as well as the source (`clientId`) for the operation.
12
+ * This provides enough information to linearize all known applied operations: acked operations happen before
13
+ * local+unacked ones, with acked operations ordered by their sequence numbers and local+unacked operations
14
+ * ordered by their localSeq.
15
+ *
16
+ * By including `clientId`, it also provides enough information to resolve whether segments are visible
17
+ * from alternative perspectives: a remote client will have seen all of its own previous operations as well as
18
+ * those at or below the op's reference sequence number.
19
+ *
20
+ * @remarks - As the `readonly` identifies suggest, these stamps should be treated as immutable.
21
+ * New operations applied to a merge-tree should create new stamps rather than modify existing ones (e.g. when
22
+ * a change's ack happens).
23
+ * @internal
24
+ */
25
+ export interface OperationStamp {
26
+ /**
27
+ * The sequence number at which this operation was applied.
28
+ */
29
+ readonly seq: number;
30
+
31
+ /**
32
+ * Short clientId for the client that performed this operation.
33
+ */
34
+ readonly clientId: number;
35
+
36
+ /**
37
+ * Local seq at which this operation was applied.
38
+ * This is defined if and only if the operation is pending an ack, i.e. `seq` is UnassignedSequenceNumber.
39
+ *
40
+ * @privateRemarks
41
+ * See {@link CollaborationWindow.localSeq} for more information on the semantics of localSeq.
42
+ */
43
+ readonly localSeq?: number;
44
+ }
45
+
46
+ /**
47
+ * {@link OperationStamp} for an 'insert' operation.
48
+ */
49
+ export interface InsertOperationStamp extends OperationStamp {
50
+ readonly type: "insert";
51
+ }
52
+
53
+ /**
54
+ * {@link OperationStamp} for a 'set remove' operation. This aligns with the `markRangeRemoved` API in MergeTree.
55
+ *
56
+ * @remarks The terminology here comes from the fact that the removal should affect only the *set* of nodes that were
57
+ * specified at the time the local client issued the remove, and not any nodes that were inserted concurrently.
58
+ *
59
+ * Not using "remove" and "obliterate" here allows us to unambiguously use the term "remove" elsewhere in code to mean
60
+ * "removed from the tree, either by MergeTree.obliterateRange or MergeTree.removeRange". This is convenient as the vast majority
61
+ * of merge-tree code only cares about segment visibility and not the specific operation that caused a segment to be removed.
62
+ */
63
+ export interface SetRemoveOperationStamp extends OperationStamp {
64
+ readonly type: "setRemove";
65
+ }
66
+
67
+ /**
68
+ * {@link OperationStamp} for a 'set remove' operation. This aligns with the `obliterateRange` API in MergeTree.
69
+ *
70
+ * @remarks The terminology here comes from the fact that the removal should affect the *slice* of nodes between the
71
+ * start and end point specified by the local client, which includes any nodes that were inserted concurrently.
72
+ *
73
+ * Not using "remove" and "obliterate" here allows us to unambiguously use the term "remove" elsewhere in code to mean
74
+ * "removed from the tree, either by MergeTree.obliterateRange or MergeTree.removeRange". This is convenient as the vast majority
75
+ * of merge-tree code only cares about segment visibility and not the specific operation that caused a segment to be removed.
76
+ */
77
+ export interface SliceRemoveOperationStamp extends OperationStamp {
78
+ readonly type: "sliceRemove";
79
+ }
80
+
81
+ export type RemoveOperationStamp = SetRemoveOperationStamp | SliceRemoveOperationStamp;
82
+
83
+ export function lessThan(a: OperationStamp, b: OperationStamp): boolean {
84
+ if (a.seq === UnassignedSequenceNumber) {
85
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
86
+ return b.seq === UnassignedSequenceNumber && a.localSeq! < b.localSeq!;
87
+ }
88
+
89
+ if (b.seq === UnassignedSequenceNumber) {
90
+ return true;
91
+ }
92
+
93
+ return a.seq < b.seq;
94
+ }
95
+
96
+ export function gte(a: OperationStamp, b: OperationStamp): boolean {
97
+ return !lessThan(a, b);
98
+ }
99
+
100
+ export function greaterThan(a: OperationStamp, b: OperationStamp): boolean {
101
+ if (a.seq === UnassignedSequenceNumber) {
102
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103
+ return b.seq !== UnassignedSequenceNumber || a.localSeq! > b.localSeq!;
104
+ }
105
+
106
+ if (b.seq === UnassignedSequenceNumber) {
107
+ return false;
108
+ }
109
+
110
+ return a.seq > b.seq;
111
+ }
112
+
113
+ export function lte(a: OperationStamp, b: OperationStamp): boolean {
114
+ return !greaterThan(a, b);
115
+ }
116
+
117
+ export function equal(a: OperationStamp, b: OperationStamp): boolean {
118
+ return a.seq === b.seq && a.clientId === b.clientId && a.localSeq === b.localSeq;
119
+ }
120
+
121
+ export function isLocal(a: OperationStamp): boolean {
122
+ return a.seq === UnassignedSequenceNumber;
123
+ }
124
+
125
+ export function isAcked(a: OperationStamp): boolean {
126
+ return a.seq !== UnassignedSequenceNumber;
127
+ }
128
+
129
+ /**
130
+ * Inserts a stamp into a sorted list of stamps in the correct (sorted) position.
131
+ *
132
+ * Beware that this uses Array.splice, thus requires asymptotics considerations.
133
+ * If inserting a variable number of timestamp, consider just pushing them and sorting the list
134
+ * after using {@link compare} instead.
135
+ */
136
+ export function spliceIntoList(list: OperationStamp[], stamp: OperationStamp): void {
137
+ if (isLocal(stamp) || list.length === 0) {
138
+ list.push(stamp);
139
+ } else {
140
+ for (let i = list.length - 1; i >= 0; i--) {
141
+ if (greaterThan(stamp, list[i])) {
142
+ list.splice(i + 1, 0, stamp);
143
+ return;
144
+ }
145
+ }
146
+
147
+ // Less than all stamps in the list: put it at the beginning.
148
+ list.unshift(stamp);
149
+ }
150
+ }
151
+
152
+ export function hasAnyAckedOperation(list: OperationStamp[]): boolean {
153
+ return list.some((ts) => isAcked(ts));
154
+ }
155
+
156
+ export function compare(a: OperationStamp, b: OperationStamp): number {
157
+ if (greaterThan(a, b)) {
158
+ return 1;
159
+ } else if (lessThan(a, b)) {
160
+ return -1;
161
+ } else {
162
+ return 0;
163
+ }
164
+ }