@fluidframework/sequence 2.0.0-internal.3.0.2 → 2.0.0-internal.3.2.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 (94) hide show
  1. package/.eslintrc.js +9 -12
  2. package/.mocharc.js +2 -2
  3. package/.vscode/launch.json +15 -14
  4. package/README.md +188 -179
  5. package/api-extractor.json +2 -2
  6. package/dist/defaultMap.d.ts.map +1 -1
  7. package/dist/defaultMap.js +5 -4
  8. package/dist/defaultMap.js.map +1 -1
  9. package/dist/defaultMapInterfaces.d.ts.map +1 -1
  10. package/dist/defaultMapInterfaces.js.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/intervalCollection.d.ts.map +1 -1
  15. package/dist/intervalCollection.js +50 -36
  16. package/dist/intervalCollection.js.map +1 -1
  17. package/dist/intervalTree.d.ts.map +1 -1
  18. package/dist/intervalTree.js.map +1 -1
  19. package/dist/localValues.d.ts.map +1 -1
  20. package/dist/localValues.js.map +1 -1
  21. package/dist/packageVersion.d.ts +1 -1
  22. package/dist/packageVersion.js +1 -1
  23. package/dist/packageVersion.js.map +1 -1
  24. package/dist/sequence.d.ts +1 -1
  25. package/dist/sequence.d.ts.map +1 -1
  26. package/dist/sequence.js +13 -17
  27. package/dist/sequence.js.map +1 -1
  28. package/dist/sequenceDeltaEvent.d.ts.map +1 -1
  29. package/dist/sequenceDeltaEvent.js.map +1 -1
  30. package/dist/sequenceFactory.d.ts.map +1 -1
  31. package/dist/sequenceFactory.js.map +1 -1
  32. package/dist/sharedIntervalCollection.d.ts.map +1 -1
  33. package/dist/sharedIntervalCollection.js.map +1 -1
  34. package/dist/sharedSequence.d.ts.map +1 -1
  35. package/dist/sharedSequence.js +3 -3
  36. package/dist/sharedSequence.js.map +1 -1
  37. package/dist/sharedString.d.ts.map +1 -1
  38. package/dist/sharedString.js +5 -4
  39. package/dist/sharedString.js.map +1 -1
  40. package/lib/defaultMap.d.ts.map +1 -1
  41. package/lib/defaultMap.js +6 -5
  42. package/lib/defaultMap.js.map +1 -1
  43. package/lib/defaultMapInterfaces.d.ts.map +1 -1
  44. package/lib/defaultMapInterfaces.js.map +1 -1
  45. package/lib/index.d.ts +2 -2
  46. package/lib/index.d.ts.map +1 -1
  47. package/lib/index.js +2 -2
  48. package/lib/index.js.map +1 -1
  49. package/lib/intervalCollection.d.ts.map +1 -1
  50. package/lib/intervalCollection.js +50 -36
  51. package/lib/intervalCollection.js.map +1 -1
  52. package/lib/intervalTree.d.ts.map +1 -1
  53. package/lib/intervalTree.js.map +1 -1
  54. package/lib/localValues.d.ts.map +1 -1
  55. package/lib/localValues.js +1 -1
  56. package/lib/localValues.js.map +1 -1
  57. package/lib/packageVersion.d.ts +1 -1
  58. package/lib/packageVersion.js +1 -1
  59. package/lib/packageVersion.js.map +1 -1
  60. package/lib/sequence.d.ts +1 -1
  61. package/lib/sequence.d.ts.map +1 -1
  62. package/lib/sequence.js +15 -19
  63. package/lib/sequence.js.map +1 -1
  64. package/lib/sequenceDeltaEvent.d.ts.map +1 -1
  65. package/lib/sequenceDeltaEvent.js.map +1 -1
  66. package/lib/sequenceFactory.d.ts.map +1 -1
  67. package/lib/sequenceFactory.js +1 -1
  68. package/lib/sequenceFactory.js.map +1 -1
  69. package/lib/sharedIntervalCollection.d.ts.map +1 -1
  70. package/lib/sharedIntervalCollection.js.map +1 -1
  71. package/lib/sharedSequence.d.ts.map +1 -1
  72. package/lib/sharedSequence.js +4 -4
  73. package/lib/sharedSequence.js.map +1 -1
  74. package/lib/sharedString.d.ts.map +1 -1
  75. package/lib/sharedString.js +5 -4
  76. package/lib/sharedString.js.map +1 -1
  77. package/package.json +55 -55
  78. package/prettier.config.cjs +1 -1
  79. package/src/defaultMap.ts +406 -405
  80. package/src/defaultMapInterfaces.ts +120 -115
  81. package/src/index.ts +27 -17
  82. package/src/intervalCollection.ts +2198 -1997
  83. package/src/intervalTree.ts +139 -139
  84. package/src/localValues.ts +64 -73
  85. package/src/packageVersion.ts +1 -1
  86. package/src/sequence.ts +739 -694
  87. package/src/sequenceDeltaEvent.ts +143 -137
  88. package/src/sequenceFactory.ts +48 -46
  89. package/src/sharedIntervalCollection.ts +150 -136
  90. package/src/sharedSequence.ts +165 -160
  91. package/src/sharedString.ts +385 -343
  92. package/tsconfig.esnext.json +6 -6
  93. package/tsconfig.json +8 -12
  94. package/.editorconfig +0 -7
@@ -9,59 +9,59 @@ import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
9
9
  import { IEvent } from "@fluidframework/common-definitions";
10
10
  import { UsageError } from "@fluidframework/container-utils";
11
11
  import {
12
- addProperties,
13
- Client,
14
- compareReferencePositions,
15
- ConflictAction,
16
- createMap,
17
- ICombiningOp,
18
- ISegment,
19
- MergeTreeDeltaType,
20
- minReferencePosition,
21
- PropertiesManager,
22
- PropertySet,
23
- RedBlackTree,
24
- LocalReferencePosition,
25
- ReferenceType,
26
- refTypeIncludesFlag,
27
- reservedRangeLabelsKey,
28
- UnassignedSequenceNumber,
29
- maxReferencePosition,
30
- createDetachedLocalReferencePosition,
31
- DetachedReferencePosition,
12
+ addProperties,
13
+ Client,
14
+ compareReferencePositions,
15
+ ConflictAction,
16
+ createMap,
17
+ ICombiningOp,
18
+ ISegment,
19
+ MergeTreeDeltaType,
20
+ minReferencePosition,
21
+ PropertiesManager,
22
+ PropertySet,
23
+ RedBlackTree,
24
+ LocalReferencePosition,
25
+ ReferenceType,
26
+ refTypeIncludesFlag,
27
+ reservedRangeLabelsKey,
28
+ UnassignedSequenceNumber,
29
+ maxReferencePosition,
30
+ createDetachedLocalReferencePosition,
31
+ DetachedReferencePosition,
32
32
  } from "@fluidframework/merge-tree";
33
33
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
34
34
  import { LoggingError } from "@fluidframework/telemetry-utils";
35
35
  import { v4 as uuid } from "uuid";
36
36
  import {
37
- IMapMessageLocalMetadata,
38
- IValueFactory,
39
- IValueOpEmitter,
40
- IValueOperation,
41
- IValueType,
42
- IValueTypeOperationValue,
37
+ IMapMessageLocalMetadata,
38
+ IValueFactory,
39
+ IValueOpEmitter,
40
+ IValueOperation,
41
+ IValueType,
42
+ IValueTypeOperationValue,
43
43
  } from "./defaultMapInterfaces";
44
44
  import { IInterval, IntervalConflictResolver, IntervalTree, IntervalNode } from "./intervalTree";
45
45
 
46
46
  const reservedIntervalIdKey = "intervalId";
47
47
 
48
48
  export enum IntervalType {
49
- Simple = 0x0,
50
- Nest = 0x1,
51
-
52
- /**
53
- * SlideOnRemove indicates that the ends of the interval will slide if the segment
54
- * they reference is removed and acked.
55
- * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md` for details
56
- * SlideOnRemove is the default interval behavior and does not need to be specified.
57
- */
58
- SlideOnRemove = 0x2, // SlideOnRemove is default behavior - all intervals are SlideOnRemove
59
-
60
- /**
61
- * A temporary interval, used internally
62
- * @internal
63
- */
64
- Transient = 0x4,
49
+ Simple = 0x0,
50
+ Nest = 0x1,
51
+
52
+ /**
53
+ * SlideOnRemove indicates that the ends of the interval will slide if the segment
54
+ * they reference is removed and acked.
55
+ * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md` for details
56
+ * SlideOnRemove is the default interval behavior and does not need to be specified.
57
+ */
58
+ SlideOnRemove = 0x2, // SlideOnRemove is default behavior - all intervals are SlideOnRemove
59
+
60
+ /**
61
+ * A temporary interval, used internally
62
+ * @internal
63
+ */
64
+ Transient = 0x4,
65
65
  }
66
66
 
67
67
  /**
@@ -70,22 +70,22 @@ export enum IntervalType {
70
70
  * @internal
71
71
  */
72
72
  export interface ISerializedInterval {
73
- /**
74
- * Sequence number at which `start` and `end` should be interpreted
75
- *
76
- * @remarks - It's unclear that this is necessary to store here.
77
- * This should just be the refSeq on the op that modified the interval, which should be available via other means.
78
- * At the time of writing, it's not plumbed through to the reconnect/rebase code, however, which does need it.
79
- */
80
- sequenceNumber: number;
81
- /** Start position of the interval (inclusive) */
82
- start: number;
83
- /** End position of the interval (inclusive) */
84
- end: number;
85
- /** Interval type to create */
86
- intervalType: IntervalType;
87
- /** Any properties the interval has */
88
- properties?: PropertySet;
73
+ /**
74
+ * Sequence number at which `start` and `end` should be interpreted
75
+ *
76
+ * @remarks - It's unclear that this is necessary to store here.
77
+ * This should just be the refSeq on the op that modified the interval, which should be available via other means.
78
+ * At the time of writing, it's not plumbed through to the reconnect/rebase code, however, which does need it.
79
+ */
80
+ sequenceNumber: number;
81
+ /** Start position of the interval (inclusive) */
82
+ start: number;
83
+ /** End position of the interval (inclusive) */
84
+ end: number;
85
+ /** Interval type to create */
86
+ intervalType: IntervalType;
87
+ /** Any properties the interval has */
88
+ properties?: PropertySet;
89
89
  }
90
90
 
91
91
  /**
@@ -93,9 +93,8 @@ export interface ISerializedInterval {
93
93
  * Changes can modify any of start/end/properties, with `undefined` signifying no change should be made.
94
94
  * @internal
95
95
  */
96
- export type SerializedIntervalDelta =
97
- Omit<ISerializedInterval, "start" | "end" | "properties">
98
- & Partial<Pick<ISerializedInterval, "start" | "end" | "properties">>;
96
+ export type SerializedIntervalDelta = Omit<ISerializedInterval, "start" | "end" | "properties"> &
97
+ Partial<Pick<ISerializedInterval, "start" | "end" | "properties">>;
99
98
 
100
99
  /**
101
100
  * A size optimization to avoid redundantly storing keys when serializing intervals
@@ -111,23 +110,26 @@ export type CompressedSerializedInterval = [number, number, number, IntervalType
111
110
  * @internal
112
111
  */
113
112
  export interface ISerializedIntervalCollectionV2 {
114
- label: string;
115
- version: 2;
116
- intervals: CompressedSerializedInterval[];
113
+ label: string;
114
+ version: 2;
115
+ intervals: CompressedSerializedInterval[];
117
116
  }
118
117
 
119
118
  /**
120
119
  * Decompress an interval after loading a summary from JSON. The exact format
121
120
  * of this compression is unspecified and subject to change
122
121
  */
123
- function decompressInterval(interval: CompressedSerializedInterval, label?: string): ISerializedInterval {
124
- return {
125
- start: interval[0],
126
- end: interval[1],
127
- sequenceNumber: interval[2],
128
- intervalType: interval[3],
129
- properties: { ...interval[4], [reservedRangeLabelsKey]: [label] },
130
- };
122
+ function decompressInterval(
123
+ interval: CompressedSerializedInterval,
124
+ label?: string,
125
+ ): ISerializedInterval {
126
+ return {
127
+ start: interval[0],
128
+ end: interval[1],
129
+ sequenceNumber: interval[2],
130
+ intervalType: interval[3],
131
+ properties: { ...interval[4], [reservedRangeLabelsKey]: [label] },
132
+ };
131
133
  }
132
134
 
133
135
  /**
@@ -135,248 +137,259 @@ function decompressInterval(interval: CompressedSerializedInterval, label?: stri
135
137
  * compression is unspecified and subject to change
136
138
  */
137
139
  function compressInterval(interval: ISerializedInterval): CompressedSerializedInterval {
138
- const { start, end, sequenceNumber, intervalType, properties } = interval;
139
-
140
- return [
141
- start,
142
- end,
143
- sequenceNumber,
144
- intervalType,
145
- // remove the `referenceRangeLabels` property as it is already stored
146
- // in the `label` field of the summary
147
- { ...properties, [reservedRangeLabelsKey]: undefined },
148
- ];
140
+ const { start, end, sequenceNumber, intervalType, properties } = interval;
141
+
142
+ return [
143
+ start,
144
+ end,
145
+ sequenceNumber,
146
+ intervalType,
147
+ // remove the `referenceRangeLabels` property as it is already stored
148
+ // in the `label` field of the summary
149
+ { ...properties, [reservedRangeLabelsKey]: undefined },
150
+ ];
149
151
  }
150
152
 
151
153
  export interface ISerializableInterval extends IInterval {
152
- /** Serializable bag of properties associated with the interval. */
153
- properties: PropertySet;
154
- /** @internal */
155
- propertyManager: PropertiesManager;
156
- /** @internal */
157
- serialize(): ISerializedInterval;
158
- /** @internal */
159
- addProperties(props: PropertySet, collaborating?: boolean, seq?: number):
160
- PropertySet | undefined;
161
- /**
162
- * Gets the id associated with this interval.
163
- * When the interval is used as part of an interval collection, this id can be used to modify or remove the
164
- * interval.
165
- * @remarks - This signature includes `undefined` strictly for backwards-compatibility reasons, as older versions
166
- * of Fluid didn't always write interval ids.
167
- */
168
- getIntervalId(): string | undefined;
154
+ /** Serializable bag of properties associated with the interval. */
155
+ properties: PropertySet;
156
+ /** @internal */
157
+ propertyManager: PropertiesManager;
158
+ /** @internal */
159
+ serialize(): ISerializedInterval;
160
+ /** @internal */
161
+ addProperties(
162
+ props: PropertySet,
163
+ collaborating?: boolean,
164
+ seq?: number,
165
+ ): PropertySet | undefined;
166
+ /**
167
+ * Gets the id associated with this interval.
168
+ * When the interval is used as part of an interval collection, this id can be used to modify or remove the
169
+ * interval.
170
+ * @remarks - This signature includes `undefined` strictly for backwards-compatibility reasons, as older versions
171
+ * of Fluid didn't always write interval ids.
172
+ */
173
+ getIntervalId(): string | undefined;
169
174
  }
170
175
 
171
176
  export interface IIntervalHelpers<TInterval extends ISerializableInterval> {
172
- compareEnds(a: TInterval, b: TInterval): number;
173
- /**
174
- *
175
- * @param label - label of the interval collection this interval is being added to. This parameter is
176
- * irrelevant for transient intervals.
177
- * @param start - numerical start position of the interval
178
- * @param end - numberical end position of the interval
179
- * @param client - client creating the interval
180
- * @param intervalType - Type of interval to create. Default is SlideOnRemove
181
- * @param op - If this create came from a remote client, op that created it. Default is undefined (i.e. local)
182
- * @param fromSnapshot - If this create came from loading a snapshot. Default is false.
183
- */
184
- create(
185
- label: string,
186
- start: number | undefined,
187
- end: number | undefined,
188
- client: Client | undefined,
189
- intervalType: IntervalType,
190
- op?: ISequencedDocumentMessage,
191
- fromSnapshot?: boolean,
192
- ): TInterval;
177
+ compareEnds(a: TInterval, b: TInterval): number;
178
+ /**
179
+ *
180
+ * @param label - label of the interval collection this interval is being added to. This parameter is
181
+ * irrelevant for transient intervals.
182
+ * @param start - numerical start position of the interval
183
+ * @param end - numberical end position of the interval
184
+ * @param client - client creating the interval
185
+ * @param intervalType - Type of interval to create. Default is SlideOnRemove
186
+ * @param op - If this create came from a remote client, op that created it. Default is undefined (i.e. local)
187
+ * @param fromSnapshot - If this create came from loading a snapshot. Default is false.
188
+ */
189
+ create(
190
+ label: string,
191
+ start: number | undefined,
192
+ end: number | undefined,
193
+ client: Client | undefined,
194
+ intervalType: IntervalType,
195
+ op?: ISequencedDocumentMessage,
196
+ fromSnapshot?: boolean,
197
+ ): TInterval;
193
198
  }
194
199
 
195
200
  /**
196
201
  * Serializable interval whose endpoints are plain-old numbers.
197
202
  */
198
203
  export class Interval implements ISerializableInterval {
199
- /**
200
- * {@inheritDoc ISerializableInterval.properties}
201
- */
202
- public properties: PropertySet;
203
- /** @internal */
204
- public auxProps: PropertySet[] | undefined;
205
- /**
206
- * {@inheritDoc ISerializableInterval.propertyManager}
207
- */
208
- public propertyManager: PropertiesManager;
209
- constructor(
210
- public start: number,
211
- public end: number,
212
- props?: PropertySet,
213
- ) {
214
- this.propertyManager = new PropertiesManager();
215
- this.properties = {};
216
-
217
- if (props) {
218
- this.addProperties(props);
219
- }
220
- }
221
-
222
- /**
223
- * {@inheritDoc ISerializableInterval.getIntervalId}
224
- */
225
- public getIntervalId(): string | undefined {
226
- const id = this.properties?.[reservedIntervalIdKey];
227
- if (id === undefined) {
228
- return undefined;
229
- }
230
- return `${id}`;
231
- }
232
-
233
- /**
234
- * @returns an array containing any auxiliary property sets added with `addPropertySet`.
235
- */
236
- public getAdditionalPropertySets(): PropertySet[] {
237
- return this.auxProps ?? [];
238
- }
239
-
240
- /**
241
- * Adds an auxiliary set of properties to this interval.
242
- * These properties can be recovered using `getAdditionalPropertySets`
243
- * @param props - set of properties to add
244
- * @remarks - This gets called as part of the default conflict resolver for `IntervalCollection<Interval>`
245
- * (i.e. non-sequence-based interval collections). However, the additional properties don't get serialized.
246
- * This functionality seems half-baked.
247
- */
248
- public addPropertySet(props: PropertySet) {
249
- if (this.auxProps === undefined) {
250
- this.auxProps = [];
251
- }
252
- this.auxProps.push(props);
253
- }
254
-
255
- /**
256
- * {@inheritDoc ISerializableInterval.serialize}
257
- * @internal
258
- */
259
- public serialize(): ISerializedInterval {
260
- const serializedInterval: ISerializedInterval = {
261
- end: this.end,
262
- intervalType: 0,
263
- sequenceNumber: 0,
264
- start: this.start,
265
- };
266
- if (this.properties) {
267
- serializedInterval.properties = this.properties;
268
- }
269
- return serializedInterval;
270
- }
271
-
272
- /**
273
- * {@inheritDoc IInterval.clone}
274
- */
275
- public clone() {
276
- return new Interval(this.start, this.end, this.properties);
277
- }
278
-
279
- /**
280
- * {@inheritDoc IInterval.compare}
281
- */
282
- public compare(b: Interval) {
283
- const startResult = this.compareStart(b);
284
- if (startResult === 0) {
285
- const endResult = this.compareEnd(b);
286
- if (endResult === 0) {
287
- const thisId = this.getIntervalId();
288
- if (thisId) {
289
- const bId = b.getIntervalId();
290
- if (bId) {
291
- return thisId > bId ? 1 : thisId < bId ? -1 : 0;
292
- }
293
- return 0;
294
- }
295
- return 0;
296
- } else {
297
- return endResult;
298
- }
299
- } else {
300
- return startResult;
301
- }
302
- }
303
-
304
- /**
305
- * {@inheritDoc IInterval.compareStart}
306
- */
307
- public compareStart(b: Interval) {
308
- return this.start - b.start;
309
- }
310
-
311
- /**
312
- * {@inheritDoc IInterval.compareEnd}
313
- */
314
- public compareEnd(b: Interval) {
315
- return this.end - b.end;
316
- }
317
-
318
- /**
319
- * {@inheritDoc IInterval.overlaps}
320
- */
321
- public overlaps(b: Interval) {
322
- const result = (this.start <= b.end) &&
323
- (this.end >= b.start);
324
- return result;
325
- }
326
-
327
- /**
328
- * {@inheritDoc IInterval.union}
329
- */
330
- public union(b: Interval) {
331
- return new Interval(Math.min(this.start, b.start),
332
- Math.max(this.end, b.end), this.properties);
333
- }
334
-
335
- public getProperties() {
336
- return this.properties;
337
- }
338
-
339
- /**
340
- * {@inheritDoc ISerializableInterval.addProperties}
341
- */
342
- public addProperties(
343
- newProps: PropertySet,
344
- collaborating: boolean = false,
345
- seq?: number,
346
- op?: ICombiningOp,
347
- ): PropertySet | undefined {
348
- if (newProps) {
349
- this.initializeProperties();
350
- return this.propertyManager.addProperties(this.properties, newProps, op, seq, collaborating);
351
- }
352
- }
353
-
354
- /**
355
- * {@inheritDoc IInterval.modify}
356
- */
357
- public modify(label: string, start: number, end: number, op?: ISequencedDocumentMessage) {
358
- const startPos = start ?? this.start;
359
- const endPos = end ?? this.end;
360
- if (this.start === startPos && this.end === endPos) {
361
- // Return undefined to indicate that no change is necessary.
362
- return;
363
- }
364
- const newInterval = new Interval(startPos, endPos);
365
- if (this.properties) {
366
- newInterval.initializeProperties();
367
- this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
368
- }
369
- return newInterval;
370
- }
371
-
372
- private initializeProperties(): void {
373
- if (!this.propertyManager) {
374
- this.propertyManager = new PropertiesManager();
375
- }
376
- if (!this.properties) {
377
- this.properties = createMap<any>();
378
- }
379
- }
204
+ /**
205
+ * {@inheritDoc ISerializableInterval.properties}
206
+ */
207
+ public properties: PropertySet;
208
+ /** @internal */
209
+ public auxProps: PropertySet[] | undefined;
210
+ /**
211
+ * {@inheritDoc ISerializableInterval.propertyManager}
212
+ */
213
+ public propertyManager: PropertiesManager;
214
+ constructor(public start: number, public end: number, props?: PropertySet) {
215
+ this.propertyManager = new PropertiesManager();
216
+ this.properties = {};
217
+
218
+ if (props) {
219
+ this.addProperties(props);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * {@inheritDoc ISerializableInterval.getIntervalId}
225
+ */
226
+ public getIntervalId(): string | undefined {
227
+ const id = this.properties?.[reservedIntervalIdKey];
228
+ if (id === undefined) {
229
+ return undefined;
230
+ }
231
+ return `${id}`;
232
+ }
233
+
234
+ /**
235
+ * @returns an array containing any auxiliary property sets added with `addPropertySet`.
236
+ */
237
+ public getAdditionalPropertySets(): PropertySet[] {
238
+ return this.auxProps ?? [];
239
+ }
240
+
241
+ /**
242
+ * Adds an auxiliary set of properties to this interval.
243
+ * These properties can be recovered using `getAdditionalPropertySets`
244
+ * @param props - set of properties to add
245
+ * @remarks - This gets called as part of the default conflict resolver for `IntervalCollection<Interval>`
246
+ * (i.e. non-sequence-based interval collections). However, the additional properties don't get serialized.
247
+ * This functionality seems half-baked.
248
+ */
249
+ public addPropertySet(props: PropertySet) {
250
+ if (this.auxProps === undefined) {
251
+ this.auxProps = [];
252
+ }
253
+ this.auxProps.push(props);
254
+ }
255
+
256
+ /**
257
+ * {@inheritDoc ISerializableInterval.serialize}
258
+ * @internal
259
+ */
260
+ public serialize(): ISerializedInterval {
261
+ const serializedInterval: ISerializedInterval = {
262
+ end: this.end,
263
+ intervalType: 0,
264
+ sequenceNumber: 0,
265
+ start: this.start,
266
+ };
267
+ if (this.properties) {
268
+ serializedInterval.properties = this.properties;
269
+ }
270
+ return serializedInterval;
271
+ }
272
+
273
+ /**
274
+ * {@inheritDoc IInterval.clone}
275
+ */
276
+ public clone() {
277
+ return new Interval(this.start, this.end, this.properties);
278
+ }
279
+
280
+ /**
281
+ * {@inheritDoc IInterval.compare}
282
+ */
283
+ public compare(b: Interval) {
284
+ const startResult = this.compareStart(b);
285
+ if (startResult === 0) {
286
+ const endResult = this.compareEnd(b);
287
+ if (endResult === 0) {
288
+ const thisId = this.getIntervalId();
289
+ if (thisId) {
290
+ const bId = b.getIntervalId();
291
+ if (bId) {
292
+ return thisId > bId ? 1 : thisId < bId ? -1 : 0;
293
+ }
294
+ return 0;
295
+ }
296
+ return 0;
297
+ } else {
298
+ return endResult;
299
+ }
300
+ } else {
301
+ return startResult;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * {@inheritDoc IInterval.compareStart}
307
+ */
308
+ public compareStart(b: Interval) {
309
+ return this.start - b.start;
310
+ }
311
+
312
+ /**
313
+ * {@inheritDoc IInterval.compareEnd}
314
+ */
315
+ public compareEnd(b: Interval) {
316
+ return this.end - b.end;
317
+ }
318
+
319
+ /**
320
+ * {@inheritDoc IInterval.overlaps}
321
+ */
322
+ public overlaps(b: Interval) {
323
+ const result = this.start <= b.end && this.end >= b.start;
324
+ return result;
325
+ }
326
+
327
+ /**
328
+ * {@inheritDoc IInterval.union}
329
+ */
330
+ public union(b: Interval) {
331
+ return new Interval(
332
+ Math.min(this.start, b.start),
333
+ Math.max(this.end, b.end),
334
+ this.properties,
335
+ );
336
+ }
337
+
338
+ public getProperties() {
339
+ return this.properties;
340
+ }
341
+
342
+ /**
343
+ * {@inheritDoc ISerializableInterval.addProperties}
344
+ */
345
+ public addProperties(
346
+ newProps: PropertySet,
347
+ collaborating: boolean = false,
348
+ seq?: number,
349
+ op?: ICombiningOp,
350
+ ): PropertySet | undefined {
351
+ if (newProps) {
352
+ this.initializeProperties();
353
+ return this.propertyManager.addProperties(
354
+ this.properties,
355
+ newProps,
356
+ op,
357
+ seq,
358
+ collaborating,
359
+ );
360
+ }
361
+ }
362
+
363
+ /**
364
+ * {@inheritDoc IInterval.modify}
365
+ */
366
+ public modify(label: string, start: number, end: number, op?: ISequencedDocumentMessage) {
367
+ const startPos = start ?? this.start;
368
+ const endPos = end ?? this.end;
369
+ if (this.start === startPos && this.end === endPos) {
370
+ // Return undefined to indicate that no change is necessary.
371
+ return;
372
+ }
373
+ const newInterval = new Interval(startPos, endPos);
374
+ if (this.properties) {
375
+ newInterval.initializeProperties();
376
+ this.propertyManager.copyTo(
377
+ this.properties,
378
+ newInterval.properties,
379
+ newInterval.propertyManager,
380
+ );
381
+ }
382
+ return newInterval;
383
+ }
384
+
385
+ private initializeProperties(): void {
386
+ if (!this.propertyManager) {
387
+ this.propertyManager = new PropertiesManager();
388
+ }
389
+ if (!this.properties) {
390
+ this.properties = createMap<any>();
391
+ }
392
+ }
380
393
  }
381
394
 
382
395
  /**
@@ -385,930 +398,1024 @@ export class Interval implements ISerializableInterval {
385
398
  * include that content.
386
399
  */
387
400
  export class SequenceInterval implements ISerializableInterval {
388
- /**
389
- * {@inheritDoc ISerializableInterval.properties}
390
- */
391
- public properties: PropertySet;
392
- /**
393
- * {@inheritDoc ISerializableInterval.propertyManager}
394
- */
395
- public propertyManager: PropertiesManager;
396
-
397
- constructor(
398
- private readonly client: Client,
399
- /**
400
- * Start endpoint of this interval.
401
- * @remarks - This endpoint can be resolved into a character position using the SharedString it's a part of.
402
- */
403
- public start: LocalReferencePosition,
404
- /**
405
- * End endpoint of this interval.
406
- * @remarks - This endpoint can be resolved into a character position using the SharedString it's a part of.
407
- */
408
- public end: LocalReferencePosition,
409
- public intervalType: IntervalType,
410
- props?: PropertySet,
411
- ) {
412
- this.propertyManager = new PropertiesManager();
413
- this.properties = {};
414
-
415
- if (props) {
416
- this.addProperties(props);
417
- }
418
- }
419
-
420
- private callbacks?: Record<"beforePositionChange" | "afterPositionChange", () => void>;
421
-
422
- /**
423
- * Subscribes to position change events on this interval if there are no current listeners.
424
- * @internal
425
- */
426
- public addPositionChangeListeners(beforePositionChange: () => void, afterPositionChange: () => void): void {
427
- if (this.callbacks === undefined) {
428
- this.callbacks = {
429
- beforePositionChange,
430
- afterPositionChange,
431
- };
432
-
433
- const startCbs = this.start.callbacks ??= {};
434
- const endCbs = this.end.callbacks ??= {};
435
- startCbs.beforeSlide = endCbs.beforeSlide = beforePositionChange;
436
- startCbs.afterSlide = endCbs.afterSlide = afterPositionChange;
437
- }
438
- }
439
-
440
- /**
441
- * Removes the currently subscribed position change listeners.
442
- * @internal
443
- */
444
- public removePositionChangeListeners(): void {
445
- if (this.callbacks) {
446
- this.callbacks = undefined;
447
- this.start.callbacks = undefined;
448
- this.end.callbacks = undefined;
449
- }
450
- }
451
-
452
- /**
453
- * {@inheritDoc ISerializableInterval.serialize}
454
- * @internal
455
- */
456
- public serialize(): ISerializedInterval {
457
- const startPosition = this.client.localReferencePositionToPosition(this.start);
458
- const endPosition = this.client.localReferencePositionToPosition(this.end);
459
- const serializedInterval: ISerializedInterval = {
460
- end: endPosition,
461
- intervalType: this.intervalType,
462
- sequenceNumber: this.client.getCurrentSeq(),
463
- start: startPosition,
464
- };
465
-
466
- if (this.properties) {
467
- serializedInterval.properties = this.properties;
468
- }
469
-
470
- return serializedInterval;
471
- }
472
-
473
- /**
474
- * {@inheritDoc IInterval.clone}
475
- */
476
- public clone() {
477
- return new SequenceInterval(this.client, this.start, this.end, this.intervalType, this.properties);
478
- }
479
-
480
- /**
481
- * {@inheritDoc IInterval.compare}
482
- */
483
- public compare(b: SequenceInterval) {
484
- const startResult = this.compareStart(b);
485
- if (startResult === 0) {
486
- const endResult = this.compareEnd(b);
487
- if (endResult === 0) {
488
- const thisId = this.getIntervalId();
489
- if (thisId) {
490
- const bId = b.getIntervalId();
491
- if (bId) {
492
- return thisId > bId ? 1 : thisId < bId ? -1 : 0;
493
- }
494
- return 0;
495
- }
496
- return 0;
497
- } else {
498
- return endResult;
499
- }
500
- } else {
501
- return startResult;
502
- }
503
- }
504
-
505
- /**
506
- * {@inheritDoc IInterval.compareStart}
507
- */
508
- public compareStart(b: SequenceInterval) {
509
- return compareReferencePositions(this.start, b.start);
510
- }
511
-
512
- /**
513
- * {@inheritDoc IInterval.compareEnd}
514
- */
515
- public compareEnd(b: SequenceInterval) {
516
- return compareReferencePositions(this.end, b.end);
517
- }
518
-
519
- /**
520
- * {@inheritDoc IInterval.overlaps}
521
- */
522
- public overlaps(b: SequenceInterval) {
523
- const result = (compareReferencePositions(this.start, b.end) <= 0) &&
524
- (compareReferencePositions(this.end, b.start) >= 0);
525
- return result;
526
- }
527
-
528
- /**
529
- * {@inheritDoc ISerializableInterval.getIntervalId}
530
- */
531
- public getIntervalId(): string | undefined {
532
- const id = this.properties?.[reservedIntervalIdKey];
533
- if (id === undefined) {
534
- return undefined;
535
- }
536
- return `${id}`;
537
- }
538
-
539
- /**
540
- * {@inheritDoc IInterval.union}
541
- */
542
- public union(b: SequenceInterval) {
543
- return new SequenceInterval(this.client, minReferencePosition(this.start, b.start),
544
- maxReferencePosition(this.end, b.end), this.intervalType);
545
- }
546
-
547
- /**
548
- * {@inheritDoc ISerializableInterval.addProperties}
549
- */
550
- public addProperties(
551
- newProps: PropertySet,
552
- collab: boolean = false,
553
- seq?: number,
554
- op?: ICombiningOp,
555
- ): PropertySet | undefined {
556
- this.initializeProperties();
557
- return this.propertyManager.addProperties(this.properties, newProps, op, seq, collab);
558
- }
559
-
560
- /**
561
- * @returns whether this interval overlaps two numerical positions.
562
- * @remarks - this is currently strict overlap, which doesn't align with the endpoint treatment of`.overlaps()`
563
- */
564
- public overlapsPos(bstart: number, bend: number) {
565
- const startPos = this.client.localReferencePositionToPosition(this.start);
566
- const endPos = this.client.localReferencePositionToPosition(this.end);
567
- return (endPos > bstart) && (startPos < bend);
568
- }
569
-
570
- /**
571
- * {@inheritDoc IInterval.modify}
572
- */
573
- public modify(label: string, start: number, end: number, op?: ISequencedDocumentMessage, localSeq?: number) {
574
- const getRefType = (baseType: ReferenceType): ReferenceType => {
575
- let refType = baseType;
576
- if (op === undefined) {
577
- refType &= ~ReferenceType.SlideOnRemove;
578
- refType |= ReferenceType.StayOnRemove;
579
- }
580
- return refType;
581
- };
582
-
583
- let startRef = this.start;
584
- if (start !== undefined) {
585
- startRef = createPositionReference(
586
- this.client, start, getRefType(this.start.refType), op, undefined, localSeq,
587
- );
588
- if (this.start.properties) {
589
- startRef.addProperties(this.start.properties);
590
- }
591
- }
592
-
593
- let endRef = this.end;
594
- if (end !== undefined) {
595
- endRef = createPositionReference(
596
- this.client, end, getRefType(this.end.refType), op, undefined, localSeq,
597
- );
598
- if (this.end.properties) {
599
- endRef.addProperties(this.end.properties);
600
- }
601
- }
602
-
603
- const newInterval = new SequenceInterval(this.client, startRef, endRef, this.intervalType);
604
- if (this.properties) {
605
- newInterval.initializeProperties();
606
- this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
607
- }
608
- return newInterval;
609
- }
610
-
611
- private initializeProperties(): void {
612
- if (!this.propertyManager) {
613
- this.propertyManager = new PropertiesManager();
614
- }
615
- if (!this.properties) {
616
- this.properties = createMap<any>();
617
- }
618
- }
401
+ /**
402
+ * {@inheritDoc ISerializableInterval.properties}
403
+ */
404
+ public properties: PropertySet;
405
+ /**
406
+ * {@inheritDoc ISerializableInterval.propertyManager}
407
+ */
408
+ public propertyManager: PropertiesManager;
409
+
410
+ constructor(
411
+ private readonly client: Client,
412
+ /**
413
+ * Start endpoint of this interval.
414
+ * @remarks - This endpoint can be resolved into a character position using the SharedString it's a part of.
415
+ */
416
+ public start: LocalReferencePosition,
417
+ /**
418
+ * End endpoint of this interval.
419
+ * @remarks - This endpoint can be resolved into a character position using the SharedString it's a part of.
420
+ */
421
+ public end: LocalReferencePosition,
422
+ public intervalType: IntervalType,
423
+ props?: PropertySet,
424
+ ) {
425
+ this.propertyManager = new PropertiesManager();
426
+ this.properties = {};
427
+
428
+ if (props) {
429
+ this.addProperties(props);
430
+ }
431
+ }
432
+
433
+ private callbacks?: Record<"beforePositionChange" | "afterPositionChange", () => void>;
434
+
435
+ /**
436
+ * Subscribes to position change events on this interval if there are no current listeners.
437
+ * @internal
438
+ */
439
+ public addPositionChangeListeners(
440
+ beforePositionChange: () => void,
441
+ afterPositionChange: () => void,
442
+ ): void {
443
+ if (this.callbacks === undefined) {
444
+ this.callbacks = {
445
+ beforePositionChange,
446
+ afterPositionChange,
447
+ };
448
+
449
+ const startCbs = (this.start.callbacks ??= {});
450
+ const endCbs = (this.end.callbacks ??= {});
451
+ startCbs.beforeSlide = endCbs.beforeSlide = beforePositionChange;
452
+ startCbs.afterSlide = endCbs.afterSlide = afterPositionChange;
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Removes the currently subscribed position change listeners.
458
+ * @internal
459
+ */
460
+ public removePositionChangeListeners(): void {
461
+ if (this.callbacks) {
462
+ this.callbacks = undefined;
463
+ this.start.callbacks = undefined;
464
+ this.end.callbacks = undefined;
465
+ }
466
+ }
467
+
468
+ /**
469
+ * {@inheritDoc ISerializableInterval.serialize}
470
+ * @internal
471
+ */
472
+ public serialize(): ISerializedInterval {
473
+ const startPosition = this.client.localReferencePositionToPosition(this.start);
474
+ const endPosition = this.client.localReferencePositionToPosition(this.end);
475
+ const serializedInterval: ISerializedInterval = {
476
+ end: endPosition,
477
+ intervalType: this.intervalType,
478
+ sequenceNumber: this.client.getCurrentSeq(),
479
+ start: startPosition,
480
+ };
481
+
482
+ if (this.properties) {
483
+ serializedInterval.properties = this.properties;
484
+ }
485
+
486
+ return serializedInterval;
487
+ }
488
+
489
+ /**
490
+ * {@inheritDoc IInterval.clone}
491
+ */
492
+ public clone() {
493
+ return new SequenceInterval(
494
+ this.client,
495
+ this.start,
496
+ this.end,
497
+ this.intervalType,
498
+ this.properties,
499
+ );
500
+ }
501
+
502
+ /**
503
+ * {@inheritDoc IInterval.compare}
504
+ */
505
+ public compare(b: SequenceInterval) {
506
+ const startResult = this.compareStart(b);
507
+ if (startResult === 0) {
508
+ const endResult = this.compareEnd(b);
509
+ if (endResult === 0) {
510
+ const thisId = this.getIntervalId();
511
+ if (thisId) {
512
+ const bId = b.getIntervalId();
513
+ if (bId) {
514
+ return thisId > bId ? 1 : thisId < bId ? -1 : 0;
515
+ }
516
+ return 0;
517
+ }
518
+ return 0;
519
+ } else {
520
+ return endResult;
521
+ }
522
+ } else {
523
+ return startResult;
524
+ }
525
+ }
526
+
527
+ /**
528
+ * {@inheritDoc IInterval.compareStart}
529
+ */
530
+ public compareStart(b: SequenceInterval) {
531
+ return compareReferencePositions(this.start, b.start);
532
+ }
533
+
534
+ /**
535
+ * {@inheritDoc IInterval.compareEnd}
536
+ */
537
+ public compareEnd(b: SequenceInterval) {
538
+ return compareReferencePositions(this.end, b.end);
539
+ }
540
+
541
+ /**
542
+ * {@inheritDoc IInterval.overlaps}
543
+ */
544
+ public overlaps(b: SequenceInterval) {
545
+ const result =
546
+ compareReferencePositions(this.start, b.end) <= 0 &&
547
+ compareReferencePositions(this.end, b.start) >= 0;
548
+ return result;
549
+ }
550
+
551
+ /**
552
+ * {@inheritDoc ISerializableInterval.getIntervalId}
553
+ */
554
+ public getIntervalId(): string | undefined {
555
+ const id = this.properties?.[reservedIntervalIdKey];
556
+ if (id === undefined) {
557
+ return undefined;
558
+ }
559
+ return `${id}`;
560
+ }
561
+
562
+ /**
563
+ * {@inheritDoc IInterval.union}
564
+ */
565
+ public union(b: SequenceInterval) {
566
+ return new SequenceInterval(
567
+ this.client,
568
+ minReferencePosition(this.start, b.start),
569
+ maxReferencePosition(this.end, b.end),
570
+ this.intervalType,
571
+ );
572
+ }
573
+
574
+ /**
575
+ * {@inheritDoc ISerializableInterval.addProperties}
576
+ */
577
+ public addProperties(
578
+ newProps: PropertySet,
579
+ collab: boolean = false,
580
+ seq?: number,
581
+ op?: ICombiningOp,
582
+ ): PropertySet | undefined {
583
+ this.initializeProperties();
584
+ return this.propertyManager.addProperties(this.properties, newProps, op, seq, collab);
585
+ }
586
+
587
+ /**
588
+ * @returns whether this interval overlaps two numerical positions.
589
+ * @remarks - this is currently strict overlap, which doesn't align with the endpoint treatment of`.overlaps()`
590
+ */
591
+ public overlapsPos(bstart: number, bend: number) {
592
+ const startPos = this.client.localReferencePositionToPosition(this.start);
593
+ const endPos = this.client.localReferencePositionToPosition(this.end);
594
+ return endPos > bstart && startPos < bend;
595
+ }
596
+
597
+ /**
598
+ * {@inheritDoc IInterval.modify}
599
+ */
600
+ public modify(
601
+ label: string,
602
+ start: number,
603
+ end: number,
604
+ op?: ISequencedDocumentMessage,
605
+ localSeq?: number,
606
+ ) {
607
+ const getRefType = (baseType: ReferenceType): ReferenceType => {
608
+ let refType = baseType;
609
+ if (op === undefined) {
610
+ refType &= ~ReferenceType.SlideOnRemove;
611
+ refType |= ReferenceType.StayOnRemove;
612
+ }
613
+ return refType;
614
+ };
615
+
616
+ let startRef = this.start;
617
+ if (start !== undefined) {
618
+ startRef = createPositionReference(
619
+ this.client,
620
+ start,
621
+ getRefType(this.start.refType),
622
+ op,
623
+ undefined,
624
+ localSeq,
625
+ );
626
+ if (this.start.properties) {
627
+ startRef.addProperties(this.start.properties);
628
+ }
629
+ }
630
+
631
+ let endRef = this.end;
632
+ if (end !== undefined) {
633
+ endRef = createPositionReference(
634
+ this.client,
635
+ end,
636
+ getRefType(this.end.refType),
637
+ op,
638
+ undefined,
639
+ localSeq,
640
+ );
641
+ if (this.end.properties) {
642
+ endRef.addProperties(this.end.properties);
643
+ }
644
+ }
645
+
646
+ const newInterval = new SequenceInterval(this.client, startRef, endRef, this.intervalType);
647
+ if (this.properties) {
648
+ newInterval.initializeProperties();
649
+ this.propertyManager.copyTo(
650
+ this.properties,
651
+ newInterval.properties,
652
+ newInterval.propertyManager,
653
+ );
654
+ }
655
+ return newInterval;
656
+ }
657
+
658
+ private initializeProperties(): void {
659
+ if (!this.propertyManager) {
660
+ this.propertyManager = new PropertiesManager();
661
+ }
662
+ if (!this.properties) {
663
+ this.properties = createMap<any>();
664
+ }
665
+ }
619
666
  }
620
667
 
621
668
  function createPositionReferenceFromSegoff(
622
- client: Client,
623
- segoff: { segment: ISegment | undefined; offset: number | undefined; },
624
- refType: ReferenceType,
625
- op?: ISequencedDocumentMessage,
626
- localSeq?: number): LocalReferencePosition {
627
- if (segoff.segment) {
628
- const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
629
- return ref;
630
- }
631
-
632
- // Creating references on detached segments is allowed for:
633
- // - Transient segments
634
- // - References coming from a remote client (location may have been concurrently removed)
635
- // - References being rebased to a new sequence number
636
- // (segment they originally referred to may have been removed with no suitable replacement)
637
- if (!op && !localSeq && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
638
- throw new UsageError("Non-transient references need segment");
639
- }
640
-
641
- return createDetachedLocalReferencePosition(refType);
669
+ client: Client,
670
+ segoff: { segment: ISegment | undefined; offset: number | undefined },
671
+ refType: ReferenceType,
672
+ op?: ISequencedDocumentMessage,
673
+ localSeq?: number,
674
+ ): LocalReferencePosition {
675
+ if (segoff.segment) {
676
+ const ref = client.createLocalReferencePosition(
677
+ segoff.segment,
678
+ segoff.offset,
679
+ refType,
680
+ undefined,
681
+ );
682
+ return ref;
683
+ }
684
+
685
+ // Creating references on detached segments is allowed for:
686
+ // - Transient segments
687
+ // - References coming from a remote client (location may have been concurrently removed)
688
+ // - References being rebased to a new sequence number
689
+ // (segment they originally referred to may have been removed with no suitable replacement)
690
+ if (!op && !localSeq && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
691
+ throw new UsageError("Non-transient references need segment");
692
+ }
693
+
694
+ return createDetachedLocalReferencePosition(refType);
642
695
  }
643
696
 
644
697
  function createPositionReference(
645
- client: Client,
646
- pos: number,
647
- refType: ReferenceType,
648
- op?: ISequencedDocumentMessage,
649
- fromSnapshot?: boolean,
650
- localSeq?: number,
698
+ client: Client,
699
+ pos: number,
700
+ refType: ReferenceType,
701
+ op?: ISequencedDocumentMessage,
702
+ fromSnapshot?: boolean,
703
+ localSeq?: number,
651
704
  ): LocalReferencePosition {
652
- let segoff;
653
- if (op) {
654
- assert((refType & ReferenceType.SlideOnRemove) !== 0, 0x2f5 /* op create references must be SlideOnRemove */);
655
- segoff = client.getContainingSegment(pos, { referenceSequenceNumber: op.referenceSequenceNumber, clientId: op.clientId });
656
- segoff = client.getSlideToSegment(segoff);
657
- } else {
658
- assert((refType & ReferenceType.SlideOnRemove) === 0 || !!fromSnapshot,
659
- 0x2f6 /* SlideOnRemove references must be op created */);
660
- segoff = client.getContainingSegment(pos, undefined, localSeq);
661
- }
662
- return createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq);
705
+ let segoff;
706
+ if (op) {
707
+ assert(
708
+ (refType & ReferenceType.SlideOnRemove) !== 0,
709
+ 0x2f5 /* op create references must be SlideOnRemove */,
710
+ );
711
+ segoff = client.getContainingSegment(pos, {
712
+ referenceSequenceNumber: op.referenceSequenceNumber,
713
+ clientId: op.clientId,
714
+ });
715
+ segoff = client.getSlideToSegment(segoff);
716
+ } else {
717
+ assert(
718
+ (refType & ReferenceType.SlideOnRemove) === 0 || !!fromSnapshot,
719
+ 0x2f6 /* SlideOnRemove references must be op created */,
720
+ );
721
+ segoff = client.getContainingSegment(pos, undefined, localSeq);
722
+ }
723
+ return createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq);
663
724
  }
664
725
 
665
726
  export function createSequenceInterval(
666
- label: string,
667
- start: number,
668
- end: number,
669
- client: Client,
670
- intervalType: IntervalType,
671
- op?: ISequencedDocumentMessage,
672
- fromSnapshot?: boolean,
727
+ label: string,
728
+ start: number,
729
+ end: number,
730
+ client: Client,
731
+ intervalType: IntervalType,
732
+ op?: ISequencedDocumentMessage,
733
+ fromSnapshot?: boolean,
673
734
  ): SequenceInterval {
674
- let beginRefType = ReferenceType.RangeBegin;
675
- let endRefType = ReferenceType.RangeEnd;
676
- if (intervalType === IntervalType.Transient) {
677
- beginRefType = ReferenceType.Transient;
678
- endRefType = ReferenceType.Transient;
679
- } else {
680
- if (intervalType === IntervalType.Nest) {
681
- beginRefType = ReferenceType.NestBegin;
682
- endRefType = ReferenceType.NestEnd;
683
- }
684
- // All non-transient interval references must eventually be SlideOnRemove
685
- // To ensure eventual consistency, they must start as StayOnRemove when
686
- // pending (created locally and creation op is not acked)
687
- if (op || fromSnapshot) {
688
- beginRefType |= ReferenceType.SlideOnRemove;
689
- endRefType |= ReferenceType.SlideOnRemove;
690
- } else {
691
- beginRefType |= ReferenceType.StayOnRemove;
692
- endRefType |= ReferenceType.StayOnRemove;
693
- }
694
- }
695
-
696
- const startLref = createPositionReference(client, start, beginRefType, op, fromSnapshot);
697
- const endLref = createPositionReference(client, end, endRefType, op, fromSnapshot);
698
- const rangeProp = {
699
- [reservedRangeLabelsKey]: [label],
700
- };
701
- startLref.addProperties(rangeProp);
702
- endLref.addProperties(rangeProp);
703
-
704
- const ival = new SequenceInterval(
705
- client,
706
- startLref,
707
- endLref,
708
- intervalType,
709
- rangeProp,
710
- );
711
- return ival;
735
+ let beginRefType = ReferenceType.RangeBegin;
736
+ let endRefType = ReferenceType.RangeEnd;
737
+ if (intervalType === IntervalType.Transient) {
738
+ beginRefType = ReferenceType.Transient;
739
+ endRefType = ReferenceType.Transient;
740
+ } else {
741
+ if (intervalType === IntervalType.Nest) {
742
+ beginRefType = ReferenceType.NestBegin;
743
+ endRefType = ReferenceType.NestEnd;
744
+ }
745
+ // All non-transient interval references must eventually be SlideOnRemove
746
+ // To ensure eventual consistency, they must start as StayOnRemove when
747
+ // pending (created locally and creation op is not acked)
748
+ if (op || fromSnapshot) {
749
+ beginRefType |= ReferenceType.SlideOnRemove;
750
+ endRefType |= ReferenceType.SlideOnRemove;
751
+ } else {
752
+ beginRefType |= ReferenceType.StayOnRemove;
753
+ endRefType |= ReferenceType.StayOnRemove;
754
+ }
755
+ }
756
+
757
+ const startLref = createPositionReference(client, start, beginRefType, op, fromSnapshot);
758
+ const endLref = createPositionReference(client, end, endRefType, op, fromSnapshot);
759
+ const rangeProp = {
760
+ [reservedRangeLabelsKey]: [label],
761
+ };
762
+ startLref.addProperties(rangeProp);
763
+ endLref.addProperties(rangeProp);
764
+
765
+ const ival = new SequenceInterval(client, startLref, endLref, intervalType, rangeProp);
766
+ return ival;
712
767
  }
713
768
 
714
769
  export function defaultIntervalConflictResolver(a: Interval, b: Interval) {
715
- a.addPropertySet(b.properties);
716
- return a;
770
+ a.addPropertySet(b.properties);
771
+ return a;
717
772
  }
718
773
 
719
774
  export function createIntervalIndex(conflict?: IntervalConflictResolver<Interval>) {
720
- const helpers: IIntervalHelpers<Interval> = {
721
- compareEnds: compareIntervalEnds,
722
- create: createInterval,
723
- };
724
- const lc = new LocalIntervalCollection<Interval>(undefined as any as Client, "", helpers);
725
- if (conflict) {
726
- lc.addConflictResolver(conflict);
727
- } else {
728
- lc.addConflictResolver(defaultIntervalConflictResolver);
729
- }
730
- return lc;
775
+ const helpers: IIntervalHelpers<Interval> = {
776
+ compareEnds: compareIntervalEnds,
777
+ create: createInterval,
778
+ };
779
+ const lc = new LocalIntervalCollection<Interval>(undefined as any as Client, "", helpers);
780
+ if (conflict) {
781
+ lc.addConflictResolver(conflict);
782
+ } else {
783
+ lc.addConflictResolver(defaultIntervalConflictResolver);
784
+ }
785
+ return lc;
731
786
  }
732
787
 
733
788
  export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
734
- private readonly intervalTree = new IntervalTree<TInterval>();
735
- private readonly endIntervalTree: RedBlackTree<TInterval, TInterval>;
736
- private readonly intervalIdMap: Map<string, TInterval> = new Map();
737
- private conflictResolver: IntervalConflictResolver<TInterval> | undefined;
738
- private endConflictResolver: ConflictAction<TInterval, TInterval> | undefined;
739
-
740
- private static readonly legacyIdPrefix = "legacy";
741
-
742
- constructor(
743
- private readonly client: Client,
744
- private readonly label: string,
745
- private readonly helpers: IIntervalHelpers<TInterval>,
746
- /** Callback invoked each time one of the endpoints of an interval slides. */
747
- private readonly onPositionChange?: (interval: TInterval, previousInterval: TInterval) => void,
748
- ) {
749
- // eslint-disable-next-line @typescript-eslint/unbound-method
750
- this.endIntervalTree = new RedBlackTree<TInterval, TInterval>(helpers.compareEnds);
751
- }
752
-
753
- public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>) {
754
- this.conflictResolver = conflictResolver;
755
- this.endConflictResolver =
756
- (key: TInterval, currentKey: TInterval) => {
757
- const ival = conflictResolver(key, currentKey);
758
- return {
759
- data: ival,
760
- key: ival,
761
- };
762
- };
763
- }
764
-
765
- public map(fn: (interval: TInterval) => void) {
766
- this.intervalTree.map(fn);
767
- }
768
-
769
- public createLegacyId(start: number, end: number): string {
770
- // Create a non-unique ID based on start and end to be used on intervals that come from legacy clients
771
- // without ID's.
772
- return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
773
- }
774
-
775
- /**
776
- * Validates that a serialized interval has the ID property. Creates an ID
777
- * if one does not already exist
778
- *
779
- * @param serializedInterval - The interval to be checked
780
- * @returns The interval's existing or newly created id
781
- */
782
- public ensureSerializedId(serializedInterval: ISerializedInterval): string {
783
- let id: string | undefined = serializedInterval.properties?.[reservedIntervalIdKey];
784
- if (id === undefined) {
785
- // An interval came over the wire without an ID, so create a non-unique one based on start/end.
786
- // This will allow all clients to refer to this interval consistently.
787
- id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
788
- const newProps = {
789
- [reservedIntervalIdKey]: id,
790
- };
791
- serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
792
- }
793
- // Make the ID immutable for safety's sake.
794
- Object.defineProperty(serializedInterval.properties, reservedIntervalIdKey, {
795
- configurable: false,
796
- enumerable: true,
797
- writable: false,
798
- });
799
-
800
- return id;
801
- }
802
-
803
- public mapUntil(fn: (interval: TInterval) => boolean) {
804
- this.intervalTree.mapUntil(fn);
805
- }
806
-
807
- public gatherIterationResults(
808
- results: TInterval[],
809
- iteratesForward: boolean,
810
- start?: number,
811
- end?: number) {
812
- if (this.intervalTree.intervals.isEmpty()) {
813
- return;
814
- }
815
-
816
- if (start === undefined && end === undefined) {
817
- // No start/end provided. Gather the whole tree in the specified order.
818
- if (iteratesForward) {
819
- this.intervalTree.map((interval: TInterval) => {
820
- results.push(interval);
821
- });
822
- } else {
823
- this.intervalTree.mapBackward((interval: TInterval) => {
824
- results.push(interval);
825
- });
826
- }
827
- } else {
828
- const transientInterval: TInterval = this.helpers.create(
829
- "transient",
830
- start,
831
- end,
832
- this.client,
833
- IntervalType.Transient,
834
- );
835
-
836
- if (start === undefined) {
837
- // Only end position provided. Since the tree is not sorted by end position,
838
- // walk the whole tree in the specified order, gathering intervals that match the end.
839
- if (iteratesForward) {
840
- this.intervalTree.map((interval: TInterval) => {
841
- if (transientInterval.compareEnd(interval) === 0) {
842
- results.push(interval);
843
- }
844
- });
845
- } else {
846
- this.intervalTree.mapBackward((interval: TInterval) => {
847
- if (transientInterval.compareEnd(interval) === 0) {
848
- results.push(interval);
849
- }
850
- });
851
- }
852
- } else {
853
- // Start and (possibly) end provided. Walk the subtrees that may contain
854
- // this start position.
855
- const compareFn =
856
- end === undefined ?
857
- (node: IntervalNode<TInterval>) => {
858
- return transientInterval.compareStart(node.key);
859
- } :
860
- (node: IntervalNode<TInterval>) => {
861
- return transientInterval.compare(node.key);
862
- };
863
- const continueLeftFn = (cmpResult: number) => cmpResult <= 0;
864
- const continueRightFn = (cmpResult: number) => cmpResult >= 0;
865
- const actionFn = (node: IntervalNode<TInterval>) => {
866
- results.push(node.key);
867
- };
868
-
869
- if (iteratesForward) {
870
- this.intervalTree.intervals.walkExactMatchesForward(
871
- compareFn, actionFn, continueLeftFn, continueRightFn,
872
- );
873
- } else {
874
- this.intervalTree.intervals.walkExactMatchesBackward(
875
- compareFn, actionFn, continueLeftFn, continueRightFn,
876
- );
877
- }
878
- }
879
- }
880
- }
881
-
882
- /**
883
- * @returns an array of all intervals contained in this collection that overlap the range
884
- * `[startPosition, endPosition]`.
885
- */
886
- public findOverlappingIntervals(startPosition: number, endPosition: number) {
887
- if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
888
- return [];
889
- }
890
- const transientInterval =
891
- this.helpers.create(
892
- "transient",
893
- startPosition,
894
- endPosition,
895
- this.client,
896
- IntervalType.Transient);
897
-
898
- const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
899
- return overlappingIntervalNodes.map((node) => node.key);
900
- }
901
-
902
- public previousInterval(pos: number) {
903
- const transientInterval = this.helpers.create(
904
- "transient", pos, pos, this.client, IntervalType.Transient);
905
- const rbNode = this.endIntervalTree.floor(transientInterval);
906
- if (rbNode) {
907
- return rbNode.data;
908
- }
909
- }
910
-
911
- public nextInterval(pos: number) {
912
- const transientInterval = this.helpers.create(
913
- "transient", pos, pos, this.client, IntervalType.Transient);
914
- const rbNode = this.endIntervalTree.ceil(transientInterval);
915
- if (rbNode) {
916
- return rbNode.data;
917
- }
918
- }
919
-
920
- public removeInterval(startPosition: number, endPosition: number) {
921
- const transientInterval = this.helpers.create(
922
- "transient", startPosition, endPosition, this.client, IntervalType.Transient);
923
- this.intervalTree.remove(transientInterval);
924
- this.endIntervalTree.remove(transientInterval);
925
- return transientInterval;
926
- }
927
-
928
- private removeIntervalFromIndex(interval: TInterval) {
929
- this.intervalTree.removeExisting(interval);
930
- this.endIntervalTree.remove(interval);
931
-
932
- const id = interval.getIntervalId();
933
-
934
- assert(id !== undefined, 0x311 /* expected id to exist on interval */);
935
-
936
- this.intervalIdMap.delete(id);
937
- }
938
-
939
- public removeExistingInterval(interval: TInterval) {
940
- this.removeIntervalFromIndex(interval);
941
- this.removeIntervalListeners(interval);
942
- }
943
-
944
- public createInterval(
945
- start: number,
946
- end: number,
947
- intervalType: IntervalType,
948
- op?: ISequencedDocumentMessage): TInterval {
949
- return this.helpers.create(this.label, start, end, this.client, intervalType, op);
950
- }
951
-
952
- public addInterval(
953
- start: number,
954
- end: number,
955
- intervalType: IntervalType,
956
- props?: PropertySet,
957
- op?: ISequencedDocumentMessage) {
958
- const interval: TInterval = this.createInterval(start, end, intervalType, op);
959
- if (interval) {
960
- if (!interval.properties) {
961
- interval.properties = createMap<any>();
962
- }
963
-
964
- if (props) {
965
- interval.addProperties(props);
966
- }
967
- interval.properties[reservedIntervalIdKey] ??= uuid();
968
- this.add(interval);
969
- }
970
- return interval;
971
- }
972
-
973
- private linkEndpointsToInterval(interval: TInterval): void {
974
- if (interval instanceof SequenceInterval) {
975
- interval.start.addProperties({ interval });
976
- interval.end.addProperties({ interval });
977
- }
978
- }
979
-
980
- private addIntervalToIndex(interval: TInterval) {
981
- const id = interval.getIntervalId();
982
- assert(id !== undefined, 0x2c0 /* "ID must be created before adding interval to collection" */);
983
- // Make the ID immutable.
984
- Object.defineProperty(interval.properties, reservedIntervalIdKey, {
985
- configurable: false,
986
- enumerable: true,
987
- writable: false,
988
- });
989
- this.intervalTree.put(interval, this.conflictResolver);
990
- this.endIntervalTree.put(interval, interval, this.endConflictResolver);
991
- this.intervalIdMap.set(id, interval);
992
- }
993
-
994
- public add(interval: TInterval): void {
995
- this.linkEndpointsToInterval(interval);
996
- this.addIntervalToIndex(interval);
997
- this.addIntervalListeners(interval);
998
- }
999
-
1000
- public getIntervalById(id: string) {
1001
- return this.intervalIdMap.get(id);
1002
- }
1003
-
1004
- public changeInterval(
1005
- interval: TInterval,
1006
- start: number | undefined,
1007
- end: number | undefined,
1008
- op?: ISequencedDocumentMessage,
1009
- localSeq?: number,
1010
- ) {
1011
- const newInterval = interval.modify(this.label, start, end, op, localSeq) as TInterval | undefined;
1012
- if (newInterval) {
1013
- this.removeExistingInterval(interval);
1014
- this.add(newInterval);
1015
- }
1016
- return newInterval;
1017
- }
1018
-
1019
- public serialize(): ISerializedIntervalCollectionV2 {
1020
- const intervals = this.intervalTree.intervals.keys();
1021
- return {
1022
- label: this.label,
1023
- intervals: intervals.map((interval) => compressInterval(interval.serialize())),
1024
- version: 2,
1025
- };
1026
- }
1027
-
1028
- private addIntervalListeners(interval: TInterval) {
1029
- const cloneRef = (ref: LocalReferencePosition) => {
1030
- const segment = ref.getSegment();
1031
- if (segment === undefined) {
1032
- // Cloning is unnecessary: refs which have slid off the string entirely
1033
- // never get slid back on. Creation code for refs doesn't accept undefined segment
1034
- // either, so this must be special-cased.
1035
- return ref;
1036
- }
1037
-
1038
- return this.client.createLocalReferencePosition(
1039
- segment,
1040
- ref.getOffset(),
1041
- ReferenceType.Transient,
1042
- ref.properties,
1043
- );
1044
- };
1045
- if (interval instanceof SequenceInterval) {
1046
- let previousInterval: TInterval & SequenceInterval | undefined;
1047
- let pendingChanges = 0;
1048
- interval.addPositionChangeListeners(
1049
- () => {
1050
- pendingChanges++;
1051
- // Note: both start and end can change and invoke beforeSlide on each endpoint before afterSlide.
1052
- if (!previousInterval) {
1053
- previousInterval = interval.clone() as TInterval & SequenceInterval;
1054
- previousInterval.start = cloneRef(previousInterval.start);
1055
- previousInterval.end = cloneRef(previousInterval.end);
1056
- this.removeIntervalFromIndex(interval);
1057
- }
1058
- },
1059
- () => {
1060
- assert(previousInterval !== undefined, 0x3fa /* Invalid interleaving of before/after slide */);
1061
- pendingChanges--;
1062
- if (pendingChanges === 0) {
1063
- this.addIntervalToIndex(interval);
1064
- this.onPositionChange?.(interval, previousInterval);
1065
- previousInterval = undefined;
1066
- }
1067
- },
1068
- );
1069
- }
1070
- }
1071
-
1072
- private removeIntervalListeners(interval: TInterval) {
1073
- if (interval instanceof SequenceInterval) {
1074
- interval.removePositionChangeListeners();
1075
- }
1076
- }
789
+ private readonly intervalTree = new IntervalTree<TInterval>();
790
+ private readonly endIntervalTree: RedBlackTree<TInterval, TInterval>;
791
+ private readonly intervalIdMap: Map<string, TInterval> = new Map();
792
+ private conflictResolver: IntervalConflictResolver<TInterval> | undefined;
793
+ private endConflictResolver: ConflictAction<TInterval, TInterval> | undefined;
794
+
795
+ private static readonly legacyIdPrefix = "legacy";
796
+
797
+ constructor(
798
+ private readonly client: Client,
799
+ private readonly label: string,
800
+ private readonly helpers: IIntervalHelpers<TInterval>,
801
+ /** Callback invoked each time one of the endpoints of an interval slides. */
802
+ private readonly onPositionChange?: (
803
+ interval: TInterval,
804
+ previousInterval: TInterval,
805
+ ) => void,
806
+ ) {
807
+ // eslint-disable-next-line @typescript-eslint/unbound-method
808
+ this.endIntervalTree = new RedBlackTree<TInterval, TInterval>(helpers.compareEnds);
809
+ }
810
+
811
+ public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>) {
812
+ this.conflictResolver = conflictResolver;
813
+ this.endConflictResolver = (key: TInterval, currentKey: TInterval) => {
814
+ const ival = conflictResolver(key, currentKey);
815
+ return {
816
+ data: ival,
817
+ key: ival,
818
+ };
819
+ };
820
+ }
821
+
822
+ public map(fn: (interval: TInterval) => void) {
823
+ this.intervalTree.map(fn);
824
+ }
825
+
826
+ public createLegacyId(start: number, end: number): string {
827
+ // Create a non-unique ID based on start and end to be used on intervals that come from legacy clients
828
+ // without ID's.
829
+ return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
830
+ }
831
+
832
+ /**
833
+ * Validates that a serialized interval has the ID property. Creates an ID
834
+ * if one does not already exist
835
+ *
836
+ * @param serializedInterval - The interval to be checked
837
+ * @returns The interval's existing or newly created id
838
+ */
839
+ public ensureSerializedId(serializedInterval: ISerializedInterval): string {
840
+ let id: string | undefined = serializedInterval.properties?.[reservedIntervalIdKey];
841
+ if (id === undefined) {
842
+ // An interval came over the wire without an ID, so create a non-unique one based on start/end.
843
+ // This will allow all clients to refer to this interval consistently.
844
+ id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
845
+ const newProps = {
846
+ [reservedIntervalIdKey]: id,
847
+ };
848
+ serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
849
+ }
850
+ // Make the ID immutable for safety's sake.
851
+ Object.defineProperty(serializedInterval.properties, reservedIntervalIdKey, {
852
+ configurable: false,
853
+ enumerable: true,
854
+ writable: false,
855
+ });
856
+
857
+ return id;
858
+ }
859
+
860
+ public mapUntil(fn: (interval: TInterval) => boolean) {
861
+ this.intervalTree.mapUntil(fn);
862
+ }
863
+
864
+ public gatherIterationResults(
865
+ results: TInterval[],
866
+ iteratesForward: boolean,
867
+ start?: number,
868
+ end?: number,
869
+ ) {
870
+ if (this.intervalTree.intervals.isEmpty()) {
871
+ return;
872
+ }
873
+
874
+ if (start === undefined && end === undefined) {
875
+ // No start/end provided. Gather the whole tree in the specified order.
876
+ if (iteratesForward) {
877
+ this.intervalTree.map((interval: TInterval) => {
878
+ results.push(interval);
879
+ });
880
+ } else {
881
+ this.intervalTree.mapBackward((interval: TInterval) => {
882
+ results.push(interval);
883
+ });
884
+ }
885
+ } else {
886
+ const transientInterval: TInterval = this.helpers.create(
887
+ "transient",
888
+ start,
889
+ end,
890
+ this.client,
891
+ IntervalType.Transient,
892
+ );
893
+
894
+ if (start === undefined) {
895
+ // Only end position provided. Since the tree is not sorted by end position,
896
+ // walk the whole tree in the specified order, gathering intervals that match the end.
897
+ if (iteratesForward) {
898
+ this.intervalTree.map((interval: TInterval) => {
899
+ if (transientInterval.compareEnd(interval) === 0) {
900
+ results.push(interval);
901
+ }
902
+ });
903
+ } else {
904
+ this.intervalTree.mapBackward((interval: TInterval) => {
905
+ if (transientInterval.compareEnd(interval) === 0) {
906
+ results.push(interval);
907
+ }
908
+ });
909
+ }
910
+ } else {
911
+ // Start and (possibly) end provided. Walk the subtrees that may contain
912
+ // this start position.
913
+ const compareFn =
914
+ end === undefined
915
+ ? (node: IntervalNode<TInterval>) => {
916
+ return transientInterval.compareStart(node.key);
917
+ }
918
+ : (node: IntervalNode<TInterval>) => {
919
+ return transientInterval.compare(node.key);
920
+ };
921
+ const continueLeftFn = (cmpResult: number) => cmpResult <= 0;
922
+ const continueRightFn = (cmpResult: number) => cmpResult >= 0;
923
+ const actionFn = (node: IntervalNode<TInterval>) => {
924
+ results.push(node.key);
925
+ };
926
+
927
+ if (iteratesForward) {
928
+ this.intervalTree.intervals.walkExactMatchesForward(
929
+ compareFn,
930
+ actionFn,
931
+ continueLeftFn,
932
+ continueRightFn,
933
+ );
934
+ } else {
935
+ this.intervalTree.intervals.walkExactMatchesBackward(
936
+ compareFn,
937
+ actionFn,
938
+ continueLeftFn,
939
+ continueRightFn,
940
+ );
941
+ }
942
+ }
943
+ }
944
+ }
945
+
946
+ /**
947
+ * @returns an array of all intervals contained in this collection that overlap the range
948
+ * `[startPosition, endPosition]`.
949
+ */
950
+ public findOverlappingIntervals(startPosition: number, endPosition: number) {
951
+ if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
952
+ return [];
953
+ }
954
+ const transientInterval = this.helpers.create(
955
+ "transient",
956
+ startPosition,
957
+ endPosition,
958
+ this.client,
959
+ IntervalType.Transient,
960
+ );
961
+
962
+ const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
963
+ return overlappingIntervalNodes.map((node) => node.key);
964
+ }
965
+
966
+ public previousInterval(pos: number) {
967
+ const transientInterval = this.helpers.create(
968
+ "transient",
969
+ pos,
970
+ pos,
971
+ this.client,
972
+ IntervalType.Transient,
973
+ );
974
+ const rbNode = this.endIntervalTree.floor(transientInterval);
975
+ if (rbNode) {
976
+ return rbNode.data;
977
+ }
978
+ }
979
+
980
+ public nextInterval(pos: number) {
981
+ const transientInterval = this.helpers.create(
982
+ "transient",
983
+ pos,
984
+ pos,
985
+ this.client,
986
+ IntervalType.Transient,
987
+ );
988
+ const rbNode = this.endIntervalTree.ceil(transientInterval);
989
+ if (rbNode) {
990
+ return rbNode.data;
991
+ }
992
+ }
993
+
994
+ public removeInterval(startPosition: number, endPosition: number) {
995
+ const transientInterval = this.helpers.create(
996
+ "transient",
997
+ startPosition,
998
+ endPosition,
999
+ this.client,
1000
+ IntervalType.Transient,
1001
+ );
1002
+ this.intervalTree.remove(transientInterval);
1003
+ this.endIntervalTree.remove(transientInterval);
1004
+ return transientInterval;
1005
+ }
1006
+
1007
+ private removeIntervalFromIndex(interval: TInterval) {
1008
+ this.intervalTree.removeExisting(interval);
1009
+ this.endIntervalTree.remove(interval);
1010
+
1011
+ const id = interval.getIntervalId();
1012
+
1013
+ assert(id !== undefined, 0x311 /* expected id to exist on interval */);
1014
+
1015
+ this.intervalIdMap.delete(id);
1016
+ }
1017
+
1018
+ public removeExistingInterval(interval: TInterval) {
1019
+ this.removeIntervalFromIndex(interval);
1020
+ this.removeIntervalListeners(interval);
1021
+ }
1022
+
1023
+ public createInterval(
1024
+ start: number,
1025
+ end: number,
1026
+ intervalType: IntervalType,
1027
+ op?: ISequencedDocumentMessage,
1028
+ ): TInterval {
1029
+ return this.helpers.create(this.label, start, end, this.client, intervalType, op);
1030
+ }
1031
+
1032
+ public addInterval(
1033
+ start: number,
1034
+ end: number,
1035
+ intervalType: IntervalType,
1036
+ props?: PropertySet,
1037
+ op?: ISequencedDocumentMessage,
1038
+ ) {
1039
+ const interval: TInterval = this.createInterval(start, end, intervalType, op);
1040
+ if (interval) {
1041
+ if (!interval.properties) {
1042
+ interval.properties = createMap<any>();
1043
+ }
1044
+
1045
+ if (props) {
1046
+ interval.addProperties(props);
1047
+ }
1048
+ interval.properties[reservedIntervalIdKey] ??= uuid();
1049
+ this.add(interval);
1050
+ }
1051
+ return interval;
1052
+ }
1053
+
1054
+ private linkEndpointsToInterval(interval: TInterval): void {
1055
+ if (interval instanceof SequenceInterval) {
1056
+ interval.start.addProperties({ interval });
1057
+ interval.end.addProperties({ interval });
1058
+ }
1059
+ }
1060
+
1061
+ private addIntervalToIndex(interval: TInterval) {
1062
+ const id = interval.getIntervalId();
1063
+ assert(
1064
+ id !== undefined,
1065
+ 0x2c0 /* "ID must be created before adding interval to collection" */,
1066
+ );
1067
+ // Make the ID immutable.
1068
+ Object.defineProperty(interval.properties, reservedIntervalIdKey, {
1069
+ configurable: false,
1070
+ enumerable: true,
1071
+ writable: false,
1072
+ });
1073
+ this.intervalTree.put(interval, this.conflictResolver);
1074
+ this.endIntervalTree.put(interval, interval, this.endConflictResolver);
1075
+ this.intervalIdMap.set(id, interval);
1076
+ }
1077
+
1078
+ public add(interval: TInterval): void {
1079
+ this.linkEndpointsToInterval(interval);
1080
+ this.addIntervalToIndex(interval);
1081
+ this.addIntervalListeners(interval);
1082
+ }
1083
+
1084
+ public getIntervalById(id: string) {
1085
+ return this.intervalIdMap.get(id);
1086
+ }
1087
+
1088
+ public changeInterval(
1089
+ interval: TInterval,
1090
+ start: number | undefined,
1091
+ end: number | undefined,
1092
+ op?: ISequencedDocumentMessage,
1093
+ localSeq?: number,
1094
+ ) {
1095
+ const newInterval = interval.modify(this.label, start, end, op, localSeq) as
1096
+ | TInterval
1097
+ | undefined;
1098
+ if (newInterval) {
1099
+ this.removeExistingInterval(interval);
1100
+ this.add(newInterval);
1101
+ }
1102
+ return newInterval;
1103
+ }
1104
+
1105
+ public serialize(): ISerializedIntervalCollectionV2 {
1106
+ const intervals = this.intervalTree.intervals.keys();
1107
+ return {
1108
+ label: this.label,
1109
+ intervals: intervals.map((interval) => compressInterval(interval.serialize())),
1110
+ version: 2,
1111
+ };
1112
+ }
1113
+
1114
+ private addIntervalListeners(interval: TInterval) {
1115
+ const cloneRef = (ref: LocalReferencePosition) => {
1116
+ const segment = ref.getSegment();
1117
+ if (segment === undefined) {
1118
+ // Cloning is unnecessary: refs which have slid off the string entirely
1119
+ // never get slid back on. Creation code for refs doesn't accept undefined segment
1120
+ // either, so this must be special-cased.
1121
+ return ref;
1122
+ }
1123
+
1124
+ return this.client.createLocalReferencePosition(
1125
+ segment,
1126
+ ref.getOffset(),
1127
+ ReferenceType.Transient,
1128
+ ref.properties,
1129
+ );
1130
+ };
1131
+ if (interval instanceof SequenceInterval) {
1132
+ let previousInterval: (TInterval & SequenceInterval) | undefined;
1133
+ let pendingChanges = 0;
1134
+ interval.addPositionChangeListeners(
1135
+ () => {
1136
+ pendingChanges++;
1137
+ // Note: both start and end can change and invoke beforeSlide on each endpoint before afterSlide.
1138
+ if (!previousInterval) {
1139
+ previousInterval = interval.clone() as TInterval & SequenceInterval;
1140
+ previousInterval.start = cloneRef(previousInterval.start);
1141
+ previousInterval.end = cloneRef(previousInterval.end);
1142
+ this.removeIntervalFromIndex(interval);
1143
+ }
1144
+ },
1145
+ () => {
1146
+ assert(
1147
+ previousInterval !== undefined,
1148
+ 0x3fa /* Invalid interleaving of before/after slide */,
1149
+ );
1150
+ pendingChanges--;
1151
+ if (pendingChanges === 0) {
1152
+ this.addIntervalToIndex(interval);
1153
+ this.onPositionChange?.(interval, previousInterval);
1154
+ previousInterval = undefined;
1155
+ }
1156
+ },
1157
+ );
1158
+ }
1159
+ }
1160
+
1161
+ private removeIntervalListeners(interval: TInterval) {
1162
+ if (interval instanceof SequenceInterval) {
1163
+ interval.removePositionChangeListeners();
1164
+ }
1165
+ }
1077
1166
  }
1078
1167
 
1079
1168
  export const compareSequenceIntervalEnds = (a: SequenceInterval, b: SequenceInterval): number =>
1080
- compareReferencePositions(a.end, b.end);
1169
+ compareReferencePositions(a.end, b.end);
1081
1170
 
1082
1171
  class SequenceIntervalCollectionFactory
1083
- implements IValueFactory<IntervalCollection<SequenceInterval>> {
1084
- public load(
1085
- emitter: IValueOpEmitter,
1086
- raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
1087
- ): IntervalCollection<SequenceInterval> {
1088
- const helpers: IIntervalHelpers<SequenceInterval> = {
1089
- compareEnds: compareSequenceIntervalEnds,
1090
- create: createSequenceInterval,
1091
- };
1092
- return new IntervalCollection<SequenceInterval>(helpers, true, emitter, raw);
1093
- }
1094
-
1095
- public store(value: IntervalCollection<SequenceInterval>): ISerializedInterval[] | ISerializedIntervalCollectionV2 {
1096
- return value.serializeInternal();
1097
- }
1172
+ implements IValueFactory<IntervalCollection<SequenceInterval>>
1173
+ {
1174
+ public load(
1175
+ emitter: IValueOpEmitter,
1176
+ raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
1177
+ ): IntervalCollection<SequenceInterval> {
1178
+ const helpers: IIntervalHelpers<SequenceInterval> = {
1179
+ compareEnds: compareSequenceIntervalEnds,
1180
+ create: createSequenceInterval,
1181
+ };
1182
+ return new IntervalCollection<SequenceInterval>(helpers, true, emitter, raw);
1183
+ }
1184
+
1185
+ public store(
1186
+ value: IntervalCollection<SequenceInterval>,
1187
+ ): ISerializedInterval[] | ISerializedIntervalCollectionV2 {
1188
+ return value.serializeInternal();
1189
+ }
1098
1190
  }
1099
1191
 
1100
1192
  export class SequenceIntervalCollectionValueType
1101
- implements IValueType<IntervalCollection<SequenceInterval>> {
1102
- public static Name = "sharedStringIntervalCollection";
1193
+ implements IValueType<IntervalCollection<SequenceInterval>>
1194
+ {
1195
+ public static Name = "sharedStringIntervalCollection";
1103
1196
 
1104
- public get name(): string {
1105
- return SequenceIntervalCollectionValueType.Name;
1106
- }
1197
+ public get name(): string {
1198
+ return SequenceIntervalCollectionValueType.Name;
1199
+ }
1107
1200
 
1108
- public get factory(): IValueFactory<IntervalCollection<SequenceInterval>> {
1109
- return SequenceIntervalCollectionValueType._factory;
1110
- }
1201
+ public get factory(): IValueFactory<IntervalCollection<SequenceInterval>> {
1202
+ return SequenceIntervalCollectionValueType._factory;
1203
+ }
1111
1204
 
1112
- public get ops(): Map<string, IValueOperation<IntervalCollection<SequenceInterval>>> {
1113
- return SequenceIntervalCollectionValueType._ops;
1114
- }
1205
+ public get ops(): Map<string, IValueOperation<IntervalCollection<SequenceInterval>>> {
1206
+ return SequenceIntervalCollectionValueType._ops;
1207
+ }
1115
1208
 
1116
- private static readonly _factory: IValueFactory<IntervalCollection<SequenceInterval>> =
1117
- new SequenceIntervalCollectionFactory();
1209
+ private static readonly _factory: IValueFactory<IntervalCollection<SequenceInterval>> =
1210
+ new SequenceIntervalCollectionFactory();
1118
1211
 
1119
- private static readonly _ops = makeOpsMap<SequenceInterval>();
1212
+ private static readonly _ops = makeOpsMap<SequenceInterval>();
1120
1213
  }
1121
1214
 
1122
1215
  const compareIntervalEnds = (a: Interval, b: Interval) => a.end - b.end;
1123
1216
 
1124
1217
  function createInterval(label: string, start: number, end: number, client: Client): Interval {
1125
- const rangeProp: PropertySet = {};
1218
+ const rangeProp: PropertySet = {};
1126
1219
 
1127
- if (label && label.length > 0) {
1128
- rangeProp[reservedRangeLabelsKey] = [label];
1129
- }
1220
+ if (label && label.length > 0) {
1221
+ rangeProp[reservedRangeLabelsKey] = [label];
1222
+ }
1130
1223
 
1131
- return new Interval(start, end, rangeProp);
1224
+ return new Interval(start, end, rangeProp);
1132
1225
  }
1133
1226
 
1134
- class IntervalCollectionFactory
1135
- implements IValueFactory<IntervalCollection<Interval>> {
1136
- public load(
1137
- emitter: IValueOpEmitter,
1138
- raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
1139
- ): IntervalCollection<Interval> {
1140
- const helpers: IIntervalHelpers<Interval> = {
1141
- compareEnds: compareIntervalEnds,
1142
- create: createInterval,
1143
- };
1144
- const collection = new IntervalCollection<Interval>(helpers, false, emitter, raw);
1145
- collection.attachGraph(undefined as any as Client, "");
1146
- return collection;
1147
- }
1148
-
1149
- public store(value: IntervalCollection<Interval>): ISerializedIntervalCollectionV2 {
1150
- return value.serializeInternal();
1151
- }
1227
+ class IntervalCollectionFactory implements IValueFactory<IntervalCollection<Interval>> {
1228
+ public load(
1229
+ emitter: IValueOpEmitter,
1230
+ raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
1231
+ ): IntervalCollection<Interval> {
1232
+ const helpers: IIntervalHelpers<Interval> = {
1233
+ compareEnds: compareIntervalEnds,
1234
+ create: createInterval,
1235
+ };
1236
+ const collection = new IntervalCollection<Interval>(helpers, false, emitter, raw);
1237
+ collection.attachGraph(undefined as any as Client, "");
1238
+ return collection;
1239
+ }
1240
+
1241
+ public store(value: IntervalCollection<Interval>): ISerializedIntervalCollectionV2 {
1242
+ return value.serializeInternal();
1243
+ }
1152
1244
  }
1153
1245
 
1154
- export class IntervalCollectionValueType
1155
- implements IValueType<IntervalCollection<Interval>> {
1156
- public static Name = "sharedIntervalCollection";
1246
+ export class IntervalCollectionValueType implements IValueType<IntervalCollection<Interval>> {
1247
+ public static Name = "sharedIntervalCollection";
1157
1248
 
1158
- public get name(): string {
1159
- return IntervalCollectionValueType.Name;
1160
- }
1249
+ public get name(): string {
1250
+ return IntervalCollectionValueType.Name;
1251
+ }
1161
1252
 
1162
- public get factory(): IValueFactory<IntervalCollection<Interval>> {
1163
- return IntervalCollectionValueType._factory;
1164
- }
1253
+ public get factory(): IValueFactory<IntervalCollection<Interval>> {
1254
+ return IntervalCollectionValueType._factory;
1255
+ }
1165
1256
 
1166
- public get ops(): Map<string, IValueOperation<IntervalCollection<Interval>>> {
1167
- return IntervalCollectionValueType._ops;
1168
- }
1257
+ public get ops(): Map<string, IValueOperation<IntervalCollection<Interval>>> {
1258
+ return IntervalCollectionValueType._ops;
1259
+ }
1169
1260
 
1170
- private static readonly _factory: IValueFactory<IntervalCollection<Interval>> =
1171
- new IntervalCollectionFactory();
1172
- private static readonly _ops = makeOpsMap<Interval>();
1261
+ private static readonly _factory: IValueFactory<IntervalCollection<Interval>> =
1262
+ new IntervalCollectionFactory();
1263
+ private static readonly _ops = makeOpsMap<Interval>();
1173
1264
  }
1174
1265
 
1175
- export function makeOpsMap<T extends ISerializableInterval>(): Map<string, IValueOperation<IntervalCollection<T>>> {
1176
- const rebase = (
1177
- collection: IntervalCollection<T>,
1178
- op: IValueTypeOperationValue,
1179
- localOpMetadata: IMapMessageLocalMetadata,
1180
- ) => {
1181
- const { localSeq } = localOpMetadata;
1182
- const rebasedValue = collection.rebaseLocalInterval(op.opName, op.value, localSeq);
1183
- const rebasedOp = { ...op, value: rebasedValue };
1184
- return { rebasedOp, rebasedLocalOpMetadata: localOpMetadata };
1185
- };
1186
-
1187
- return new Map<string, IValueOperation<IntervalCollection<T>>>(
1188
- [[
1189
- "add",
1190
- {
1191
- process: (collection, params, local, op, localOpMetadata) => {
1192
- // if params is undefined, the interval was deleted during
1193
- // rebasing
1194
- if (!params) {
1195
- return;
1196
- }
1197
- assert(op !== undefined, 0x3fb /* op should exist here */);
1198
- collection.ackAdd(params, local, op, localOpMetadata);
1199
- },
1200
- rebase,
1201
- },
1202
- ],
1203
- [
1204
- "delete",
1205
- {
1206
- process: (collection, params, local, op) => {
1207
- assert(op !== undefined, 0x3fc /* op should exist here */);
1208
- collection.ackDelete(params, local, op);
1209
- },
1210
- rebase: (collection, op, localOpMetadata) => {
1211
- // Deletion of intervals is based on id, so requires no rebasing.
1212
- return { rebasedOp: op, rebasedLocalOpMetadata: localOpMetadata };
1213
- },
1214
- },
1215
- ],
1216
- [
1217
- "change",
1218
- {
1219
- process: (collection, params, local, op, localOpMetadata) => {
1220
- // if params is undefined, the interval was deleted during
1221
- // rebasing
1222
- if (!params) {
1223
- return;
1224
- }
1225
- assert(op !== undefined, 0x3fd /* op should exist here */);
1226
- collection.ackChange(params, local, op, localOpMetadata);
1227
- },
1228
- rebase,
1229
- },
1230
- ]]);
1266
+ export function makeOpsMap<T extends ISerializableInterval>(): Map<
1267
+ string,
1268
+ IValueOperation<IntervalCollection<T>>
1269
+ > {
1270
+ const rebase = (
1271
+ collection: IntervalCollection<T>,
1272
+ op: IValueTypeOperationValue,
1273
+ localOpMetadata: IMapMessageLocalMetadata,
1274
+ ) => {
1275
+ const { localSeq } = localOpMetadata;
1276
+ const rebasedValue = collection.rebaseLocalInterval(op.opName, op.value, localSeq);
1277
+ const rebasedOp = { ...op, value: rebasedValue };
1278
+ return { rebasedOp, rebasedLocalOpMetadata: localOpMetadata };
1279
+ };
1280
+
1281
+ return new Map<string, IValueOperation<IntervalCollection<T>>>([
1282
+ [
1283
+ "add",
1284
+ {
1285
+ process: (collection, params, local, op, localOpMetadata) => {
1286
+ // if params is undefined, the interval was deleted during
1287
+ // rebasing
1288
+ if (!params) {
1289
+ return;
1290
+ }
1291
+ assert(op !== undefined, 0x3fb /* op should exist here */);
1292
+ collection.ackAdd(params, local, op, localOpMetadata);
1293
+ },
1294
+ rebase,
1295
+ },
1296
+ ],
1297
+ [
1298
+ "delete",
1299
+ {
1300
+ process: (collection, params, local, op) => {
1301
+ assert(op !== undefined, 0x3fc /* op should exist here */);
1302
+ collection.ackDelete(params, local, op);
1303
+ },
1304
+ rebase: (collection, op, localOpMetadata) => {
1305
+ // Deletion of intervals is based on id, so requires no rebasing.
1306
+ return { rebasedOp: op, rebasedLocalOpMetadata: localOpMetadata };
1307
+ },
1308
+ },
1309
+ ],
1310
+ [
1311
+ "change",
1312
+ {
1313
+ process: (collection, params, local, op, localOpMetadata) => {
1314
+ // if params is undefined, the interval was deleted during
1315
+ // rebasing
1316
+ if (!params) {
1317
+ return;
1318
+ }
1319
+ assert(op !== undefined, 0x3fd /* op should exist here */);
1320
+ collection.ackChange(params, local, op, localOpMetadata);
1321
+ },
1322
+ rebase,
1323
+ },
1324
+ ],
1325
+ ]);
1231
1326
  }
1232
1327
 
1233
1328
  export type DeserializeCallback = (properties: PropertySet) => void;
1234
1329
 
1235
1330
  export class IntervalCollectionIterator<TInterval extends ISerializableInterval>
1236
- implements Iterator<TInterval> {
1237
- private readonly results: TInterval[];
1238
- private index: number;
1239
-
1240
- constructor(
1241
- collection: IntervalCollection<TInterval>,
1242
- iteratesForward: boolean = true,
1243
- start?: number,
1244
- end?: number) {
1245
- this.results = [];
1246
- this.index = 0;
1247
-
1248
- collection.gatherIterationResults(this.results, iteratesForward, start, end);
1249
- }
1250
-
1251
- public next(): IteratorResult<TInterval> {
1252
- if (this.index < this.results.length) {
1253
- return {
1254
- value: this.results[this.index++],
1255
- done: false,
1256
- };
1257
- }
1258
-
1259
- return {
1260
- value: undefined,
1261
- done: true,
1262
- };
1263
- }
1331
+ implements Iterator<TInterval>
1332
+ {
1333
+ private readonly results: TInterval[];
1334
+ private index: number;
1335
+
1336
+ constructor(
1337
+ collection: IntervalCollection<TInterval>,
1338
+ iteratesForward: boolean = true,
1339
+ start?: number,
1340
+ end?: number,
1341
+ ) {
1342
+ this.results = [];
1343
+ this.index = 0;
1344
+
1345
+ collection.gatherIterationResults(this.results, iteratesForward, start, end);
1346
+ }
1347
+
1348
+ public next(): IteratorResult<TInterval> {
1349
+ if (this.index < this.results.length) {
1350
+ return {
1351
+ value: this.results[this.index++],
1352
+ done: false,
1353
+ };
1354
+ }
1355
+
1356
+ return {
1357
+ value: undefined,
1358
+ done: true,
1359
+ };
1360
+ }
1264
1361
  }
1265
1362
 
1266
1363
  /**
1267
1364
  * Change events emitted by `IntervalCollection`s
1268
1365
  */
1269
1366
  export interface IIntervalCollectionEvent<TInterval extends ISerializableInterval> extends IEvent {
1270
- /**
1271
- * This event is invoked whenever the endpoints of an interval may have changed.
1272
- * This can happen on:
1273
- * - local endpoint modification
1274
- * - ack of a remote endpoint modification
1275
- * - position change due to segment sliding (slides due to mergeTree segment deletion will always appear local)
1276
- * The `interval` argument reflects the new values.
1277
- * `previousInterval` contains transient `ReferencePosition`s at the same location as the interval's original
1278
- * endpoints. These references should be used for position information only.
1279
- * `local` reflects whether the change originated locally.
1280
- * `op` is defined if and only if the server has acked this change.
1281
- */
1282
- (event: "changeInterval",
1283
- listener: (
1284
- interval: TInterval,
1285
- previousInterval: TInterval,
1286
- local: boolean,
1287
- op: ISequencedDocumentMessage | undefined
1288
- ) => void);
1289
- /**
1290
- * This event is invoked whenever an interval is added or removed from the collection.
1291
- * `local` reflects whether the change originated locally.
1292
- * `op` is defined if and only if the server has acked this change.
1293
- */
1294
- (event: "addInterval" | "deleteInterval",
1295
- listener: (interval: TInterval, local: boolean, op: ISequencedDocumentMessage | undefined) => void);
1296
- /**
1297
- * This event is invoked whenever an interval's properties have changed.
1298
- * `interval` reflects the state of the updated properties.
1299
- * `propertyDeltas` is a map-like whose keys contain all values that were changed, and whose
1300
- * values contain all previous values of the property set.
1301
- * This object can be used directly in a call to `changeProperties` to revert the property change if desired.
1302
- * `local` reflects whether the change originated locally.
1303
- * `op` is defined if and only if the server has acked this change.
1304
- */
1305
- (event: "propertyChanged",
1306
- listener: (
1307
- interval: TInterval,
1308
- propertyDeltas: PropertySet,
1309
- local: boolean,
1310
- op: ISequencedDocumentMessage | undefined
1311
- ) => void);
1367
+ /**
1368
+ * This event is invoked whenever the endpoints of an interval may have changed.
1369
+ * This can happen on:
1370
+ * - local endpoint modification
1371
+ * - ack of a remote endpoint modification
1372
+ * - position change due to segment sliding (slides due to mergeTree segment deletion will always appear local)
1373
+ * The `interval` argument reflects the new values.
1374
+ * `previousInterval` contains transient `ReferencePosition`s at the same location as the interval's original
1375
+ * endpoints. These references should be used for position information only.
1376
+ * `local` reflects whether the change originated locally.
1377
+ * `op` is defined if and only if the server has acked this change.
1378
+ */
1379
+ (
1380
+ event: "changeInterval",
1381
+ listener: (
1382
+ interval: TInterval,
1383
+ previousInterval: TInterval,
1384
+ local: boolean,
1385
+ op: ISequencedDocumentMessage | undefined,
1386
+ ) => void,
1387
+ );
1388
+ /**
1389
+ * This event is invoked whenever an interval is added or removed from the collection.
1390
+ * `local` reflects whether the change originated locally.
1391
+ * `op` is defined if and only if the server has acked this change.
1392
+ */
1393
+ (
1394
+ event: "addInterval" | "deleteInterval",
1395
+ listener: (
1396
+ interval: TInterval,
1397
+ local: boolean,
1398
+ op: ISequencedDocumentMessage | undefined,
1399
+ ) => void,
1400
+ );
1401
+ /**
1402
+ * This event is invoked whenever an interval's properties have changed.
1403
+ * `interval` reflects the state of the updated properties.
1404
+ * `propertyDeltas` is a map-like whose keys contain all values that were changed, and whose
1405
+ * values contain all previous values of the property set.
1406
+ * This object can be used directly in a call to `changeProperties` to revert the property change if desired.
1407
+ * `local` reflects whether the change originated locally.
1408
+ * `op` is defined if and only if the server has acked this change.
1409
+ */
1410
+ (
1411
+ event: "propertyChanged",
1412
+ listener: (
1413
+ interval: TInterval,
1414
+ propertyDeltas: PropertySet,
1415
+ local: boolean,
1416
+ op: ISequencedDocumentMessage | undefined,
1417
+ ) => void,
1418
+ );
1312
1419
  }
1313
1420
 
1314
1421
  /**
@@ -1318,838 +1425,930 @@ export interface IIntervalCollectionEvent<TInterval extends ISerializableInterva
1318
1425
  * This aligns with its usage in `SharedSegmentSequence`, which allows associating intervals to positions in the
1319
1426
  * sequence DDS which are broadcast to all other clients in an eventually consistent fashion.
1320
1427
  */
1321
- export class IntervalCollection<TInterval extends ISerializableInterval>
1322
- extends TypedEventEmitter<IIntervalCollectionEvent<TInterval>> {
1323
- private savedSerializedIntervals?: ISerializedInterval[];
1324
- private localCollection: LocalIntervalCollection<TInterval> | undefined;
1325
- private onDeserialize: DeserializeCallback | undefined;
1326
- private client: Client | undefined;
1327
- private readonly localSeqToSerializedInterval = new Map<number, ISerializedInterval | SerializedIntervalDelta>();
1328
- private readonly localSeqToRebasedInterval = new Map<number, ISerializedInterval | SerializedIntervalDelta>();
1329
- private readonly pendingChangesStart: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
1330
- private readonly pendingChangesEnd: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
1331
-
1332
- public get attached(): boolean {
1333
- return !!this.localCollection;
1334
- }
1335
-
1336
- /** @internal */
1337
- constructor(
1338
- private readonly helpers: IIntervalHelpers<TInterval>,
1339
- private readonly requiresClient: boolean,
1340
- private readonly emitter: IValueOpEmitter,
1341
- serializedIntervals: ISerializedInterval[] | ISerializedIntervalCollectionV2,
1342
- ) {
1343
- super();
1344
-
1345
- this.savedSerializedIntervals = Array.isArray(serializedIntervals)
1346
- ? serializedIntervals
1347
- : serializedIntervals.intervals.map((i) => decompressInterval(i, serializedIntervals.label));
1348
- }
1349
-
1350
- private rebasePositionWithSegmentSlide(
1351
- pos: number,
1352
- seqNumberFrom: number,
1353
- localSeq: number
1354
- ): number | undefined {
1355
- if (!this.client) {
1356
- throw new LoggingError("mergeTree client must exist");
1357
- }
1358
- const { clientId } = this.client.getCollabWindow();
1359
- const { segment, offset } = this.client.getContainingSegment(pos, { referenceSequenceNumber: seqNumberFrom, clientId: this.client.getLongClientId(clientId) }, localSeq);
1360
-
1361
- // if segment is undefined, it slid off the string
1362
- assert(segment !== undefined, 0x54e /* No segment found */);
1363
-
1364
- const segoff = this.client.getSlideToSegment({ segment, offset }) ?? segment;
1365
-
1366
- // case happens when rebasing op, but concurrently entire string has been deleted
1367
- if (segoff.segment === undefined || segoff.offset === undefined) {
1368
- return DetachedReferencePosition;
1369
- }
1370
-
1371
- assert(offset !== undefined && 0 <= offset && offset < segment.cachedLength, 0x54f /* Invalid offset */);
1372
- return this.client.findReconnectionPosition(segoff.segment, localSeq) + segoff.offset;
1373
- }
1374
-
1375
- private computeRebasedPositions(localSeq: number): ISerializedInterval | SerializedIntervalDelta {
1376
- assert(this.client !== undefined, 0x550 /* Client should be defined when computing rebased position */);
1377
- const original = this.localSeqToSerializedInterval.get(localSeq);
1378
- assert(original !== undefined, 0x551 /* Failed to store pending serialized interval info for this localSeq. */);
1379
- const rebased = { ...original };
1380
- const { start, end, sequenceNumber } = original;
1381
- if (start !== undefined) {
1382
- rebased.start = this.rebasePositionWithSegmentSlide(start, sequenceNumber, localSeq);
1383
- }
1384
- if (end !== undefined) {
1385
- rebased.end = this.rebasePositionWithSegmentSlide(end, sequenceNumber, localSeq);
1386
- }
1387
- return rebased;
1388
- }
1389
-
1390
- /** @internal */
1391
- public attachGraph(client: Client, label: string) {
1392
- if (this.attached) {
1393
- throw new LoggingError("Only supports one Sequence attach");
1394
- }
1395
-
1396
- if ((client === undefined) && (this.requiresClient)) {
1397
- throw new LoggingError("Client required for this collection");
1398
- }
1399
-
1400
- // Instantiate the local interval collection based on the saved intervals
1401
- this.client = client;
1402
- if (client) {
1403
- client.on("normalize", () => {
1404
- for (const localSeq of this.localSeqToSerializedInterval.keys()) {
1405
- this.localSeqToRebasedInterval.set(localSeq, this.computeRebasedPositions(localSeq));
1406
- }
1407
- });
1408
- }
1409
-
1410
- this.localCollection = new LocalIntervalCollection<TInterval>(
1411
- client,
1412
- label,
1413
- this.helpers,
1414
- (interval, previousInterval) => this.emitChange(interval, previousInterval, true),
1415
- );
1416
- if (this.savedSerializedIntervals) {
1417
- for (const serializedInterval of this.savedSerializedIntervals) {
1418
- this.localCollection.ensureSerializedId(serializedInterval);
1419
- const { start, end, intervalType, properties } = serializedInterval;
1420
- const interval = this.helpers.create(
1421
- label,
1422
- start,
1423
- end,
1424
- client,
1425
- intervalType,
1426
- undefined,
1427
- true,
1428
- );
1429
- if (properties) {
1430
- interval.addProperties(properties);
1431
- }
1432
- this.localCollection.add(interval);
1433
- }
1434
- }
1435
- this.savedSerializedIntervals = undefined;
1436
- }
1437
-
1438
- /**
1439
- * Gets the next local sequence number, modifying this client's collab window in doing so.
1440
- */
1441
- private getNextLocalSeq(): number {
1442
- if (this.client) {
1443
- return ++this.client.getCollabWindow().localSeq;
1444
- }
1445
-
1446
- return 0;
1447
- }
1448
-
1449
- private emitChange(
1450
- interval: TInterval,
1451
- previousInterval: TInterval,
1452
- local: boolean,
1453
- op?: ISequencedDocumentMessage,
1454
- ): void {
1455
- // Temporarily make references transient so that positional queries work (non-transient refs
1456
- // on resolve to DetachedPosition on any segments that don't contain them). The original refType
1457
- // is restored as single-endpoint changes re-use previous references.
1458
- let startRefType: ReferenceType;
1459
- let endRefType: ReferenceType;
1460
- if (previousInterval instanceof SequenceInterval) {
1461
- startRefType = previousInterval.start.refType;
1462
- endRefType = previousInterval.end.refType;
1463
- previousInterval.start.refType = ReferenceType.Transient;
1464
- previousInterval.end.refType = ReferenceType.Transient;
1465
- this.emit("changeInterval", interval, previousInterval, local, op);
1466
- previousInterval.start.refType = startRefType;
1467
- previousInterval.end.refType = endRefType;
1468
- } else {
1469
- this.emit("changeInterval", interval, previousInterval, local, op);
1470
- }
1471
- }
1472
-
1473
- /**
1474
- * @returns the interval in this collection that has the provided `id`.
1475
- * If no interval in the collection has this `id`, returns `undefined`.
1476
- */
1477
- public getIntervalById(id: string) {
1478
- if (!this.localCollection) {
1479
- throw new LoggingError("attach must be called before accessing intervals");
1480
- }
1481
- return this.localCollection.getIntervalById(id);
1482
- }
1483
-
1484
- /**
1485
- * Creates a new interval and add it to the collection.
1486
- * @param start - interval start position
1487
- * @param end - interval end position
1488
- * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
1489
- * @param props - properties of the interval
1490
- * @returns - the created interval
1491
- */
1492
- public add(
1493
- start: number,
1494
- end: number,
1495
- intervalType: IntervalType,
1496
- props?: PropertySet,
1497
- ): TInterval {
1498
- if (!this.localCollection) {
1499
- throw new LoggingError("attach must be called prior to adding intervals");
1500
- }
1501
- if (intervalType & IntervalType.Transient) {
1502
- throw new LoggingError("Can not add transient intervals");
1503
- }
1504
-
1505
- const interval: TInterval = this.localCollection.addInterval(start, end, intervalType, props);
1506
-
1507
- if (interval) {
1508
- const serializedInterval = {
1509
- end,
1510
- intervalType,
1511
- properties: interval.properties,
1512
- sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1513
- start,
1514
- };
1515
- const localSeq = this.getNextLocalSeq();
1516
- this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1517
- // Local ops get submitted to the server. Remote ops have the deserializer run.
1518
- this.emitter.emit("add", undefined, serializedInterval, { localSeq });
1519
- }
1520
-
1521
- this.emit("addInterval", interval, true, undefined);
1522
-
1523
- return interval;
1524
- }
1525
-
1526
- private deleteExistingInterval(interval: TInterval, local: boolean, op?: ISequencedDocumentMessage) {
1527
- if (!this.localCollection) {
1528
- throw new LoggingError("Attach must be called before accessing intervals");
1529
- }
1530
- // The given interval is known to exist in the collection.
1531
- this.localCollection.removeExistingInterval(interval);
1532
-
1533
- if (interval) {
1534
- // Local ops get submitted to the server. Remote ops have the deserializer run.
1535
- if (local) {
1536
- this.emitter.emit(
1537
- "delete",
1538
- undefined,
1539
- interval.serialize(),
1540
- { localSeq: this.getNextLocalSeq() },
1541
- );
1542
- } else {
1543
- if (this.onDeserialize) {
1544
- this.onDeserialize(interval);
1545
- }
1546
- }
1547
- }
1548
-
1549
- this.emit("deleteInterval", interval, local, op);
1550
- }
1551
-
1552
- /**
1553
- * Removes an interval from the collection.
1554
- * @param id - Id of the interval to remove
1555
- * @returns the removed interval
1556
- */
1557
- public removeIntervalById(id: string) {
1558
- if (!this.localCollection) {
1559
- throw new LoggingError("Attach must be called before accessing intervals");
1560
- }
1561
- const interval = this.localCollection.getIntervalById(id);
1562
- if (interval) {
1563
- this.deleteExistingInterval(interval, true, undefined);
1564
- }
1565
- return interval;
1566
- }
1567
-
1568
- /**
1569
- * Changes the properties on an existing interval.
1570
- * @param id - Id of the interval whose properties should be changed
1571
- * @param props - Property set to apply to the interval. Shallow merging is used between any existing properties
1572
- * and `prop`, i.e. the interval will end up with a property object equivalent to `{ ...oldProps, ...props }`.
1573
- */
1574
- public changeProperties(id: string, props: PropertySet) {
1575
- if (!this.attached) {
1576
- throw new LoggingError("Attach must be called before accessing intervals");
1577
- }
1578
- if (typeof (id) !== "string") {
1579
- throw new LoggingError("Change API requires an ID that is a string");
1580
- }
1581
- if (!props) {
1582
- throw new LoggingError("changeProperties should be called with a property set");
1583
- }
1584
-
1585
- const interval = this.getIntervalById(id);
1586
- if (interval) {
1587
- // Pass Unassigned as the sequence number to indicate that this is a local op that is waiting for an ack.
1588
- const deltaProps = interval.addProperties(props, true, UnassignedSequenceNumber);
1589
- const serializedInterval: ISerializedInterval = interval.serialize();
1590
-
1591
- // Emit a change op that will only change properties. Add the ID to
1592
- // the property bag provided by the caller.
1593
- serializedInterval.start = undefined as any;
1594
- serializedInterval.end = undefined as any;
1595
-
1596
- serializedInterval.properties = props;
1597
- serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
1598
- const localSeq = this.getNextLocalSeq();
1599
- this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1600
- this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1601
- this.emit("propertyChanged", interval, deltaProps, true, undefined);
1602
- }
1603
- }
1604
-
1605
- /**
1606
- * Changes the endpoints of an existing interval.
1607
- * @param id - Id of the interval to change
1608
- * @param start - New start value, if defined. `undefined` signifies this endpoint should be left unchanged.
1609
- * @param end - New end value, if defined. `undefined` signifies this endpoint should be left unchanged.
1610
- * @returns the interval that was changed, if it existed in the collection.
1611
- */
1612
- public change(id: string, start?: number, end?: number): TInterval | undefined {
1613
- if (!this.localCollection) {
1614
- throw new LoggingError("Attach must be called before accessing intervals");
1615
- }
1616
-
1617
- // Force id to be a string.
1618
- if (typeof (id) !== "string") {
1619
- throw new LoggingError("Change API requires an ID that is a string");
1620
- }
1621
-
1622
- const interval = this.getIntervalById(id);
1623
- if (interval) {
1624
- const newInterval = this.localCollection.changeInterval(interval, start, end);
1625
- if (!newInterval) {
1626
- return undefined;
1627
- }
1628
- const serializedInterval: SerializedIntervalDelta = interval.serialize();
1629
- serializedInterval.start = start;
1630
- serializedInterval.end = end;
1631
- // Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
1632
- serializedInterval.properties =
1633
- {
1634
- [reservedIntervalIdKey]: interval.getIntervalId(),
1635
- };
1636
- const localSeq = this.getNextLocalSeq();
1637
- this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1638
- this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1639
- this.addPendingChange(id, serializedInterval);
1640
- this.emitChange(newInterval, interval, true);
1641
- return newInterval;
1642
- }
1643
- // No interval to change
1644
- return undefined;
1645
- }
1646
-
1647
- private addPendingChange(id: string, serializedInterval: SerializedIntervalDelta) {
1648
- if (serializedInterval.start !== undefined) {
1649
- this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1650
- }
1651
- if (serializedInterval.end !== undefined) {
1652
- this.addPendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
1653
- }
1654
- }
1655
-
1656
- private addPendingChangeHelper(
1657
- id: string,
1658
- pendingChanges: Map<string, SerializedIntervalDelta[]>,
1659
- serializedInterval: SerializedIntervalDelta,
1660
- ) {
1661
- let entries: SerializedIntervalDelta[] | undefined = pendingChanges.get(id);
1662
- if (!entries) {
1663
- entries = [];
1664
- pendingChanges.set(id, entries);
1665
- }
1666
- entries.push(serializedInterval);
1667
- }
1668
-
1669
- private removePendingChange(serializedInterval: SerializedIntervalDelta) {
1670
- // Change ops always have an ID.
1671
- const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1672
- if (serializedInterval.start !== undefined) {
1673
- this.removePendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1674
- }
1675
- if (serializedInterval.end !== undefined) {
1676
- this.removePendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
1677
- }
1678
- }
1679
-
1680
- private removePendingChangeHelper(
1681
- id: string,
1682
- pendingChanges: Map<string, SerializedIntervalDelta[]>,
1683
- serializedInterval: SerializedIntervalDelta,
1684
- ) {
1685
- const entries = pendingChanges.get(id);
1686
- if (entries) {
1687
- const pendingChange = entries.shift();
1688
- if (entries.length === 0) {
1689
- pendingChanges.delete(id);
1690
- }
1691
- if (pendingChange?.start !== serializedInterval.start ||
1692
- pendingChange?.end !== serializedInterval.end) {
1693
- throw new LoggingError("Mismatch in pending changes");
1694
- }
1695
- }
1696
- }
1697
-
1698
- private hasPendingChangeStart(id: string) {
1699
- const entries = this.pendingChangesStart.get(id);
1700
- return entries && entries.length !== 0;
1701
- }
1702
-
1703
- private hasPendingChangeEnd(id: string) {
1704
- const entries = this.pendingChangesEnd.get(id);
1705
- return entries && entries.length !== 0;
1706
- }
1707
-
1708
- /** @internal */
1709
- public ackChange(
1710
- serializedInterval: ISerializedInterval,
1711
- local: boolean,
1712
- op: ISequencedDocumentMessage,
1713
- localOpMetadata: IMapMessageLocalMetadata | undefined,
1714
- ) {
1715
- if (!this.localCollection) {
1716
- throw new LoggingError("Attach must be called before accessing intervals");
1717
- }
1718
-
1719
- if (local) {
1720
- assert(localOpMetadata !== undefined, 0x552 /* op metadata should be defined for local op */);
1721
- this.localSeqToSerializedInterval.delete(localOpMetadata?.localSeq);
1722
- // This is an ack from the server. Remove the pending change.
1723
- this.removePendingChange(serializedInterval);
1724
- }
1725
-
1726
- // Note that the ID is in the property bag only to allow us to find the interval.
1727
- // This API cannot change the ID, and writing to the ID property will result in an exception. So we
1728
- // strip it out of the properties here.
1729
- const { [reservedIntervalIdKey]: id, ...newProps } = serializedInterval.properties ?? {};
1730
- assert(id !== undefined, 0x3fe /* id must exist on the interval */);
1731
- const interval: TInterval | undefined = this.getIntervalById(id);
1732
- if (!interval) {
1733
- // The interval has been removed locally; no-op.
1734
- return;
1735
- }
1736
-
1737
- if (local) {
1738
- // Let the propertyManager prune its pending change-properties set.
1739
- interval.propertyManager?.ackPendingProperties(
1740
- {
1741
- type: MergeTreeDeltaType.ANNOTATE,
1742
- props: serializedInterval.properties ?? {},
1743
- });
1744
-
1745
- this.ackInterval(interval, op);
1746
- } else {
1747
- // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
1748
- // should be the winning change.
1749
- let start: number | undefined;
1750
- let end: number | undefined;
1751
- // Track pending start/end independently of one another.
1752
- if (!this.hasPendingChangeStart(id)) {
1753
- start = serializedInterval.start;
1754
- }
1755
- if (!this.hasPendingChangeEnd(id)) {
1756
- end = serializedInterval.end;
1757
- }
1758
-
1759
- let newInterval = interval;
1760
- if (start !== undefined || end !== undefined) {
1761
- // If changeInterval gives us a new interval, work with that one. Otherwise keep working with
1762
- // the one we originally found in the tree.
1763
- newInterval = this.localCollection.changeInterval(interval, start, end, op) ?? interval;
1764
- }
1765
- const deltaProps = newInterval.addProperties(newProps, true, op.sequenceNumber);
1766
- if (this.onDeserialize) {
1767
- this.onDeserialize(newInterval);
1768
- }
1769
-
1770
- if (newInterval !== interval) {
1771
- this.emitChange(newInterval, interval, local, op);
1772
- }
1773
-
1774
- const changedProperties = Object.keys(newProps).length > 0;
1775
- if (changedProperties) {
1776
- this.emit("propertyChanged", interval, deltaProps, local, op);
1777
- }
1778
- }
1779
- }
1780
-
1781
- public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>): void {
1782
- if (!this.localCollection) {
1783
- throw new LoggingError("attachSequence must be called");
1784
- }
1785
- this.localCollection.addConflictResolver(conflictResolver);
1786
- }
1787
-
1788
- public attachDeserializer(onDeserialize: DeserializeCallback): void {
1789
- // If no deserializer is specified can skip all processing work
1790
- if (!onDeserialize) {
1791
- return;
1792
- }
1793
-
1794
- // Start by storing the callbacks so that any subsequent modifications make use of them
1795
- this.onDeserialize = onDeserialize;
1796
-
1797
- // Trigger the async prepare work across all values in the collection
1798
- this.localCollection?.map((interval) => {
1799
- onDeserialize(interval);
1800
- });
1801
- }
1802
-
1803
- /**
1804
- * Returns new interval after rebasing. If undefined, the interval was
1805
- * deleted as a result of rebasing. This can occur if the interval applies
1806
- * to a range that no longer exists, and the interval was unable to slide.
1807
- *
1808
- * @internal
1809
- */
1810
- public rebaseLocalInterval(
1811
- opName: string,
1812
- serializedInterval: SerializedIntervalDelta,
1813
- localSeq: number,
1814
- ): SerializedIntervalDelta | undefined {
1815
- if (!this.client) {
1816
- // If there's no associated mergeTree client, the originally submitted op is still correct.
1817
- return serializedInterval;
1818
- }
1819
- if (!this.attached) {
1820
- throw new LoggingError("attachSequence must be called");
1821
- }
1822
-
1823
- const { intervalType, properties } = serializedInterval;
1824
-
1825
- const { start: startRebased, end: endRebased } = this.localSeqToRebasedInterval.get(localSeq)
1826
- ?? this.computeRebasedPositions(localSeq);
1827
-
1828
- const intervalId = properties?.[reservedIntervalIdKey];
1829
- const localInterval = this.localCollection?.getIntervalById(intervalId);
1830
-
1831
- const rebased: SerializedIntervalDelta = {
1832
- start: startRebased,
1833
- end: endRebased,
1834
- intervalType,
1835
- sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1836
- properties,
1837
- };
1838
-
1839
- if (opName === "change" && (this.hasPendingChangeStart(intervalId) || this.hasPendingChangeEnd(intervalId))) {
1840
- this.removePendingChange(serializedInterval);
1841
- this.addPendingChange(intervalId, rebased);
1842
- }
1843
-
1844
- // if the interval slid off the string, rebase the op to be a noop and delete the interval.
1845
- if (startRebased === DetachedReferencePosition || endRebased === DetachedReferencePosition) {
1846
- if (localInterval) {
1847
- this.localCollection?.removeExistingInterval(localInterval);
1848
- }
1849
- return undefined;
1850
- }
1851
-
1852
- if (localInterval !== undefined) {
1853
- // we know we must be using `SequenceInterval` because `this.client` exists
1854
- assert(
1855
- localInterval instanceof SequenceInterval,
1856
- 0x3a0 /* localInterval must be `SequenceInterval` when used with client */,
1857
- );
1858
- // The rebased op may place this interval's endpoints on different segments. Calling `changeInterval` here
1859
- // updates the local client's state to be consistent with the emitted op.
1860
- this.localCollection?.changeInterval(localInterval, startRebased, endRebased, undefined, localSeq);
1861
- }
1862
-
1863
- return rebased;
1864
- }
1865
-
1866
- private getSlideToSegment(lref: LocalReferencePosition) {
1867
- if (!this.client) {
1868
- throw new LoggingError("client does not exist");
1869
- }
1870
- const segoff = { segment: lref.getSegment(), offset: lref.getOffset() };
1871
- if (segoff.segment?.localRefs?.has(lref) !== true) {
1872
- return undefined;
1873
- }
1874
- const newSegoff = this.client.getSlideToSegment(segoff);
1875
- const value: { segment: ISegment | undefined; offset: number | undefined; } | undefined
1876
- = (segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset) ? undefined : newSegoff;
1877
- return value;
1878
- }
1879
-
1880
- private setSlideOnRemove(lref: LocalReferencePosition) {
1881
- let refType = lref.refType;
1882
- refType = refType & ~ReferenceType.StayOnRemove;
1883
- refType = refType | ReferenceType.SlideOnRemove;
1884
- lref.refType = refType;
1885
- }
1886
-
1887
- private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
1888
- // Only SequenceIntervals need potential sliding
1889
- if (!(interval instanceof SequenceInterval)) {
1890
- return;
1891
- }
1892
-
1893
- if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove) &&
1894
- !refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove)) {
1895
- return;
1896
- }
1897
-
1898
- const newStart = this.getSlideToSegment(interval.start);
1899
- const newEnd = this.getSlideToSegment(interval.end);
1900
-
1901
- const id = interval.properties[reservedIntervalIdKey];
1902
- const hasPendingStartChange = this.hasPendingChangeStart(id);
1903
- const hasPendingEndChange = this.hasPendingChangeEnd(id);
1904
-
1905
- if (!hasPendingStartChange) {
1906
- this.setSlideOnRemove(interval.start);
1907
- }
1908
-
1909
- if (!hasPendingEndChange) {
1910
- this.setSlideOnRemove(interval.end);
1911
- }
1912
-
1913
- const needsStartUpdate = newStart !== undefined && !hasPendingStartChange;
1914
- const needsEndUpdate = newEnd !== undefined && !hasPendingEndChange;
1915
-
1916
- if (needsStartUpdate || needsEndUpdate) {
1917
- if (!this.localCollection) {
1918
- throw new LoggingError("Attach must be called before accessing intervals");
1919
- }
1920
-
1921
- // `interval`'s endpoints will get modified in-place, so clone it prior to doing so for event emission.
1922
- const oldInterval = interval.clone() as TInterval & SequenceInterval;
1923
-
1924
- // In this case, where we change the start or end of an interval,
1925
- // it is necessary to remove and re-add the interval listeners.
1926
- // This ensures that the correct listeners are added to the LocalReferencePosition.
1927
- this.localCollection.removeExistingInterval(interval);
1928
- if (!this.client) {
1929
- throw new LoggingError("client does not exist");
1930
- }
1931
-
1932
- if (needsStartUpdate) {
1933
- const props = interval.start.properties;
1934
- interval.start = createPositionReferenceFromSegoff(this.client, newStart, interval.start.refType, op);
1935
- if (props) {
1936
- interval.start.addProperties(props);
1937
- }
1938
- const oldSeg = oldInterval.start.getSegment();
1939
- // remove and rebuild start interval as transient for event
1940
- this.client.removeLocalReferencePosition(oldInterval.start);
1941
- oldInterval.start.refType = ReferenceType.Transient;
1942
- oldSeg?.localRefs?.addLocalRef(
1943
- oldInterval.start,
1944
- oldInterval.start.getOffset());
1945
- }
1946
- if (needsEndUpdate) {
1947
- const props = interval.end.properties;
1948
- interval.end = createPositionReferenceFromSegoff(this.client, newEnd, interval.end.refType, op);
1949
- if (props) {
1950
- interval.end.addProperties(props);
1951
- }
1952
- // remove and rebuild end interval as transient for event
1953
- const oldSeg = oldInterval.end.getSegment();
1954
- this.client.removeLocalReferencePosition(oldInterval.end);
1955
- oldInterval.end.refType = ReferenceType.Transient;
1956
- oldSeg?.localRefs?.addLocalRef(
1957
- oldInterval.end,
1958
- oldInterval.end.getOffset());
1959
- }
1960
- this.localCollection.add(interval);
1961
- this.emitChange(interval, oldInterval as TInterval, true, op);
1962
- }
1963
- }
1964
-
1965
- /** @internal */
1966
- public ackAdd(
1967
- serializedInterval: ISerializedInterval,
1968
- local: boolean,
1969
- op: ISequencedDocumentMessage,
1970
- localOpMetadata: IMapMessageLocalMetadata | undefined,
1971
- ) {
1972
- if (local) {
1973
- assert(localOpMetadata !== undefined, 0x553 /* op metadata should be defined for local op */);
1974
- this.localSeqToSerializedInterval.delete(localOpMetadata.localSeq);
1975
- const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1976
- const localInterval = this.getIntervalById(id);
1977
- if (localInterval) {
1978
- this.ackInterval(localInterval, op);
1979
- }
1980
- return;
1981
- }
1982
-
1983
- if (!this.localCollection) {
1984
- throw new LoggingError("attachSequence must be called");
1985
- }
1986
-
1987
- this.localCollection.ensureSerializedId(serializedInterval);
1988
-
1989
- const interval: TInterval = this.localCollection.addInterval(
1990
- serializedInterval.start,
1991
- serializedInterval.end,
1992
- serializedInterval.intervalType,
1993
- serializedInterval.properties,
1994
- op);
1995
-
1996
- if (interval) {
1997
- if (this.onDeserialize) {
1998
- this.onDeserialize(interval);
1999
- }
2000
- }
2001
-
2002
- this.emit("addInterval", interval, local, op);
2003
-
2004
- return interval;
2005
- }
2006
-
2007
- /** @internal */
2008
- public ackDelete(
2009
- serializedInterval: ISerializedInterval,
2010
- local: boolean,
2011
- op: ISequencedDocumentMessage): void {
2012
- if (local) {
2013
- // Local ops were applied when the message was created and there's no "pending delete"
2014
- // state to bookkeep: remote operation application takes into account possibility of
2015
- // locally deleted interval whenever a lookup happens.
2016
- return;
2017
- }
2018
-
2019
- if (!this.localCollection) {
2020
- throw new LoggingError("attach must be called prior to deleting intervals");
2021
- }
2022
-
2023
- const id = this.localCollection.ensureSerializedId(serializedInterval);
2024
- const interval = this.localCollection.getIntervalById(id);
2025
- if (interval) {
2026
- this.deleteExistingInterval(interval, local, op);
2027
- }
2028
- }
2029
-
2030
- /**
2031
- * @internal
2032
- */
2033
- public serializeInternal(): ISerializedIntervalCollectionV2 {
2034
- if (!this.localCollection) {
2035
- throw new LoggingError("attachSequence must be called");
2036
- }
2037
-
2038
- return this.localCollection.serialize();
2039
- }
2040
-
2041
- /**
2042
- * @returns an iterator over all intervals in this collection.
2043
- */
2044
- public [Symbol.iterator](): IntervalCollectionIterator<TInterval> {
2045
- const iterator = new IntervalCollectionIterator<TInterval>(this);
2046
- return iterator;
2047
- }
2048
-
2049
- /**
2050
- * @returns a forward iterator over all intervals in this collection with start point equal to `startPosition`.
2051
- */
2052
- public CreateForwardIteratorWithStartPosition(startPosition: number): IntervalCollectionIterator<TInterval> {
2053
- const iterator = new IntervalCollectionIterator<TInterval>(this, true, startPosition);
2054
- return iterator;
2055
- }
2056
-
2057
- /**
2058
- * @returns a backward iterator over all intervals in this collection with start point equal to `startPosition`.
2059
- */
2060
- public CreateBackwardIteratorWithStartPosition(startPosition: number): IntervalCollectionIterator<TInterval> {
2061
- const iterator = new IntervalCollectionIterator<TInterval>(this, false, startPosition);
2062
- return iterator;
2063
- }
2064
-
2065
- /**
2066
- * @returns a forward iterator over all intervals in this collection with end point equal to `endPosition`.
2067
- */
2068
- public CreateForwardIteratorWithEndPosition(endPosition: number): IntervalCollectionIterator<TInterval> {
2069
- const iterator = new IntervalCollectionIterator<TInterval>(this, true, undefined, endPosition);
2070
- return iterator;
2071
- }
2072
-
2073
- /**
2074
- * @returns a backward iterator over all intervals in this collection with end point equal to `endPosition`.
2075
- */
2076
- public CreateBackwardIteratorWithEndPosition(endPosition: number): IntervalCollectionIterator<TInterval> {
2077
- const iterator = new IntervalCollectionIterator<TInterval>(this, false, undefined, endPosition);
2078
- return iterator;
2079
- }
2080
-
2081
- /**
2082
- * Gathers iteration results that optionally match a start/end criteria into the provided array.
2083
- * @param results - Array to gather the results into. In lieu of a return value, this array will be populated with
2084
- * intervals matching the query upon edit.
2085
- * @param iteratesForward - whether or not iteration should be in the forward direction
2086
- * @param start - If provided, only match intervals whose start point is equal to `start`.
2087
- * @param end - If provided, only match intervals whose end point is equal to `end`.
2088
- */
2089
- public gatherIterationResults(
2090
- results: TInterval[],
2091
- iteratesForward: boolean,
2092
- start?: number,
2093
- end?: number) {
2094
- if (!this.localCollection) {
2095
- return;
2096
- }
2097
-
2098
- this.localCollection.gatherIterationResults(results, iteratesForward, start, end);
2099
- }
2100
-
2101
- /**
2102
- * @returns an array of all intervals in this collection that overlap with the interval
2103
- * `[startPosition, endPosition]`.
2104
- */
2105
- public findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[] {
2106
- if (!this.localCollection) {
2107
- throw new LoggingError("attachSequence must be called");
2108
- }
2109
-
2110
- return this.localCollection.findOverlappingIntervals(startPosition, endPosition);
2111
- }
2112
-
2113
- /**
2114
- * Applies a function to each interval in this collection.
2115
- */
2116
- public map(fn: (interval: TInterval) => void) {
2117
- if (!this.localCollection) {
2118
- throw new LoggingError("attachSequence must be called");
2119
- }
2120
-
2121
- this.localCollection.map(fn);
2122
- }
2123
-
2124
- public previousInterval(pos: number): TInterval | undefined {
2125
- if (!this.localCollection) {
2126
- throw new LoggingError("attachSequence must be called");
2127
- }
2128
-
2129
- return this.localCollection.previousInterval(pos);
2130
- }
2131
-
2132
- public nextInterval(pos: number): TInterval | undefined {
2133
- if (!this.localCollection) {
2134
- throw new LoggingError("attachSequence must be called");
2135
- }
2136
-
2137
- return this.localCollection.nextInterval(pos);
2138
- }
1428
+ export class IntervalCollection<TInterval extends ISerializableInterval> extends TypedEventEmitter<
1429
+ IIntervalCollectionEvent<TInterval>
1430
+ > {
1431
+ private savedSerializedIntervals?: ISerializedInterval[];
1432
+ private localCollection: LocalIntervalCollection<TInterval> | undefined;
1433
+ private onDeserialize: DeserializeCallback | undefined;
1434
+ private client: Client | undefined;
1435
+ private readonly localSeqToSerializedInterval = new Map<
1436
+ number,
1437
+ ISerializedInterval | SerializedIntervalDelta
1438
+ >();
1439
+ private readonly localSeqToRebasedInterval = new Map<
1440
+ number,
1441
+ ISerializedInterval | SerializedIntervalDelta
1442
+ >();
1443
+ private readonly pendingChangesStart: Map<string, ISerializedInterval[]> = new Map<
1444
+ string,
1445
+ ISerializedInterval[]
1446
+ >();
1447
+ private readonly pendingChangesEnd: Map<string, ISerializedInterval[]> = new Map<
1448
+ string,
1449
+ ISerializedInterval[]
1450
+ >();
1451
+
1452
+ public get attached(): boolean {
1453
+ return !!this.localCollection;
1454
+ }
1455
+
1456
+ /** @internal */
1457
+ constructor(
1458
+ private readonly helpers: IIntervalHelpers<TInterval>,
1459
+ private readonly requiresClient: boolean,
1460
+ private readonly emitter: IValueOpEmitter,
1461
+ serializedIntervals: ISerializedInterval[] | ISerializedIntervalCollectionV2,
1462
+ ) {
1463
+ super();
1464
+
1465
+ this.savedSerializedIntervals = Array.isArray(serializedIntervals)
1466
+ ? serializedIntervals
1467
+ : serializedIntervals.intervals.map((i) =>
1468
+ decompressInterval(i, serializedIntervals.label),
1469
+ );
1470
+ }
1471
+
1472
+ private rebasePositionWithSegmentSlide(
1473
+ pos: number,
1474
+ seqNumberFrom: number,
1475
+ localSeq: number,
1476
+ ): number | undefined {
1477
+ if (!this.client) {
1478
+ throw new LoggingError("mergeTree client must exist");
1479
+ }
1480
+ const { clientId } = this.client.getCollabWindow();
1481
+ const { segment, offset } = this.client.getContainingSegment(
1482
+ pos,
1483
+ {
1484
+ referenceSequenceNumber: seqNumberFrom,
1485
+ clientId: this.client.getLongClientId(clientId),
1486
+ },
1487
+ localSeq,
1488
+ );
1489
+
1490
+ // if segment is undefined, it slid off the string
1491
+ assert(segment !== undefined, 0x54e /* No segment found */);
1492
+
1493
+ const segoff = this.client.getSlideToSegment({ segment, offset }) ?? segment;
1494
+
1495
+ // case happens when rebasing op, but concurrently entire string has been deleted
1496
+ if (segoff.segment === undefined || segoff.offset === undefined) {
1497
+ return DetachedReferencePosition;
1498
+ }
1499
+
1500
+ assert(
1501
+ offset !== undefined && 0 <= offset && offset < segment.cachedLength,
1502
+ 0x54f /* Invalid offset */,
1503
+ );
1504
+ return this.client.findReconnectionPosition(segoff.segment, localSeq) + segoff.offset;
1505
+ }
1506
+
1507
+ private computeRebasedPositions(
1508
+ localSeq: number,
1509
+ ): ISerializedInterval | SerializedIntervalDelta {
1510
+ assert(
1511
+ this.client !== undefined,
1512
+ 0x550 /* Client should be defined when computing rebased position */,
1513
+ );
1514
+ const original = this.localSeqToSerializedInterval.get(localSeq);
1515
+ assert(
1516
+ original !== undefined,
1517
+ 0x551 /* Failed to store pending serialized interval info for this localSeq. */,
1518
+ );
1519
+ const rebased = { ...original };
1520
+ const { start, end, sequenceNumber } = original;
1521
+ if (start !== undefined) {
1522
+ rebased.start = this.rebasePositionWithSegmentSlide(start, sequenceNumber, localSeq);
1523
+ }
1524
+ if (end !== undefined) {
1525
+ rebased.end = this.rebasePositionWithSegmentSlide(end, sequenceNumber, localSeq);
1526
+ }
1527
+ return rebased;
1528
+ }
1529
+
1530
+ /** @internal */
1531
+ public attachGraph(client: Client, label: string) {
1532
+ if (this.attached) {
1533
+ throw new LoggingError("Only supports one Sequence attach");
1534
+ }
1535
+
1536
+ if (client === undefined && this.requiresClient) {
1537
+ throw new LoggingError("Client required for this collection");
1538
+ }
1539
+
1540
+ // Instantiate the local interval collection based on the saved intervals
1541
+ this.client = client;
1542
+ if (client) {
1543
+ client.on("normalize", () => {
1544
+ for (const localSeq of this.localSeqToSerializedInterval.keys()) {
1545
+ this.localSeqToRebasedInterval.set(
1546
+ localSeq,
1547
+ this.computeRebasedPositions(localSeq),
1548
+ );
1549
+ }
1550
+ });
1551
+ }
1552
+
1553
+ this.localCollection = new LocalIntervalCollection<TInterval>(
1554
+ client,
1555
+ label,
1556
+ this.helpers,
1557
+ (interval, previousInterval) => this.emitChange(interval, previousInterval, true),
1558
+ );
1559
+ if (this.savedSerializedIntervals) {
1560
+ for (const serializedInterval of this.savedSerializedIntervals) {
1561
+ this.localCollection.ensureSerializedId(serializedInterval);
1562
+ const { start, end, intervalType, properties } = serializedInterval;
1563
+ const interval = this.helpers.create(
1564
+ label,
1565
+ start,
1566
+ end,
1567
+ client,
1568
+ intervalType,
1569
+ undefined,
1570
+ true,
1571
+ );
1572
+ if (properties) {
1573
+ interval.addProperties(properties);
1574
+ }
1575
+ this.localCollection.add(interval);
1576
+ }
1577
+ }
1578
+ this.savedSerializedIntervals = undefined;
1579
+ }
1580
+
1581
+ /**
1582
+ * Gets the next local sequence number, modifying this client's collab window in doing so.
1583
+ */
1584
+ private getNextLocalSeq(): number {
1585
+ if (this.client) {
1586
+ return ++this.client.getCollabWindow().localSeq;
1587
+ }
1588
+
1589
+ return 0;
1590
+ }
1591
+
1592
+ private emitChange(
1593
+ interval: TInterval,
1594
+ previousInterval: TInterval,
1595
+ local: boolean,
1596
+ op?: ISequencedDocumentMessage,
1597
+ ): void {
1598
+ // Temporarily make references transient so that positional queries work (non-transient refs
1599
+ // on resolve to DetachedPosition on any segments that don't contain them). The original refType
1600
+ // is restored as single-endpoint changes re-use previous references.
1601
+ let startRefType: ReferenceType;
1602
+ let endRefType: ReferenceType;
1603
+ if (previousInterval instanceof SequenceInterval) {
1604
+ startRefType = previousInterval.start.refType;
1605
+ endRefType = previousInterval.end.refType;
1606
+ previousInterval.start.refType = ReferenceType.Transient;
1607
+ previousInterval.end.refType = ReferenceType.Transient;
1608
+ this.emit("changeInterval", interval, previousInterval, local, op);
1609
+ previousInterval.start.refType = startRefType;
1610
+ previousInterval.end.refType = endRefType;
1611
+ } else {
1612
+ this.emit("changeInterval", interval, previousInterval, local, op);
1613
+ }
1614
+ }
1615
+
1616
+ /**
1617
+ * @returns the interval in this collection that has the provided `id`.
1618
+ * If no interval in the collection has this `id`, returns `undefined`.
1619
+ */
1620
+ public getIntervalById(id: string) {
1621
+ if (!this.localCollection) {
1622
+ throw new LoggingError("attach must be called before accessing intervals");
1623
+ }
1624
+ return this.localCollection.getIntervalById(id);
1625
+ }
1626
+
1627
+ /**
1628
+ * Creates a new interval and add it to the collection.
1629
+ * @param start - interval start position
1630
+ * @param end - interval end position
1631
+ * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
1632
+ * @param props - properties of the interval
1633
+ * @returns - the created interval
1634
+ */
1635
+ public add(
1636
+ start: number,
1637
+ end: number,
1638
+ intervalType: IntervalType,
1639
+ props?: PropertySet,
1640
+ ): TInterval {
1641
+ if (!this.localCollection) {
1642
+ throw new LoggingError("attach must be called prior to adding intervals");
1643
+ }
1644
+ if (intervalType & IntervalType.Transient) {
1645
+ throw new LoggingError("Can not add transient intervals");
1646
+ }
1647
+
1648
+ const interval: TInterval = this.localCollection.addInterval(
1649
+ start,
1650
+ end,
1651
+ intervalType,
1652
+ props,
1653
+ );
1654
+
1655
+ if (interval) {
1656
+ const serializedInterval = {
1657
+ end,
1658
+ intervalType,
1659
+ properties: interval.properties,
1660
+ sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1661
+ start,
1662
+ };
1663
+ const localSeq = this.getNextLocalSeq();
1664
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1665
+ // Local ops get submitted to the server. Remote ops have the deserializer run.
1666
+ this.emitter.emit("add", undefined, serializedInterval, { localSeq });
1667
+ }
1668
+
1669
+ this.emit("addInterval", interval, true, undefined);
1670
+
1671
+ return interval;
1672
+ }
1673
+
1674
+ private deleteExistingInterval(
1675
+ interval: TInterval,
1676
+ local: boolean,
1677
+ op?: ISequencedDocumentMessage,
1678
+ ) {
1679
+ if (!this.localCollection) {
1680
+ throw new LoggingError("Attach must be called before accessing intervals");
1681
+ }
1682
+ // The given interval is known to exist in the collection.
1683
+ this.localCollection.removeExistingInterval(interval);
1684
+
1685
+ if (interval) {
1686
+ // Local ops get submitted to the server. Remote ops have the deserializer run.
1687
+ if (local) {
1688
+ this.emitter.emit("delete", undefined, interval.serialize(), {
1689
+ localSeq: this.getNextLocalSeq(),
1690
+ });
1691
+ } else {
1692
+ if (this.onDeserialize) {
1693
+ this.onDeserialize(interval);
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ this.emit("deleteInterval", interval, local, op);
1699
+ }
1700
+
1701
+ /**
1702
+ * Removes an interval from the collection.
1703
+ * @param id - Id of the interval to remove
1704
+ * @returns the removed interval
1705
+ */
1706
+ public removeIntervalById(id: string) {
1707
+ if (!this.localCollection) {
1708
+ throw new LoggingError("Attach must be called before accessing intervals");
1709
+ }
1710
+ const interval = this.localCollection.getIntervalById(id);
1711
+ if (interval) {
1712
+ this.deleteExistingInterval(interval, true, undefined);
1713
+ }
1714
+ return interval;
1715
+ }
1716
+
1717
+ /**
1718
+ * Changes the properties on an existing interval.
1719
+ * @param id - Id of the interval whose properties should be changed
1720
+ * @param props - Property set to apply to the interval. Shallow merging is used between any existing properties
1721
+ * and `prop`, i.e. the interval will end up with a property object equivalent to `{ ...oldProps, ...props }`.
1722
+ */
1723
+ public changeProperties(id: string, props: PropertySet) {
1724
+ if (!this.attached) {
1725
+ throw new LoggingError("Attach must be called before accessing intervals");
1726
+ }
1727
+ if (typeof id !== "string") {
1728
+ throw new LoggingError("Change API requires an ID that is a string");
1729
+ }
1730
+ if (!props) {
1731
+ throw new LoggingError("changeProperties should be called with a property set");
1732
+ }
1733
+
1734
+ const interval = this.getIntervalById(id);
1735
+ if (interval) {
1736
+ // Pass Unassigned as the sequence number to indicate that this is a local op that is waiting for an ack.
1737
+ const deltaProps = interval.addProperties(props, true, UnassignedSequenceNumber);
1738
+ const serializedInterval: ISerializedInterval = interval.serialize();
1739
+
1740
+ // Emit a change op that will only change properties. Add the ID to
1741
+ // the property bag provided by the caller.
1742
+ serializedInterval.start = undefined as any;
1743
+ serializedInterval.end = undefined as any;
1744
+
1745
+ serializedInterval.properties = props;
1746
+ serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
1747
+ const localSeq = this.getNextLocalSeq();
1748
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1749
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1750
+ this.emit("propertyChanged", interval, deltaProps, true, undefined);
1751
+ }
1752
+ }
1753
+
1754
+ /**
1755
+ * Changes the endpoints of an existing interval.
1756
+ * @param id - Id of the interval to change
1757
+ * @param start - New start value, if defined. `undefined` signifies this endpoint should be left unchanged.
1758
+ * @param end - New end value, if defined. `undefined` signifies this endpoint should be left unchanged.
1759
+ * @returns the interval that was changed, if it existed in the collection.
1760
+ */
1761
+ public change(id: string, start?: number, end?: number): TInterval | undefined {
1762
+ if (!this.localCollection) {
1763
+ throw new LoggingError("Attach must be called before accessing intervals");
1764
+ }
1765
+
1766
+ // Force id to be a string.
1767
+ if (typeof id !== "string") {
1768
+ throw new LoggingError("Change API requires an ID that is a string");
1769
+ }
1770
+
1771
+ const interval = this.getIntervalById(id);
1772
+ if (interval) {
1773
+ const newInterval = this.localCollection.changeInterval(interval, start, end);
1774
+ if (!newInterval) {
1775
+ return undefined;
1776
+ }
1777
+ const serializedInterval: SerializedIntervalDelta = interval.serialize();
1778
+ serializedInterval.start = start;
1779
+ serializedInterval.end = end;
1780
+ // Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
1781
+ serializedInterval.properties = {
1782
+ [reservedIntervalIdKey]: interval.getIntervalId(),
1783
+ };
1784
+ const localSeq = this.getNextLocalSeq();
1785
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1786
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1787
+ this.addPendingChange(id, serializedInterval);
1788
+ this.emitChange(newInterval, interval, true);
1789
+ return newInterval;
1790
+ }
1791
+ // No interval to change
1792
+ return undefined;
1793
+ }
1794
+
1795
+ private addPendingChange(id: string, serializedInterval: SerializedIntervalDelta) {
1796
+ if (serializedInterval.start !== undefined) {
1797
+ this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1798
+ }
1799
+ if (serializedInterval.end !== undefined) {
1800
+ this.addPendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
1801
+ }
1802
+ }
1803
+
1804
+ private addPendingChangeHelper(
1805
+ id: string,
1806
+ pendingChanges: Map<string, SerializedIntervalDelta[]>,
1807
+ serializedInterval: SerializedIntervalDelta,
1808
+ ) {
1809
+ let entries: SerializedIntervalDelta[] | undefined = pendingChanges.get(id);
1810
+ if (!entries) {
1811
+ entries = [];
1812
+ pendingChanges.set(id, entries);
1813
+ }
1814
+ entries.push(serializedInterval);
1815
+ }
1816
+
1817
+ private removePendingChange(serializedInterval: SerializedIntervalDelta) {
1818
+ // Change ops always have an ID.
1819
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1820
+ if (serializedInterval.start !== undefined) {
1821
+ this.removePendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1822
+ }
1823
+ if (serializedInterval.end !== undefined) {
1824
+ this.removePendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
1825
+ }
1826
+ }
1827
+
1828
+ private removePendingChangeHelper(
1829
+ id: string,
1830
+ pendingChanges: Map<string, SerializedIntervalDelta[]>,
1831
+ serializedInterval: SerializedIntervalDelta,
1832
+ ) {
1833
+ const entries = pendingChanges.get(id);
1834
+ if (entries) {
1835
+ const pendingChange = entries.shift();
1836
+ if (entries.length === 0) {
1837
+ pendingChanges.delete(id);
1838
+ }
1839
+ if (
1840
+ pendingChange?.start !== serializedInterval.start ||
1841
+ pendingChange?.end !== serializedInterval.end
1842
+ ) {
1843
+ throw new LoggingError("Mismatch in pending changes");
1844
+ }
1845
+ }
1846
+ }
1847
+
1848
+ private hasPendingChangeStart(id: string) {
1849
+ const entries = this.pendingChangesStart.get(id);
1850
+ return entries && entries.length !== 0;
1851
+ }
1852
+
1853
+ private hasPendingChangeEnd(id: string) {
1854
+ const entries = this.pendingChangesEnd.get(id);
1855
+ return entries && entries.length !== 0;
1856
+ }
1857
+
1858
+ /** @internal */
1859
+ public ackChange(
1860
+ serializedInterval: ISerializedInterval,
1861
+ local: boolean,
1862
+ op: ISequencedDocumentMessage,
1863
+ localOpMetadata: IMapMessageLocalMetadata | undefined,
1864
+ ) {
1865
+ if (!this.localCollection) {
1866
+ throw new LoggingError("Attach must be called before accessing intervals");
1867
+ }
1868
+
1869
+ if (local) {
1870
+ assert(
1871
+ localOpMetadata !== undefined,
1872
+ 0x552 /* op metadata should be defined for local op */,
1873
+ );
1874
+ this.localSeqToSerializedInterval.delete(localOpMetadata?.localSeq);
1875
+ // This is an ack from the server. Remove the pending change.
1876
+ this.removePendingChange(serializedInterval);
1877
+ }
1878
+
1879
+ // Note that the ID is in the property bag only to allow us to find the interval.
1880
+ // This API cannot change the ID, and writing to the ID property will result in an exception. So we
1881
+ // strip it out of the properties here.
1882
+ const { [reservedIntervalIdKey]: id, ...newProps } = serializedInterval.properties ?? {};
1883
+ assert(id !== undefined, 0x3fe /* id must exist on the interval */);
1884
+ const interval: TInterval | undefined = this.getIntervalById(id);
1885
+ if (!interval) {
1886
+ // The interval has been removed locally; no-op.
1887
+ return;
1888
+ }
1889
+
1890
+ if (local) {
1891
+ // Let the propertyManager prune its pending change-properties set.
1892
+ interval.propertyManager?.ackPendingProperties({
1893
+ type: MergeTreeDeltaType.ANNOTATE,
1894
+ props: serializedInterval.properties ?? {},
1895
+ });
1896
+
1897
+ this.ackInterval(interval, op);
1898
+ } else {
1899
+ // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
1900
+ // should be the winning change.
1901
+ let start: number | undefined;
1902
+ let end: number | undefined;
1903
+ // Track pending start/end independently of one another.
1904
+ if (!this.hasPendingChangeStart(id)) {
1905
+ start = serializedInterval.start;
1906
+ }
1907
+ if (!this.hasPendingChangeEnd(id)) {
1908
+ end = serializedInterval.end;
1909
+ }
1910
+
1911
+ let newInterval = interval;
1912
+ if (start !== undefined || end !== undefined) {
1913
+ // If changeInterval gives us a new interval, work with that one. Otherwise keep working with
1914
+ // the one we originally found in the tree.
1915
+ newInterval =
1916
+ this.localCollection.changeInterval(interval, start, end, op) ?? interval;
1917
+ }
1918
+ const deltaProps = newInterval.addProperties(newProps, true, op.sequenceNumber);
1919
+ if (this.onDeserialize) {
1920
+ this.onDeserialize(newInterval);
1921
+ }
1922
+
1923
+ if (newInterval !== interval) {
1924
+ this.emitChange(newInterval, interval, local, op);
1925
+ }
1926
+
1927
+ const changedProperties = Object.keys(newProps).length > 0;
1928
+ if (changedProperties) {
1929
+ this.emit("propertyChanged", interval, deltaProps, local, op);
1930
+ }
1931
+ }
1932
+ }
1933
+
1934
+ public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>): void {
1935
+ if (!this.localCollection) {
1936
+ throw new LoggingError("attachSequence must be called");
1937
+ }
1938
+ this.localCollection.addConflictResolver(conflictResolver);
1939
+ }
1940
+
1941
+ public attachDeserializer(onDeserialize: DeserializeCallback): void {
1942
+ // If no deserializer is specified can skip all processing work
1943
+ if (!onDeserialize) {
1944
+ return;
1945
+ }
1946
+
1947
+ // Start by storing the callbacks so that any subsequent modifications make use of them
1948
+ this.onDeserialize = onDeserialize;
1949
+
1950
+ // Trigger the async prepare work across all values in the collection
1951
+ this.localCollection?.map((interval) => {
1952
+ onDeserialize(interval);
1953
+ });
1954
+ }
1955
+
1956
+ /**
1957
+ * Returns new interval after rebasing. If undefined, the interval was
1958
+ * deleted as a result of rebasing. This can occur if the interval applies
1959
+ * to a range that no longer exists, and the interval was unable to slide.
1960
+ *
1961
+ * @internal
1962
+ */
1963
+ public rebaseLocalInterval(
1964
+ opName: string,
1965
+ serializedInterval: SerializedIntervalDelta,
1966
+ localSeq: number,
1967
+ ): SerializedIntervalDelta | undefined {
1968
+ if (!this.client) {
1969
+ // If there's no associated mergeTree client, the originally submitted op is still correct.
1970
+ return serializedInterval;
1971
+ }
1972
+ if (!this.attached) {
1973
+ throw new LoggingError("attachSequence must be called");
1974
+ }
1975
+
1976
+ const { intervalType, properties } = serializedInterval;
1977
+
1978
+ const { start: startRebased, end: endRebased } =
1979
+ this.localSeqToRebasedInterval.get(localSeq) ?? this.computeRebasedPositions(localSeq);
1980
+
1981
+ const intervalId = properties?.[reservedIntervalIdKey];
1982
+ const localInterval = this.localCollection?.getIntervalById(intervalId);
1983
+
1984
+ const rebased: SerializedIntervalDelta = {
1985
+ start: startRebased,
1986
+ end: endRebased,
1987
+ intervalType,
1988
+ sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1989
+ properties,
1990
+ };
1991
+
1992
+ if (
1993
+ opName === "change" &&
1994
+ (this.hasPendingChangeStart(intervalId) || this.hasPendingChangeEnd(intervalId))
1995
+ ) {
1996
+ this.removePendingChange(serializedInterval);
1997
+ this.addPendingChange(intervalId, rebased);
1998
+ }
1999
+
2000
+ // if the interval slid off the string, rebase the op to be a noop and delete the interval.
2001
+ if (
2002
+ startRebased === DetachedReferencePosition ||
2003
+ endRebased === DetachedReferencePosition
2004
+ ) {
2005
+ if (localInterval) {
2006
+ this.localCollection?.removeExistingInterval(localInterval);
2007
+ }
2008
+ return undefined;
2009
+ }
2010
+
2011
+ if (localInterval !== undefined) {
2012
+ // we know we must be using `SequenceInterval` because `this.client` exists
2013
+ assert(
2014
+ localInterval instanceof SequenceInterval,
2015
+ 0x3a0 /* localInterval must be `SequenceInterval` when used with client */,
2016
+ );
2017
+ // The rebased op may place this interval's endpoints on different segments. Calling `changeInterval` here
2018
+ // updates the local client's state to be consistent with the emitted op.
2019
+ this.localCollection?.changeInterval(
2020
+ localInterval,
2021
+ startRebased,
2022
+ endRebased,
2023
+ undefined,
2024
+ localSeq,
2025
+ );
2026
+ }
2027
+
2028
+ return rebased;
2029
+ }
2030
+
2031
+ private getSlideToSegment(lref: LocalReferencePosition) {
2032
+ if (!this.client) {
2033
+ throw new LoggingError("client does not exist");
2034
+ }
2035
+ const segoff = { segment: lref.getSegment(), offset: lref.getOffset() };
2036
+ if (segoff.segment?.localRefs?.has(lref) !== true) {
2037
+ return undefined;
2038
+ }
2039
+ const newSegoff = this.client.getSlideToSegment(segoff);
2040
+ const value: { segment: ISegment | undefined; offset: number | undefined } | undefined =
2041
+ segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset
2042
+ ? undefined
2043
+ : newSegoff;
2044
+ return value;
2045
+ }
2046
+
2047
+ private setSlideOnRemove(lref: LocalReferencePosition) {
2048
+ let refType = lref.refType;
2049
+ refType = refType & ~ReferenceType.StayOnRemove;
2050
+ refType = refType | ReferenceType.SlideOnRemove;
2051
+ lref.refType = refType;
2052
+ }
2053
+
2054
+ private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
2055
+ // Only SequenceIntervals need potential sliding
2056
+ if (!(interval instanceof SequenceInterval)) {
2057
+ return;
2058
+ }
2059
+
2060
+ if (
2061
+ !refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove) &&
2062
+ !refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove)
2063
+ ) {
2064
+ return;
2065
+ }
2066
+
2067
+ const newStart = this.getSlideToSegment(interval.start);
2068
+ const newEnd = this.getSlideToSegment(interval.end);
2069
+
2070
+ const id = interval.properties[reservedIntervalIdKey];
2071
+ const hasPendingStartChange = this.hasPendingChangeStart(id);
2072
+ const hasPendingEndChange = this.hasPendingChangeEnd(id);
2073
+
2074
+ if (!hasPendingStartChange) {
2075
+ this.setSlideOnRemove(interval.start);
2076
+ }
2077
+
2078
+ if (!hasPendingEndChange) {
2079
+ this.setSlideOnRemove(interval.end);
2080
+ }
2081
+
2082
+ const needsStartUpdate = newStart !== undefined && !hasPendingStartChange;
2083
+ const needsEndUpdate = newEnd !== undefined && !hasPendingEndChange;
2084
+
2085
+ if (needsStartUpdate || needsEndUpdate) {
2086
+ if (!this.localCollection) {
2087
+ throw new LoggingError("Attach must be called before accessing intervals");
2088
+ }
2089
+
2090
+ // `interval`'s endpoints will get modified in-place, so clone it prior to doing so for event emission.
2091
+ const oldInterval = interval.clone() as TInterval & SequenceInterval;
2092
+
2093
+ // In this case, where we change the start or end of an interval,
2094
+ // it is necessary to remove and re-add the interval listeners.
2095
+ // This ensures that the correct listeners are added to the LocalReferencePosition.
2096
+ this.localCollection.removeExistingInterval(interval);
2097
+ if (!this.client) {
2098
+ throw new LoggingError("client does not exist");
2099
+ }
2100
+
2101
+ if (needsStartUpdate) {
2102
+ const props = interval.start.properties;
2103
+ interval.start = createPositionReferenceFromSegoff(
2104
+ this.client,
2105
+ newStart,
2106
+ interval.start.refType,
2107
+ op,
2108
+ );
2109
+ if (props) {
2110
+ interval.start.addProperties(props);
2111
+ }
2112
+ const oldSeg = oldInterval.start.getSegment();
2113
+ // remove and rebuild start interval as transient for event
2114
+ this.client.removeLocalReferencePosition(oldInterval.start);
2115
+ oldInterval.start.refType = ReferenceType.Transient;
2116
+ oldSeg?.localRefs?.addLocalRef(oldInterval.start, oldInterval.start.getOffset());
2117
+ }
2118
+ if (needsEndUpdate) {
2119
+ const props = interval.end.properties;
2120
+ interval.end = createPositionReferenceFromSegoff(
2121
+ this.client,
2122
+ newEnd,
2123
+ interval.end.refType,
2124
+ op,
2125
+ );
2126
+ if (props) {
2127
+ interval.end.addProperties(props);
2128
+ }
2129
+ // remove and rebuild end interval as transient for event
2130
+ const oldSeg = oldInterval.end.getSegment();
2131
+ this.client.removeLocalReferencePosition(oldInterval.end);
2132
+ oldInterval.end.refType = ReferenceType.Transient;
2133
+ oldSeg?.localRefs?.addLocalRef(oldInterval.end, oldInterval.end.getOffset());
2134
+ }
2135
+ this.localCollection.add(interval);
2136
+ this.emitChange(interval, oldInterval as TInterval, true, op);
2137
+ }
2138
+ }
2139
+
2140
+ /** @internal */
2141
+ public ackAdd(
2142
+ serializedInterval: ISerializedInterval,
2143
+ local: boolean,
2144
+ op: ISequencedDocumentMessage,
2145
+ localOpMetadata: IMapMessageLocalMetadata | undefined,
2146
+ ) {
2147
+ if (local) {
2148
+ assert(
2149
+ localOpMetadata !== undefined,
2150
+ 0x553 /* op metadata should be defined for local op */,
2151
+ );
2152
+ this.localSeqToSerializedInterval.delete(localOpMetadata.localSeq);
2153
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
2154
+ const localInterval = this.getIntervalById(id);
2155
+ if (localInterval) {
2156
+ this.ackInterval(localInterval, op);
2157
+ }
2158
+ return;
2159
+ }
2160
+
2161
+ if (!this.localCollection) {
2162
+ throw new LoggingError("attachSequence must be called");
2163
+ }
2164
+
2165
+ this.localCollection.ensureSerializedId(serializedInterval);
2166
+
2167
+ const interval: TInterval = this.localCollection.addInterval(
2168
+ serializedInterval.start,
2169
+ serializedInterval.end,
2170
+ serializedInterval.intervalType,
2171
+ serializedInterval.properties,
2172
+ op,
2173
+ );
2174
+
2175
+ if (interval) {
2176
+ if (this.onDeserialize) {
2177
+ this.onDeserialize(interval);
2178
+ }
2179
+ }
2180
+
2181
+ this.emit("addInterval", interval, local, op);
2182
+
2183
+ return interval;
2184
+ }
2185
+
2186
+ /** @internal */
2187
+ public ackDelete(
2188
+ serializedInterval: ISerializedInterval,
2189
+ local: boolean,
2190
+ op: ISequencedDocumentMessage,
2191
+ ): void {
2192
+ if (local) {
2193
+ // Local ops were applied when the message was created and there's no "pending delete"
2194
+ // state to bookkeep: remote operation application takes into account possibility of
2195
+ // locally deleted interval whenever a lookup happens.
2196
+ return;
2197
+ }
2198
+
2199
+ if (!this.localCollection) {
2200
+ throw new LoggingError("attach must be called prior to deleting intervals");
2201
+ }
2202
+
2203
+ const id = this.localCollection.ensureSerializedId(serializedInterval);
2204
+ const interval = this.localCollection.getIntervalById(id);
2205
+ if (interval) {
2206
+ this.deleteExistingInterval(interval, local, op);
2207
+ }
2208
+ }
2209
+
2210
+ /**
2211
+ * @internal
2212
+ */
2213
+ public serializeInternal(): ISerializedIntervalCollectionV2 {
2214
+ if (!this.localCollection) {
2215
+ throw new LoggingError("attachSequence must be called");
2216
+ }
2217
+
2218
+ return this.localCollection.serialize();
2219
+ }
2220
+
2221
+ /**
2222
+ * @returns an iterator over all intervals in this collection.
2223
+ */
2224
+ public [Symbol.iterator](): IntervalCollectionIterator<TInterval> {
2225
+ const iterator = new IntervalCollectionIterator<TInterval>(this);
2226
+ return iterator;
2227
+ }
2228
+
2229
+ /**
2230
+ * @returns a forward iterator over all intervals in this collection with start point equal to `startPosition`.
2231
+ */
2232
+ public CreateForwardIteratorWithStartPosition(
2233
+ startPosition: number,
2234
+ ): IntervalCollectionIterator<TInterval> {
2235
+ const iterator = new IntervalCollectionIterator<TInterval>(this, true, startPosition);
2236
+ return iterator;
2237
+ }
2238
+
2239
+ /**
2240
+ * @returns a backward iterator over all intervals in this collection with start point equal to `startPosition`.
2241
+ */
2242
+ public CreateBackwardIteratorWithStartPosition(
2243
+ startPosition: number,
2244
+ ): IntervalCollectionIterator<TInterval> {
2245
+ const iterator = new IntervalCollectionIterator<TInterval>(this, false, startPosition);
2246
+ return iterator;
2247
+ }
2248
+
2249
+ /**
2250
+ * @returns a forward iterator over all intervals in this collection with end point equal to `endPosition`.
2251
+ */
2252
+ public CreateForwardIteratorWithEndPosition(
2253
+ endPosition: number,
2254
+ ): IntervalCollectionIterator<TInterval> {
2255
+ const iterator = new IntervalCollectionIterator<TInterval>(
2256
+ this,
2257
+ true,
2258
+ undefined,
2259
+ endPosition,
2260
+ );
2261
+ return iterator;
2262
+ }
2263
+
2264
+ /**
2265
+ * @returns a backward iterator over all intervals in this collection with end point equal to `endPosition`.
2266
+ */
2267
+ public CreateBackwardIteratorWithEndPosition(
2268
+ endPosition: number,
2269
+ ): IntervalCollectionIterator<TInterval> {
2270
+ const iterator = new IntervalCollectionIterator<TInterval>(
2271
+ this,
2272
+ false,
2273
+ undefined,
2274
+ endPosition,
2275
+ );
2276
+ return iterator;
2277
+ }
2278
+
2279
+ /**
2280
+ * Gathers iteration results that optionally match a start/end criteria into the provided array.
2281
+ * @param results - Array to gather the results into. In lieu of a return value, this array will be populated with
2282
+ * intervals matching the query upon edit.
2283
+ * @param iteratesForward - whether or not iteration should be in the forward direction
2284
+ * @param start - If provided, only match intervals whose start point is equal to `start`.
2285
+ * @param end - If provided, only match intervals whose end point is equal to `end`.
2286
+ */
2287
+ public gatherIterationResults(
2288
+ results: TInterval[],
2289
+ iteratesForward: boolean,
2290
+ start?: number,
2291
+ end?: number,
2292
+ ) {
2293
+ if (!this.localCollection) {
2294
+ return;
2295
+ }
2296
+
2297
+ this.localCollection.gatherIterationResults(results, iteratesForward, start, end);
2298
+ }
2299
+
2300
+ /**
2301
+ * @returns an array of all intervals in this collection that overlap with the interval
2302
+ * `[startPosition, endPosition]`.
2303
+ */
2304
+ public findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[] {
2305
+ if (!this.localCollection) {
2306
+ throw new LoggingError("attachSequence must be called");
2307
+ }
2308
+
2309
+ return this.localCollection.findOverlappingIntervals(startPosition, endPosition);
2310
+ }
2311
+
2312
+ /**
2313
+ * Applies a function to each interval in this collection.
2314
+ */
2315
+ public map(fn: (interval: TInterval) => void) {
2316
+ if (!this.localCollection) {
2317
+ throw new LoggingError("attachSequence must be called");
2318
+ }
2319
+
2320
+ this.localCollection.map(fn);
2321
+ }
2322
+
2323
+ public previousInterval(pos: number): TInterval | undefined {
2324
+ if (!this.localCollection) {
2325
+ throw new LoggingError("attachSequence must be called");
2326
+ }
2327
+
2328
+ return this.localCollection.previousInterval(pos);
2329
+ }
2330
+
2331
+ public nextInterval(pos: number): TInterval | undefined {
2332
+ if (!this.localCollection) {
2333
+ throw new LoggingError("attachSequence must be called");
2334
+ }
2335
+
2336
+ return this.localCollection.nextInterval(pos);
2337
+ }
2139
2338
  }
2140
2339
 
2141
2340
  /**
2142
2341
  * Information that identifies an interval within a `Sequence`.
2143
2342
  */
2144
2343
  export interface IntervalLocator {
2145
- /**
2146
- * Label for the collection the interval is a part of
2147
- */
2148
- label: string;
2149
- /**
2150
- * Interval within that collection
2151
- */
2152
- interval: SequenceInterval;
2344
+ /**
2345
+ * Label for the collection the interval is a part of
2346
+ */
2347
+ label: string;
2348
+ /**
2349
+ * Interval within that collection
2350
+ */
2351
+ interval: SequenceInterval;
2153
2352
  }
2154
2353
 
2155
2354
  /**
@@ -2158,10 +2357,12 @@ export interface IntervalLocator {
2158
2357
  * on the merge tree directly by app code), otherwise an {@link IntervalLocator} for the interval this
2159
2358
  * endpoint is a part of.
2160
2359
  */
2161
- export function intervalLocatorFromEndpoint(potentialEndpoint: LocalReferencePosition): IntervalLocator | undefined {
2162
- const {
2163
- interval,
2164
- [reservedRangeLabelsKey]: collectionNameArray,
2165
- } = potentialEndpoint.properties ?? {};
2166
- return (interval && collectionNameArray?.length === 1) ? { label: collectionNameArray[0], interval } : undefined;
2360
+ export function intervalLocatorFromEndpoint(
2361
+ potentialEndpoint: LocalReferencePosition,
2362
+ ): IntervalLocator | undefined {
2363
+ const { interval, [reservedRangeLabelsKey]: collectionNameArray } =
2364
+ potentialEndpoint.properties ?? {};
2365
+ return interval && collectionNameArray?.length === 1
2366
+ ? { label: collectionNameArray[0], interval }
2367
+ : undefined;
2167
2368
  }