@fluidframework/sequence 2.0.0-dev.2.3.0.115467 → 2.0.0-dev.4.1.0.148229

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 (96) 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 +204 -177
  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 +15 -3
  15. package/dist/intervalCollection.d.ts.map +1 -1
  16. package/dist/intervalCollection.js +150 -79
  17. package/dist/intervalCollection.js.map +1 -1
  18. package/dist/intervalTree.d.ts.map +1 -1
  19. package/dist/intervalTree.js.map +1 -1
  20. package/dist/localValues.d.ts.map +1 -1
  21. package/dist/localValues.js.map +1 -1
  22. package/dist/packageVersion.d.ts +1 -1
  23. package/dist/packageVersion.js +1 -1
  24. package/dist/packageVersion.js.map +1 -1
  25. package/dist/sequence.d.ts +1 -1
  26. package/dist/sequence.d.ts.map +1 -1
  27. package/dist/sequence.js +16 -42
  28. package/dist/sequence.js.map +1 -1
  29. package/dist/sequenceDeltaEvent.d.ts.map +1 -1
  30. package/dist/sequenceDeltaEvent.js.map +1 -1
  31. package/dist/sequenceFactory.d.ts.map +1 -1
  32. package/dist/sequenceFactory.js.map +1 -1
  33. package/dist/sharedIntervalCollection.d.ts.map +1 -1
  34. package/dist/sharedIntervalCollection.js.map +1 -1
  35. package/dist/sharedSequence.d.ts.map +1 -1
  36. package/dist/sharedSequence.js +3 -3
  37. package/dist/sharedSequence.js.map +1 -1
  38. package/dist/sharedString.d.ts.map +1 -1
  39. package/dist/sharedString.js +5 -4
  40. package/dist/sharedString.js.map +1 -1
  41. package/lib/defaultMap.d.ts.map +1 -1
  42. package/lib/defaultMap.js +6 -5
  43. package/lib/defaultMap.js.map +1 -1
  44. package/lib/defaultMapInterfaces.d.ts.map +1 -1
  45. package/lib/defaultMapInterfaces.js.map +1 -1
  46. package/lib/index.d.ts +2 -2
  47. package/lib/index.d.ts.map +1 -1
  48. package/lib/index.js +2 -2
  49. package/lib/index.js.map +1 -1
  50. package/lib/intervalCollection.d.ts +15 -3
  51. package/lib/intervalCollection.d.ts.map +1 -1
  52. package/lib/intervalCollection.js +150 -79
  53. package/lib/intervalCollection.js.map +1 -1
  54. package/lib/intervalTree.d.ts.map +1 -1
  55. package/lib/intervalTree.js.map +1 -1
  56. package/lib/localValues.d.ts.map +1 -1
  57. package/lib/localValues.js +1 -1
  58. package/lib/localValues.js.map +1 -1
  59. package/lib/packageVersion.d.ts +1 -1
  60. package/lib/packageVersion.js +1 -1
  61. package/lib/packageVersion.js.map +1 -1
  62. package/lib/sequence.d.ts +1 -1
  63. package/lib/sequence.d.ts.map +1 -1
  64. package/lib/sequence.js +17 -43
  65. package/lib/sequence.js.map +1 -1
  66. package/lib/sequenceDeltaEvent.d.ts.map +1 -1
  67. package/lib/sequenceDeltaEvent.js.map +1 -1
  68. package/lib/sequenceFactory.d.ts.map +1 -1
  69. package/lib/sequenceFactory.js +1 -1
  70. package/lib/sequenceFactory.js.map +1 -1
  71. package/lib/sharedIntervalCollection.d.ts.map +1 -1
  72. package/lib/sharedIntervalCollection.js.map +1 -1
  73. package/lib/sharedSequence.d.ts.map +1 -1
  74. package/lib/sharedSequence.js +4 -4
  75. package/lib/sharedSequence.js.map +1 -1
  76. package/lib/sharedString.d.ts.map +1 -1
  77. package/lib/sharedString.js +5 -4
  78. package/lib/sharedString.js.map +1 -1
  79. package/package.json +79 -67
  80. package/prettier.config.cjs +1 -1
  81. package/src/defaultMap.ts +406 -405
  82. package/src/defaultMapInterfaces.ts +120 -115
  83. package/src/index.ts +27 -17
  84. package/src/intervalCollection.ts +2206 -1932
  85. package/src/intervalTree.ts +139 -139
  86. package/src/localValues.ts +64 -73
  87. package/src/packageVersion.ts +1 -1
  88. package/src/sequence.ts +738 -710
  89. package/src/sequenceDeltaEvent.ts +143 -137
  90. package/src/sequenceFactory.ts +48 -46
  91. package/src/sharedIntervalCollection.ts +150 -136
  92. package/src/sharedSequence.ts +165 -160
  93. package/src/sharedString.ts +385 -343
  94. package/tsconfig.esnext.json +6 -6
  95. package/tsconfig.json +8 -12
  96. package/.editorconfig +0 -7
package/src/sequence.ts CHANGED
@@ -4,49 +4,46 @@
4
4
  */
5
5
  import { Deferred, bufferToString, assert } from "@fluidframework/common-utils";
6
6
  import { ChildLogger } from "@fluidframework/telemetry-utils";
7
+ import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
7
8
  import {
8
- ISequencedDocumentMessage,
9
- MessageType,
10
- } from "@fluidframework/protocol-definitions";
11
- import {
12
- IChannelAttributes,
13
- IFluidDataStoreRuntime,
14
- IChannelStorageService,
9
+ IChannelAttributes,
10
+ IFluidDataStoreRuntime,
11
+ IChannelStorageService,
15
12
  } from "@fluidframework/datastore-definitions";
16
13
  import {
17
- Client,
18
- createAnnotateRangeOp,
19
- createGroupOp,
20
- createInsertOp,
21
- createRemoveRangeOp,
22
- ICombiningOp,
23
- IJSONSegment,
24
- IMergeTreeAnnotateMsg,
25
- IMergeTreeDeltaOp,
26
- IMergeTreeGroupMsg,
27
- IMergeTreeOp,
28
- IMergeTreeRemoveMsg,
29
- IRelativePosition,
30
- ISegment,
31
- ISegmentAction,
32
- LocalReferencePosition,
33
- matchProperties,
34
- MergeTreeDeltaType,
35
- PropertySet,
36
- RangeStackMap,
37
- ReferencePosition,
38
- ReferenceType,
39
- MergeTreeRevertibleDriver,
40
- SegmentGroup,
14
+ Client,
15
+ createAnnotateRangeOp,
16
+ createGroupOp,
17
+ createInsertOp,
18
+ createRemoveRangeOp,
19
+ ICombiningOp,
20
+ IJSONSegment,
21
+ IMergeTreeAnnotateMsg,
22
+ IMergeTreeDeltaOp,
23
+ IMergeTreeGroupMsg,
24
+ IMergeTreeOp,
25
+ IMergeTreeRemoveMsg,
26
+ IRelativePosition,
27
+ ISegment,
28
+ ISegmentAction,
29
+ LocalReferencePosition,
30
+ matchProperties,
31
+ MergeTreeDeltaType,
32
+ PropertySet,
33
+ RangeStackMap,
34
+ ReferencePosition,
35
+ ReferenceType,
36
+ MergeTreeRevertibleDriver,
37
+ SegmentGroup,
41
38
  } from "@fluidframework/merge-tree";
42
39
  import { ObjectStoragePartition, SummaryTreeBuilder } from "@fluidframework/runtime-utils";
43
40
  import {
44
- IFluidSerializer,
45
- makeHandlesSerializable,
46
- parseHandles,
47
- SharedObject,
48
- ISharedObjectEvents,
49
- SummarySerializer,
41
+ IFluidSerializer,
42
+ makeHandlesSerializable,
43
+ parseHandles,
44
+ SharedObject,
45
+ ISharedObjectEvents,
46
+ SummarySerializer,
50
47
  } from "@fluidframework/shared-object-base";
51
48
  import { IEventThisPlaceHolder } from "@fluidframework/common-definitions";
52
49
  import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtime-definitions";
@@ -54,9 +51,9 @@ import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtim
54
51
  import { DefaultMap } from "./defaultMap";
55
52
  import { IMapMessageLocalMetadata, IValueChanged } from "./defaultMapInterfaces";
56
53
  import {
57
- IntervalCollection,
58
- SequenceInterval,
59
- SequenceIntervalCollectionValueType,
54
+ IntervalCollection,
55
+ SequenceInterval,
56
+ SequenceIntervalCollectionValueType,
60
57
  } from "./intervalCollection";
61
58
  import { SequenceDeltaEvent, SequenceMaintenanceEvent } from "./sequenceDeltaEvent";
62
59
  import { ISharedIntervalCollection } from "./sharedIntervalCollection";
@@ -98,677 +95,708 @@ const contentPath = "content";
98
95
  * - `target` - The sequence itself.
99
96
  */
100
97
  export interface ISharedSegmentSequenceEvents extends ISharedObjectEvents {
101
- (event: "createIntervalCollection",
102
- listener: (label: string, local: boolean, target: IEventThisPlaceHolder) => void);
103
- (event: "sequenceDelta", listener: (event: SequenceDeltaEvent, target: IEventThisPlaceHolder) => void);
104
- (event: "maintenance",
105
- listener: (event: SequenceMaintenanceEvent, target: IEventThisPlaceHolder) => void);
98
+ (
99
+ event: "createIntervalCollection",
100
+ listener: (label: string, local: boolean, target: IEventThisPlaceHolder) => void,
101
+ );
102
+ (
103
+ event: "sequenceDelta",
104
+ listener: (event: SequenceDeltaEvent, target: IEventThisPlaceHolder) => void,
105
+ );
106
+ (
107
+ event: "maintenance",
108
+ listener: (event: SequenceMaintenanceEvent, target: IEventThisPlaceHolder) => void,
109
+ );
106
110
  }
107
111
 
108
112
  export abstract class SharedSegmentSequence<T extends ISegment>
109
- extends SharedObject<ISharedSegmentSequenceEvents>
110
- implements ISharedIntervalCollection<SequenceInterval>, MergeTreeRevertibleDriver {
111
- get loaded(): Promise<void> {
112
- return this.loadedDeferred.promise;
113
- }
114
-
115
- private static createOpsFromDelta(event: SequenceDeltaEvent): IMergeTreeDeltaOp[] {
116
- const ops: IMergeTreeDeltaOp[] = [];
117
- for (const r of event.ranges) {
118
- switch (event.deltaOperation) {
119
- case MergeTreeDeltaType.ANNOTATE: {
120
- const lastAnnotate = ops[ops.length - 1] as IMergeTreeAnnotateMsg;
121
- const props = {};
122
- for (const key of Object.keys(r.propertyDeltas)) {
123
- props[key] = r.segment.properties?.[key] ?? null;
124
- }
125
- if (lastAnnotate && lastAnnotate.pos2 === r.position &&
126
- matchProperties(lastAnnotate.props, props)) {
127
- lastAnnotate.pos2 += r.segment.cachedLength;
128
- } else {
129
- ops.push(createAnnotateRangeOp(
130
- r.position,
131
- r.position + r.segment.cachedLength,
132
- props,
133
- undefined));
134
- }
135
- break;
136
- }
137
-
138
- case MergeTreeDeltaType.INSERT:
139
- ops.push(createInsertOp(
140
- r.position,
141
- r.segment.clone().toJSONObject()));
142
- break;
143
-
144
- case MergeTreeDeltaType.REMOVE: {
145
- const lastRem = ops[ops.length - 1] as IMergeTreeRemoveMsg;
146
- if (lastRem?.pos1 === r.position) {
147
- assert(lastRem.pos2 !== undefined, 0x3ff /* pos2 should not be undefined here */);
148
- lastRem.pos2 += r.segment.cachedLength;
149
- } else {
150
- ops.push(createRemoveRangeOp(
151
- r.position,
152
- r.position + r.segment.cachedLength));
153
- }
154
- break;
155
- }
156
-
157
- default:
158
- }
159
- }
160
- return ops;
161
- }
162
-
163
- protected client: Client;
164
- /** `Deferred` that triggers once the object is loaded */
165
- protected loadedDeferred = new Deferred<void>();
166
- // cache out going ops created when partial loading
167
- private readonly loadedDeferredOutgoingOps:
168
- [IMergeTreeOp, SegmentGroup | SegmentGroup[]][] = [];
169
- // cache incoming ops that arrive when partial loading
170
- private deferIncomingOps = true;
171
- private readonly loadedDeferredIncomingOps: ISequencedDocumentMessage[] = [];
172
-
173
- private messagesSinceMSNChange: ISequencedDocumentMessage[] = [];
174
- private readonly intervalCollections: DefaultMap<IntervalCollection<SequenceInterval>>;
175
- constructor(
176
- private readonly dataStoreRuntime: IFluidDataStoreRuntime,
177
- public id: string,
178
- attributes: IChannelAttributes,
179
- public readonly segmentFromSpec: (spec: IJSONSegment) => ISegment,
180
- ) {
181
- super(id, dataStoreRuntime, attributes, "fluid_sequence_");
182
-
183
- this.loadedDeferred.promise.catch((error) => {
184
- this.logger.sendErrorEvent({ eventName: "SequenceLoadFailed" }, error);
185
- });
186
-
187
- this.client = new Client(
188
- segmentFromSpec,
189
- ChildLogger.create(this.logger, "SharedSegmentSequence.MergeTreeClient"),
190
- dataStoreRuntime.options);
191
-
192
- super.on("newListener", (event) => {
193
- switch (event) {
194
- case "sequenceDelta":
195
- if (!this.client.mergeTreeDeltaCallback) {
196
- this.client.mergeTreeDeltaCallback = (opArgs, deltaArgs) => {
197
- this.emit("sequenceDelta", new SequenceDeltaEvent(opArgs, deltaArgs, this.client), this);
198
- };
199
- }
200
- break;
201
- case "maintenance":
202
- if (!this.client.mergeTreeMaintenanceCallback) {
203
- this.client.mergeTreeMaintenanceCallback = (args, opArgs) => {
204
- this.emit("maintenance", new SequenceMaintenanceEvent(opArgs, args, this.client), this);
205
- };
206
- }
207
- break;
208
- default:
209
- }
210
- });
211
- super.on("removeListener", (event: string | symbol) => {
212
- switch (event) {
213
- case "sequenceDelta":
214
- if (super.listenerCount(event) === 0) {
215
- this.client.mergeTreeDeltaCallback = undefined;
216
- }
217
- break;
218
- case "maintenance":
219
- if (super.listenerCount(event) === 0) {
220
- this.client.mergeTreeMaintenanceCallback = undefined;
221
- }
222
- break;
223
- default:
224
- break;
225
- }
226
- });
227
-
228
- this.intervalCollections = new DefaultMap(
229
- this.serializer,
230
- this.handle,
231
- (op, localOpMetadata) => this.submitLocalMessage(op, localOpMetadata),
232
- new SequenceIntervalCollectionValueType(),
233
- );
234
- }
235
-
236
- /**
237
- * @param start - The inclusive start of the range to remove
238
- * @param end - The exclusive end of the range to remove
239
- */
240
- public removeRange(start: number, end: number): IMergeTreeRemoveMsg {
241
- const removeOp = this.client.removeRangeLocal(start, end);
242
- this.submitSequenceMessage(removeOp);
243
- return removeOp;
244
- }
245
-
246
- public groupOperation(groupOp: IMergeTreeGroupMsg) {
247
- this.client.localTransaction(groupOp);
248
- this.submitSequenceMessage(groupOp);
249
- }
250
-
251
- /**
252
- * Finds the segment information (i.e. segment + offset) corresponding to a character position in the SharedString.
253
- * If the position is past the end of the string, `segment` and `offset` on the returned object may be undefined.
254
- * @param pos - Character position (index) into the current local view of the SharedString.
255
- */
256
- public getContainingSegment(pos: number): { segment: T | undefined; offset: number | undefined; } {
257
- return this.client.getContainingSegment<T>(pos);
258
- }
259
-
260
- /**
261
- * Returns the length of the current sequence for the client
262
- */
263
- public getLength() {
264
- return this.client.getLength();
265
- }
266
-
267
- /**
268
- * Returns the current position of a segment, and -1 if the segment
269
- * does not exist in this sequence
270
- * @param segment - The segment to get the position of
271
- */
272
- public getPosition(segment: ISegment): number {
273
- return this.client.getPosition(segment);
274
- }
275
-
276
- /**
277
- * Annotates the range with the provided properties
278
- *
279
- * @param start - The inclusive start position of the range to annotate
280
- * @param end - The exclusive end position of the range to annotate
281
- * @param props - The properties to annotate the range with
282
- * @param combiningOp - Optional. Specifies how to combine values for the property, such as "incr" for increment.
283
- *
284
- */
285
- public annotateRange(
286
- start: number,
287
- end: number,
288
- props: PropertySet,
289
- combiningOp?: ICombiningOp) {
290
- const annotateOp =
291
- this.client.annotateRangeLocal(start, end, props, combiningOp);
292
- if (annotateOp) {
293
- this.submitSequenceMessage(annotateOp);
294
- }
295
- }
296
-
297
- public getPropertiesAtPosition(pos: number) {
298
- return this.client.getPropertiesAtPosition(pos);
299
- }
300
-
301
- public getRangeExtentsOfPosition(pos: number) {
302
- return this.client.getRangeExtentsOfPosition(pos);
303
- }
304
-
305
- /**
306
- * Creates a `LocalReferencePosition` on this SharedString. If the refType does not include
307
- * ReferenceType.Transient, the returned reference will be added to the localRefs on the provided segment.
308
- * @param segment - Segment to add the local reference on
309
- * @param offset - Offset on the segment at which to place the local reference
310
- * @param refType - ReferenceType for the created local reference
311
- * @param properties - PropertySet to place on the created local reference
312
- */
313
- public createLocalReferencePosition(
314
- segment: T,
315
- offset: number,
316
- refType: ReferenceType,
317
- properties: PropertySet | undefined): LocalReferencePosition {
318
- return this.client.createLocalReferencePosition(
319
- segment,
320
- offset,
321
- refType,
322
- properties);
323
- }
324
-
325
- /**
326
- * Resolves a `ReferencePosition` into a character position using this client's perspective.
327
- */
328
- public localReferencePositionToPosition(lref: ReferencePosition): number {
329
- return this.client.localReferencePositionToPosition(lref);
330
- }
331
-
332
- /**
333
- * Removes a `LocalReferencePosition` from this SharedString.
334
- */
335
- public removeLocalReferencePosition(lref: LocalReferencePosition) {
336
- return this.client.removeLocalReferencePosition(lref);
337
- }
338
-
339
- /**
340
- * Resolves a remote client's position against the local sequence
341
- * and returns the remote client's position relative to the local
342
- * sequence. The client ref seq must be above the minimum sequence number
343
- * or the return value will be undefined.
344
- * Generally this method is used in conjunction with signals which provide
345
- * point in time values for the below parameters, and is useful for things
346
- * like displaying user position. It should not be used with persisted values
347
- * as persisted values will quickly become invalid as the remoteClientRefSeq
348
- * moves below the minimum sequence number
349
- * @param remoteClientPosition - The remote client's position to resolve
350
- * @param remoteClientRefSeq - The reference sequence number of the remote client
351
- * @param remoteClientId - The client id of the remote client
352
- */
353
- public resolveRemoteClientPosition(
354
- remoteClientPosition: number,
355
- remoteClientRefSeq: number,
356
- remoteClientId: string): number | undefined {
357
- return this.client.resolveRemoteClientPosition(
358
- remoteClientPosition,
359
- remoteClientRefSeq,
360
- remoteClientId,
361
- );
362
- }
363
-
364
- public submitSequenceMessage(message: IMergeTreeOp) {
365
- if (!this.isAttached()) {
366
- return;
367
- }
368
- const translated = makeHandlesSerializable(message, this.serializer, this.handle);
369
- const metadata = this.client.peekPendingSegmentGroups(
370
- message.type === MergeTreeDeltaType.GROUP ? message.ops.length : 1);
371
-
372
- // if loading isn't complete, we need to cache
373
- // local ops until loading is complete, and then
374
- // they will be resent
375
- if (!this.loadedDeferred.isCompleted) {
376
- this.loadedDeferredOutgoingOps.push(metadata ? [translated, metadata] : translated);
377
- } else {
378
- this.submitLocalMessage(translated, metadata);
379
- }
380
- }
381
-
382
- /**
383
- * Given a position specified relative to a marker id, lookup the marker
384
- * and convert the position to a character position.
385
- * @param relativePos - Id of marker (may be indirect) and whether position is before or after marker.
386
- */
387
- public posFromRelativePos(relativePos: IRelativePosition) {
388
- return this.client.posFromRelativePos(relativePos);
389
- }
390
-
391
- /**
392
- * Walk the underlying segments of the sequence.
393
- * The walked segments may extend beyond the range
394
- * if the segments cross the ranges start or end boundaries.
395
- * Set split range to true to ensure only segments within the
396
- * range are walked.
397
- *
398
- * @param handler - The function to handle each segment
399
- * @param start - Optional. The start of range walk.
400
- * @param end - Optional. The end of range walk
401
- * @param accum - Optional. An object that will be passed to the handler for accumulation
402
- * @param splitRange - Optional. Splits boundary segments on the range boundaries
403
- */
404
- public walkSegments<TClientData>(
405
- handler: ISegmentAction<TClientData>,
406
- start?: number,
407
- end?: number,
408
- accum?: TClientData,
409
- splitRange: boolean = false,
410
- ): void {
411
- this.client.walkSegments(handler, start, end, accum as TClientData, splitRange);
412
- }
413
-
414
- public getStackContext(startPos: number, rangeLabels: string[]): RangeStackMap {
415
- return this.client.getStackContext(startPos, rangeLabels);
416
- }
417
-
418
- /**
419
- * @returns - The most recent sequence number which has been acked by the server and processed by this
420
- * SharedSegmentSequence.
421
- */
422
- public getCurrentSeq() {
423
- return this.client.getCurrentSeq();
424
- }
425
-
426
- /**
427
- * Inserts a segment directly before a `ReferencePosition`.
428
- * @param refPos - The reference position to insert the segment at
429
- * @param segment - The segment to insert
430
- */
431
- public insertAtReferencePosition(pos: ReferencePosition, segment: T) {
432
- const insertOp = this.client.insertAtReferencePositionLocal(pos, segment);
433
- if (insertOp) {
434
- this.submitSequenceMessage(insertOp);
435
- }
436
- }
437
- /**
438
- * Inserts a segment
439
- * @param start - The position to insert the segment at
440
- * @param spec - The segment to inserts spec
441
- */
442
- public insertFromSpec(pos: number, spec: IJSONSegment) {
443
- const segment = this.segmentFromSpec(spec);
444
- const insertOp = this.client.insertSegmentLocal(pos, segment);
445
- if (insertOp) {
446
- this.submitSequenceMessage(insertOp);
447
- }
448
- }
449
-
450
- /**
451
- * Retrieves the interval collection keyed on `label`. If no such interval collection exists,
452
- * creates one.
453
- */
454
- public getIntervalCollection(label: string): IntervalCollection<SequenceInterval> {
455
- return this.intervalCollections.get(label);
456
- }
457
-
458
- /**
459
- * @returns An iterable object that enumerates the IntervalCollection labels.
460
- *
461
- * @example
462
- * ```typescript
463
- * const iter = this.getIntervalCollectionKeys();
464
- * for (key of iter)
465
- * const collection = this.getIntervalCollection(key);
466
- * ...
467
- * ```
468
- */
469
- public getIntervalCollectionLabels(): IterableIterator<string> {
470
- return this.intervalCollections.keys();
471
- }
472
-
473
- /**
474
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
475
- */
476
- protected summarizeCore(
477
- serializer: IFluidSerializer,
478
- telemetryContext?: ITelemetryContext,
479
- ): ISummaryTreeWithStats {
480
- const builder = new SummaryTreeBuilder();
481
-
482
- // conditionally write the interval collection blob
483
- // only if it has entries
484
- if (this.intervalCollections.size > 0) {
485
- builder.addBlob(snapshotFileName, this.intervalCollections.serialize(serializer));
486
- }
487
-
488
- builder.addWithStats(contentPath, this.summarizeMergeTree(serializer));
489
-
490
- return builder.getSummaryTree();
491
- }
492
-
493
- /**
494
- * Runs serializer over the GC data for this SharedMatrix.
495
- * All the IFluidHandle's represent routes to other objects.
496
- */
497
- protected processGCDataCore(serializer: SummarySerializer) {
498
- if (this.intervalCollections.size > 0) {
499
- this.intervalCollections.serialize(serializer);
500
- }
501
-
502
- this.client.serializeGCData(this.handle, serializer);
503
- }
504
-
505
- /**
506
- * Replace the range specified from start to end with the provided segment
507
- * This is done by inserting the segment at the end of the range, followed
508
- * by removing the contents of the range
509
- * For a zero or reverse range (start \>= end), insert at end do not remove anything
510
- * @param start - The start of the range to replace
511
- * @param end - The end of the range to replace
512
- * @param segment - The segment that will replace the range
513
- */
514
- protected replaceRange(start: number, end: number, segment: ISegment) {
515
- // Insert at the max end of the range when start > end, but still remove the range later
516
- const insertIndex: number = Math.max(start, end);
517
-
518
- // Insert first, so local references can slide to the inserted seg if any
519
- const insert = this.client.insertSegmentLocal(insertIndex, segment);
520
- if (insert) {
521
- if (start < end) {
522
- const remove = this.client.removeRangeLocal(start, end);
523
- const op = remove ? createGroupOp(insert, remove) : insert;
524
- this.submitSequenceMessage(op);
525
- } else {
526
- this.submitSequenceMessage(insert);
527
- }
528
- }
529
- }
530
-
531
- /**
532
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onConnect}
533
- */
534
- protected onConnect() {
535
- // Update merge tree collaboration information with new client ID and then resend pending ops
536
- this.client.startOrUpdateCollaboration(this.runtime.clientId);
537
- }
538
-
539
- /**
540
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
541
- */
542
- protected onDisconnect() { }
543
-
544
- /**
545
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
546
- */
547
- protected reSubmitCore(content: any, localOpMetadata: unknown) {
548
- if (!this.intervalCollections.tryResubmitMessage(content, localOpMetadata as IMapMessageLocalMetadata)) {
549
- this.submitSequenceMessage(
550
- this.client.regeneratePendingOp(
551
- content as IMergeTreeOp,
552
- localOpMetadata as SegmentGroup | SegmentGroup[]));
553
- }
554
- }
555
-
556
- /**
557
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
558
- */
559
- protected async loadCore(storage: IChannelStorageService) {
560
- if (await storage.contains(snapshotFileName)) {
561
- const blob = await storage.readBlob(snapshotFileName);
562
- const header = bufferToString(blob, "utf8");
563
- this.intervalCollections.populate(header);
564
- }
565
-
566
- try {
567
- // this will load the header, and return a promise
568
- // that will resolve when the body is loaded
569
- // and the catchup ops are available.
570
- const { catchupOpsP } = await this.client.load(
571
- this.runtime,
572
- new ObjectStoragePartition(storage, contentPath),
573
- this.serializer);
574
-
575
- // setup a promise to process the
576
- // catch up ops, and finishing the loading process
577
- const loadCatchUpOps = catchupOpsP
578
- .then((msgs) => {
579
- msgs.forEach((m) => {
580
- const collabWindow = this.client.getCollabWindow();
581
- if (m.minimumSequenceNumber < collabWindow.minSeq
582
- || m.referenceSequenceNumber < collabWindow.minSeq
583
- || m.sequenceNumber <= collabWindow.minSeq
584
- || m.sequenceNumber <= collabWindow.currentSeq) {
585
- throw new Error(`Invalid catchup operations in snapshot: ${JSON.stringify({
586
- op: {
587
- seq: m.sequenceNumber,
588
- minSeq: m.minimumSequenceNumber,
589
- refSeq: m.referenceSequenceNumber,
590
- },
591
- collabWindow: {
592
- seq: collabWindow.currentSeq,
593
- minSeq: collabWindow.minSeq,
594
- },
595
- })}`);
596
- }
597
- this.processMergeTreeMsg(m);
598
- });
599
- this.loadFinished();
600
- })
601
- .catch((error) => {
602
- this.loadFinished(error);
603
- });
604
- if (this.dataStoreRuntime.options?.sequenceInitializeFromHeaderOnly !== true) {
605
- // if we not doing partial load, await the catch up ops,
606
- // and the finalization of the load
607
- await loadCatchUpOps;
608
- }
609
- } catch (error) {
610
- this.loadFinished(error);
611
- }
612
- }
613
-
614
- /**
615
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
616
- */
617
- protected processCore(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown) {
618
- // if loading isn't complete, we need to cache all
619
- // incoming ops to be applied after loading is complete
620
- if (this.deferIncomingOps) {
621
- assert(!local, 0x072 /* "Unexpected local op when loading not finished" */);
622
- this.loadedDeferredIncomingOps.push(message);
623
- } else {
624
- assert(message.type === MessageType.Operation, 0x073 /* "Sequence message not operation" */);
625
-
626
- const handled = this.intervalCollections.tryProcessMessage(
627
- message.contents,
628
- local,
629
- message,
630
- localOpMetadata,
631
- );
632
-
633
- if (!handled) {
634
- this.processMergeTreeMsg(message, local);
635
- }
636
- }
637
- }
638
-
639
- /**
640
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.didAttach}
641
- */
642
- protected didAttach() {
643
- // If we are not local, and we've attached we need to start generating and sending ops
644
- // so start collaboration and provide a default client id incase we are not connected
645
- if (this.isAttached()) {
646
- this.client.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
647
- }
648
- }
649
-
650
- /**
651
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.initializeLocalCore}
652
- */
653
- protected initializeLocalCore() {
654
- super.initializeLocalCore();
655
- this.loadFinished();
656
- }
657
-
658
- /**
659
- * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
660
- */
661
- protected applyStashedOp(content: any): unknown {
662
- return this.client.applyStashedOp(content);
663
- }
664
-
665
- private summarizeMergeTree(serializer: IFluidSerializer): ISummaryTreeWithStats {
666
- // Are we fully loaded? If not, things will go south
667
- assert(this.loadedDeferred.isCompleted, 0x074 /* "Snapshot called when not fully loaded" */);
668
- const minSeq = this.runtime.deltaManager.minimumSequenceNumber;
669
-
670
- this.processMinSequenceNumberChanged(minSeq);
671
-
672
- this.messagesSinceMSNChange.forEach((m) => { m.minimumSequenceNumber = minSeq; });
673
-
674
- return this.client.summarize(this.runtime, this.handle, serializer, this.messagesSinceMSNChange);
675
- }
676
-
677
- private processMergeTreeMsg(rawMessage: ISequencedDocumentMessage, local?: boolean) {
678
- const message = parseHandles(rawMessage, this.serializer);
679
-
680
- const ops: IMergeTreeDeltaOp[] = [];
681
- function transformOps(event: SequenceDeltaEvent) {
682
- ops.push(...SharedSegmentSequence.createOpsFromDelta(event));
683
- }
684
- const needsTransformation = message.referenceSequenceNumber !== message.sequenceNumber - 1;
685
- let stashMessage: Readonly<ISequencedDocumentMessage> = message;
686
- if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
687
- if (needsTransformation) {
688
- this.on("sequenceDelta", transformOps);
689
- }
690
- }
691
-
692
- this.client.applyMsg(message, local);
693
-
694
- if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
695
- if (needsTransformation) {
696
- this.removeListener("sequenceDelta", transformOps);
697
- // shallow clone the message as we only overwrite top level properties,
698
- // like referenceSequenceNumber and content only
699
- stashMessage = {
700
- ...message,
701
- referenceSequenceNumber: stashMessage.sequenceNumber - 1,
702
- contents: ops.length !== 1 ? createGroupOp(...ops) : ops[0],
703
- };
704
- }
705
-
706
- this.messagesSinceMSNChange.push(stashMessage);
707
-
708
- // Do GC every once in a while...
709
- if (this.messagesSinceMSNChange.length > 20
710
- && this.messagesSinceMSNChange[20].sequenceNumber < message.minimumSequenceNumber) {
711
- this.processMinSequenceNumberChanged(message.minimumSequenceNumber);
712
- }
713
- }
714
- }
715
-
716
- private processMinSequenceNumberChanged(minSeq: number) {
717
- let index = 0;
718
- for (; index < this.messagesSinceMSNChange.length; index++) {
719
- if (this.messagesSinceMSNChange[index].sequenceNumber > minSeq) {
720
- break;
721
- }
722
- }
723
- if (index !== 0) {
724
- this.messagesSinceMSNChange = this.messagesSinceMSNChange.slice(index);
725
- }
726
- }
727
-
728
- private loadFinished(error?: any) {
729
- if (!this.loadedDeferred.isCompleted) {
730
- // Initialize the interval collections
731
- this.initializeIntervalCollections();
732
- if (error) {
733
- this.loadedDeferred.reject(error);
734
- throw error;
735
- } else {
736
- // it is important this series remains synchronous
737
- // first we stop deferring incoming ops, and apply then all
738
- this.deferIncomingOps = false;
739
- for (const message of this.loadedDeferredIncomingOps) {
740
- this.processCore(message, false, undefined);
741
- }
742
- this.loadedDeferredIncomingOps.length = 0;
743
-
744
- // then resolve the loaded promise
745
- // and resubmit all the outstanding ops, as the snapshot
746
- // is fully loaded, and all outstanding ops are applied
747
- this.loadedDeferred.resolve();
748
-
749
- for (const [messageContent, opMetadata] of this.loadedDeferredOutgoingOps) {
750
- this.reSubmitCore(messageContent, opMetadata);
751
- }
752
- this.loadedDeferredOutgoingOps.length = 0;
753
- }
754
- }
755
- }
756
-
757
- private initializeIntervalCollections() {
758
- // Listen and initialize new SharedIntervalCollections
759
- this.intervalCollections.eventEmitter.on("create", ({ key, previousValue }: IValueChanged, local: boolean) => {
760
- const intervalCollection = this.intervalCollections.get(key);
761
- if (!intervalCollection.attached) {
762
- intervalCollection.attachGraph(this.client, key);
763
- }
764
- assert(previousValue === undefined, 0x2c1 /* "Creating an interval collection that already exists?" */);
765
- this.emit("createIntervalCollection", key, local, this);
766
- });
767
-
768
- // Initialize existing SharedIntervalCollections
769
- for (const key of this.intervalCollections.keys()) {
770
- const intervalCollection = this.intervalCollections.get(key);
771
- intervalCollection.attachGraph(this.client, key);
772
- }
773
- }
113
+ extends SharedObject<ISharedSegmentSequenceEvents>
114
+ implements ISharedIntervalCollection<SequenceInterval>, MergeTreeRevertibleDriver
115
+ {
116
+ get loaded(): Promise<void> {
117
+ return this.loadedDeferred.promise;
118
+ }
119
+
120
+ private static createOpsFromDelta(event: SequenceDeltaEvent): IMergeTreeDeltaOp[] {
121
+ const ops: IMergeTreeDeltaOp[] = [];
122
+ for (const r of event.ranges) {
123
+ switch (event.deltaOperation) {
124
+ case MergeTreeDeltaType.ANNOTATE: {
125
+ const lastAnnotate = ops[ops.length - 1] as IMergeTreeAnnotateMsg;
126
+ const props = {};
127
+ for (const key of Object.keys(r.propertyDeltas)) {
128
+ props[key] = r.segment.properties?.[key] ?? null;
129
+ }
130
+ if (
131
+ lastAnnotate &&
132
+ lastAnnotate.pos2 === r.position &&
133
+ matchProperties(lastAnnotate.props, props)
134
+ ) {
135
+ lastAnnotate.pos2 += r.segment.cachedLength;
136
+ } else {
137
+ ops.push(
138
+ createAnnotateRangeOp(
139
+ r.position,
140
+ r.position + r.segment.cachedLength,
141
+ props,
142
+ undefined,
143
+ ),
144
+ );
145
+ }
146
+ break;
147
+ }
148
+
149
+ case MergeTreeDeltaType.INSERT:
150
+ ops.push(createInsertOp(r.position, r.segment.clone().toJSONObject()));
151
+ break;
152
+
153
+ case MergeTreeDeltaType.REMOVE: {
154
+ const lastRem = ops[ops.length - 1] as IMergeTreeRemoveMsg;
155
+ if (lastRem?.pos1 === r.position) {
156
+ assert(
157
+ lastRem.pos2 !== undefined,
158
+ 0x3ff /* pos2 should not be undefined here */,
159
+ );
160
+ lastRem.pos2 += r.segment.cachedLength;
161
+ } else {
162
+ ops.push(
163
+ createRemoveRangeOp(r.position, r.position + r.segment.cachedLength),
164
+ );
165
+ }
166
+ break;
167
+ }
168
+
169
+ default:
170
+ }
171
+ }
172
+ return ops;
173
+ }
174
+
175
+ protected client: Client;
176
+ /** `Deferred` that triggers once the object is loaded */
177
+ protected loadedDeferred = new Deferred<void>();
178
+ // cache out going ops created when partial loading
179
+ private readonly loadedDeferredOutgoingOps: [IMergeTreeOp, SegmentGroup | SegmentGroup[]][] =
180
+ [];
181
+ // cache incoming ops that arrive when partial loading
182
+ private deferIncomingOps = true;
183
+ private readonly loadedDeferredIncomingOps: ISequencedDocumentMessage[] = [];
184
+
185
+ private messagesSinceMSNChange: ISequencedDocumentMessage[] = [];
186
+ private readonly intervalCollections: DefaultMap<IntervalCollection<SequenceInterval>>;
187
+ constructor(
188
+ private readonly dataStoreRuntime: IFluidDataStoreRuntime,
189
+ public id: string,
190
+ attributes: IChannelAttributes,
191
+ public readonly segmentFromSpec: (spec: IJSONSegment) => ISegment,
192
+ ) {
193
+ super(id, dataStoreRuntime, attributes, "fluid_sequence_");
194
+
195
+ this.loadedDeferred.promise.catch((error) => {
196
+ this.logger.sendErrorEvent({ eventName: "SequenceLoadFailed" }, error);
197
+ });
198
+
199
+ this.client = new Client(
200
+ segmentFromSpec,
201
+ ChildLogger.create(this.logger, "SharedSegmentSequence.MergeTreeClient"),
202
+ dataStoreRuntime.options,
203
+ );
204
+
205
+ this.client.on("delta", (opArgs, deltaArgs) => {
206
+ this.emit(
207
+ "sequenceDelta",
208
+ new SequenceDeltaEvent(opArgs, deltaArgs, this.client),
209
+ this,
210
+ );
211
+ });
212
+
213
+ this.client.on("maintenance", (args, opArgs) => {
214
+ this.emit("maintenance", new SequenceMaintenanceEvent(opArgs, args, this.client), this);
215
+ });
216
+
217
+ this.intervalCollections = new DefaultMap(
218
+ this.serializer,
219
+ this.handle,
220
+ (op, localOpMetadata) => this.submitLocalMessage(op, localOpMetadata),
221
+ new SequenceIntervalCollectionValueType(),
222
+ );
223
+ }
224
+
225
+ /**
226
+ * @param start - The inclusive start of the range to remove
227
+ * @param end - The exclusive end of the range to remove
228
+ */
229
+ public removeRange(start: number, end: number): IMergeTreeRemoveMsg {
230
+ const removeOp = this.client.removeRangeLocal(start, end);
231
+ this.submitSequenceMessage(removeOp);
232
+ return removeOp;
233
+ }
234
+
235
+ public groupOperation(groupOp: IMergeTreeGroupMsg) {
236
+ this.client.localTransaction(groupOp);
237
+ this.submitSequenceMessage(groupOp);
238
+ }
239
+
240
+ /**
241
+ * Finds the segment information (i.e. segment + offset) corresponding to a character position in the SharedString.
242
+ * If the position is past the end of the string, `segment` and `offset` on the returned object may be undefined.
243
+ * @param pos - Character position (index) into the current local view of the SharedString.
244
+ */
245
+ public getContainingSegment(pos: number): {
246
+ segment: T | undefined;
247
+ offset: number | undefined;
248
+ } {
249
+ return this.client.getContainingSegment<T>(pos);
250
+ }
251
+
252
+ /**
253
+ * Returns the length of the current sequence for the client
254
+ */
255
+ public getLength() {
256
+ return this.client.getLength();
257
+ }
258
+
259
+ /**
260
+ * Returns the current position of a segment, and -1 if the segment
261
+ * does not exist in this sequence
262
+ * @param segment - The segment to get the position of
263
+ */
264
+ public getPosition(segment: ISegment): number {
265
+ return this.client.getPosition(segment);
266
+ }
267
+
268
+ /**
269
+ * Annotates the range with the provided properties
270
+ *
271
+ * @param start - The inclusive start position of the range to annotate
272
+ * @param end - The exclusive end position of the range to annotate
273
+ * @param props - The properties to annotate the range with
274
+ * @param combiningOp - Optional. Specifies how to combine values for the property, such as "incr" for increment.
275
+ *
276
+ */
277
+ public annotateRange(
278
+ start: number,
279
+ end: number,
280
+ props: PropertySet,
281
+ combiningOp?: ICombiningOp,
282
+ ) {
283
+ const annotateOp = this.client.annotateRangeLocal(start, end, props, combiningOp);
284
+ if (annotateOp) {
285
+ this.submitSequenceMessage(annotateOp);
286
+ }
287
+ }
288
+
289
+ public getPropertiesAtPosition(pos: number) {
290
+ return this.client.getPropertiesAtPosition(pos);
291
+ }
292
+
293
+ public getRangeExtentsOfPosition(pos: number) {
294
+ return this.client.getRangeExtentsOfPosition(pos);
295
+ }
296
+
297
+ /**
298
+ * Creates a `LocalReferencePosition` on this SharedString. If the refType does not include
299
+ * ReferenceType.Transient, the returned reference will be added to the localRefs on the provided segment.
300
+ * @param segment - Segment to add the local reference on
301
+ * @param offset - Offset on the segment at which to place the local reference
302
+ * @param refType - ReferenceType for the created local reference
303
+ * @param properties - PropertySet to place on the created local reference
304
+ */
305
+ public createLocalReferencePosition(
306
+ segment: T,
307
+ offset: number,
308
+ refType: ReferenceType,
309
+ properties: PropertySet | undefined,
310
+ ): LocalReferencePosition {
311
+ return this.client.createLocalReferencePosition(segment, offset, refType, properties);
312
+ }
313
+
314
+ /**
315
+ * Resolves a `ReferencePosition` into a character position using this client's perspective.
316
+ */
317
+ public localReferencePositionToPosition(lref: ReferencePosition): number {
318
+ return this.client.localReferencePositionToPosition(lref);
319
+ }
320
+
321
+ /**
322
+ * Removes a `LocalReferencePosition` from this SharedString.
323
+ */
324
+ public removeLocalReferencePosition(lref: LocalReferencePosition) {
325
+ return this.client.removeLocalReferencePosition(lref);
326
+ }
327
+
328
+ /**
329
+ * Resolves a remote client's position against the local sequence
330
+ * and returns the remote client's position relative to the local
331
+ * sequence. The client ref seq must be above the minimum sequence number
332
+ * or the return value will be undefined.
333
+ * Generally this method is used in conjunction with signals which provide
334
+ * point in time values for the below parameters, and is useful for things
335
+ * like displaying user position. It should not be used with persisted values
336
+ * as persisted values will quickly become invalid as the remoteClientRefSeq
337
+ * moves below the minimum sequence number
338
+ * @param remoteClientPosition - The remote client's position to resolve
339
+ * @param remoteClientRefSeq - The reference sequence number of the remote client
340
+ * @param remoteClientId - The client id of the remote client
341
+ */
342
+ public resolveRemoteClientPosition(
343
+ remoteClientPosition: number,
344
+ remoteClientRefSeq: number,
345
+ remoteClientId: string,
346
+ ): number | undefined {
347
+ return this.client.resolveRemoteClientPosition(
348
+ remoteClientPosition,
349
+ remoteClientRefSeq,
350
+ remoteClientId,
351
+ );
352
+ }
353
+
354
+ public submitSequenceMessage(message: IMergeTreeOp) {
355
+ if (!this.isAttached()) {
356
+ return;
357
+ }
358
+ const translated = makeHandlesSerializable(message, this.serializer, this.handle);
359
+ const metadata = this.client.peekPendingSegmentGroups(
360
+ message.type === MergeTreeDeltaType.GROUP ? message.ops.length : 1,
361
+ );
362
+
363
+ // if loading isn't complete, we need to cache
364
+ // local ops until loading is complete, and then
365
+ // they will be resent
366
+ if (!this.loadedDeferred.isCompleted) {
367
+ this.loadedDeferredOutgoingOps.push(metadata ? [translated, metadata] : translated);
368
+ } else {
369
+ this.submitLocalMessage(translated, metadata);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Given a position specified relative to a marker id, lookup the marker
375
+ * and convert the position to a character position.
376
+ * @param relativePos - Id of marker (may be indirect) and whether position is before or after marker.
377
+ */
378
+ public posFromRelativePos(relativePos: IRelativePosition) {
379
+ return this.client.posFromRelativePos(relativePos);
380
+ }
381
+
382
+ /**
383
+ * Walk the underlying segments of the sequence.
384
+ * The walked segments may extend beyond the range
385
+ * if the segments cross the ranges start or end boundaries.
386
+ * Set split range to true to ensure only segments within the
387
+ * range are walked.
388
+ *
389
+ * @param handler - The function to handle each segment
390
+ * @param start - Optional. The start of range walk.
391
+ * @param end - Optional. The end of range walk
392
+ * @param accum - Optional. An object that will be passed to the handler for accumulation
393
+ * @param splitRange - Optional. Splits boundary segments on the range boundaries
394
+ */
395
+ public walkSegments<TClientData>(
396
+ handler: ISegmentAction<TClientData>,
397
+ start?: number,
398
+ end?: number,
399
+ accum?: TClientData,
400
+ splitRange: boolean = false,
401
+ ): void {
402
+ this.client.walkSegments(handler, start, end, accum as TClientData, splitRange);
403
+ }
404
+
405
+ public getStackContext(startPos: number, rangeLabels: string[]): RangeStackMap {
406
+ return this.client.getStackContext(startPos, rangeLabels);
407
+ }
408
+
409
+ /**
410
+ * @returns - The most recent sequence number which has been acked by the server and processed by this
411
+ * SharedSegmentSequence.
412
+ */
413
+ public getCurrentSeq() {
414
+ return this.client.getCurrentSeq();
415
+ }
416
+
417
+ /**
418
+ * Inserts a segment directly before a `ReferencePosition`.
419
+ * @param refPos - The reference position to insert the segment at
420
+ * @param segment - The segment to insert
421
+ */
422
+ public insertAtReferencePosition(pos: ReferencePosition, segment: T) {
423
+ const insertOp = this.client.insertAtReferencePositionLocal(pos, segment);
424
+ if (insertOp) {
425
+ this.submitSequenceMessage(insertOp);
426
+ }
427
+ }
428
+ /**
429
+ * Inserts a segment
430
+ * @param start - The position to insert the segment at
431
+ * @param spec - The segment to inserts spec
432
+ */
433
+ public insertFromSpec(pos: number, spec: IJSONSegment) {
434
+ const segment = this.segmentFromSpec(spec);
435
+ const insertOp = this.client.insertSegmentLocal(pos, segment);
436
+ if (insertOp) {
437
+ this.submitSequenceMessage(insertOp);
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Retrieves the interval collection keyed on `label`. If no such interval collection exists,
443
+ * creates one.
444
+ */
445
+ public getIntervalCollection(label: string): IntervalCollection<SequenceInterval> {
446
+ return this.intervalCollections.get(label);
447
+ }
448
+
449
+ /**
450
+ * @returns An iterable object that enumerates the IntervalCollection labels.
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * const iter = this.getIntervalCollectionKeys();
455
+ * for (key of iter)
456
+ * const collection = this.getIntervalCollection(key);
457
+ * ...
458
+ * ```
459
+ */
460
+ public getIntervalCollectionLabels(): IterableIterator<string> {
461
+ return this.intervalCollections.keys();
462
+ }
463
+
464
+ /**
465
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
466
+ */
467
+ protected summarizeCore(
468
+ serializer: IFluidSerializer,
469
+ telemetryContext?: ITelemetryContext,
470
+ ): ISummaryTreeWithStats {
471
+ const builder = new SummaryTreeBuilder();
472
+
473
+ // conditionally write the interval collection blob
474
+ // only if it has entries
475
+ if (this.intervalCollections.size > 0) {
476
+ builder.addBlob(snapshotFileName, this.intervalCollections.serialize(serializer));
477
+ }
478
+
479
+ builder.addWithStats(contentPath, this.summarizeMergeTree(serializer));
480
+
481
+ return builder.getSummaryTree();
482
+ }
483
+
484
+ /**
485
+ * Runs serializer over the GC data for this SharedMatrix.
486
+ * All the IFluidHandle's represent routes to other objects.
487
+ */
488
+ protected processGCDataCore(serializer: SummarySerializer) {
489
+ if (this.intervalCollections.size > 0) {
490
+ this.intervalCollections.serialize(serializer);
491
+ }
492
+
493
+ this.client.serializeGCData(this.handle, serializer);
494
+ }
495
+
496
+ /**
497
+ * Replace the range specified from start to end with the provided segment
498
+ * This is done by inserting the segment at the end of the range, followed
499
+ * by removing the contents of the range
500
+ * For a zero or reverse range (start \>= end), insert at end do not remove anything
501
+ * @param start - The start of the range to replace
502
+ * @param end - The end of the range to replace
503
+ * @param segment - The segment that will replace the range
504
+ */
505
+ protected replaceRange(start: number, end: number, segment: ISegment) {
506
+ // Insert at the max end of the range when start > end, but still remove the range later
507
+ const insertIndex: number = Math.max(start, end);
508
+
509
+ // Insert first, so local references can slide to the inserted seg if any
510
+ const insert = this.client.insertSegmentLocal(insertIndex, segment);
511
+ if (insert) {
512
+ if (start < end) {
513
+ const remove = this.client.removeRangeLocal(start, end);
514
+ const op = remove ? createGroupOp(insert, remove) : insert;
515
+ this.submitSequenceMessage(op);
516
+ } else {
517
+ this.submitSequenceMessage(insert);
518
+ }
519
+ }
520
+ }
521
+
522
+ /**
523
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onConnect}
524
+ */
525
+ protected onConnect() {
526
+ // Update merge tree collaboration information with new client ID and then resend pending ops
527
+ this.client.startOrUpdateCollaboration(this.runtime.clientId);
528
+ }
529
+
530
+ /**
531
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
532
+ */
533
+ protected onDisconnect() {}
534
+
535
+ /**
536
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
537
+ */
538
+ protected reSubmitCore(content: any, localOpMetadata: unknown) {
539
+ if (
540
+ !this.intervalCollections.tryResubmitMessage(
541
+ content,
542
+ localOpMetadata as IMapMessageLocalMetadata,
543
+ )
544
+ ) {
545
+ this.submitSequenceMessage(
546
+ this.client.regeneratePendingOp(
547
+ content as IMergeTreeOp,
548
+ localOpMetadata as SegmentGroup | SegmentGroup[],
549
+ ),
550
+ );
551
+ }
552
+ }
553
+
554
+ /**
555
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
556
+ */
557
+ protected async loadCore(storage: IChannelStorageService) {
558
+ if (await storage.contains(snapshotFileName)) {
559
+ const blob = await storage.readBlob(snapshotFileName);
560
+ const header = bufferToString(blob, "utf8");
561
+ this.intervalCollections.populate(header);
562
+ }
563
+
564
+ try {
565
+ // this will load the header, and return a promise
566
+ // that will resolve when the body is loaded
567
+ // and the catchup ops are available.
568
+ const { catchupOpsP } = await this.client.load(
569
+ this.runtime,
570
+ new ObjectStoragePartition(storage, contentPath),
571
+ this.serializer,
572
+ );
573
+
574
+ // setup a promise to process the
575
+ // catch up ops, and finishing the loading process
576
+ const loadCatchUpOps = catchupOpsP
577
+ .then((msgs) => {
578
+ msgs.forEach((m) => {
579
+ const collabWindow = this.client.getCollabWindow();
580
+ if (
581
+ m.minimumSequenceNumber < collabWindow.minSeq ||
582
+ m.referenceSequenceNumber < collabWindow.minSeq ||
583
+ m.sequenceNumber <= collabWindow.minSeq ||
584
+ m.sequenceNumber <= collabWindow.currentSeq
585
+ ) {
586
+ throw new Error(
587
+ `Invalid catchup operations in snapshot: ${JSON.stringify({
588
+ op: {
589
+ seq: m.sequenceNumber,
590
+ minSeq: m.minimumSequenceNumber,
591
+ refSeq: m.referenceSequenceNumber,
592
+ },
593
+ collabWindow: {
594
+ seq: collabWindow.currentSeq,
595
+ minSeq: collabWindow.minSeq,
596
+ },
597
+ })}`,
598
+ );
599
+ }
600
+ this.processMergeTreeMsg(m);
601
+ });
602
+ this.loadFinished();
603
+ })
604
+ .catch((error) => {
605
+ this.loadFinished(error);
606
+ });
607
+ if (this.dataStoreRuntime.options?.sequenceInitializeFromHeaderOnly !== true) {
608
+ // if we not doing partial load, await the catch up ops,
609
+ // and the finalization of the load
610
+ await loadCatchUpOps;
611
+ }
612
+ } catch (error) {
613
+ this.loadFinished(error);
614
+ }
615
+ }
616
+
617
+ /**
618
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
619
+ */
620
+ protected processCore(
621
+ message: ISequencedDocumentMessage,
622
+ local: boolean,
623
+ localOpMetadata: unknown,
624
+ ) {
625
+ // if loading isn't complete, we need to cache all
626
+ // incoming ops to be applied after loading is complete
627
+ if (this.deferIncomingOps) {
628
+ assert(!local, 0x072 /* "Unexpected local op when loading not finished" */);
629
+ this.loadedDeferredIncomingOps.push(message);
630
+ } else {
631
+ assert(
632
+ message.type === MessageType.Operation,
633
+ 0x073 /* "Sequence message not operation" */,
634
+ );
635
+
636
+ const handled = this.intervalCollections.tryProcessMessage(
637
+ message.contents,
638
+ local,
639
+ message,
640
+ localOpMetadata,
641
+ );
642
+
643
+ if (!handled) {
644
+ this.processMergeTreeMsg(message, local);
645
+ }
646
+ }
647
+ }
648
+
649
+ /**
650
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.didAttach}
651
+ */
652
+ protected didAttach() {
653
+ // If we are not local, and we've attached we need to start generating and sending ops
654
+ // so start collaboration and provide a default client id incase we are not connected
655
+ if (this.isAttached()) {
656
+ this.client.startOrUpdateCollaboration(this.runtime.clientId ?? "attached");
657
+ }
658
+ }
659
+
660
+ /**
661
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.initializeLocalCore}
662
+ */
663
+ protected initializeLocalCore() {
664
+ super.initializeLocalCore();
665
+ this.loadFinished();
666
+ }
667
+
668
+ /**
669
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
670
+ */
671
+ protected applyStashedOp(content: any): unknown {
672
+ return this.client.applyStashedOp(content);
673
+ }
674
+
675
+ private summarizeMergeTree(serializer: IFluidSerializer): ISummaryTreeWithStats {
676
+ // Are we fully loaded? If not, things will go south
677
+ assert(
678
+ this.loadedDeferred.isCompleted,
679
+ 0x074 /* "Snapshot called when not fully loaded" */,
680
+ );
681
+ const minSeq = this.runtime.deltaManager.minimumSequenceNumber;
682
+
683
+ this.processMinSequenceNumberChanged(minSeq);
684
+
685
+ this.messagesSinceMSNChange.forEach((m) => {
686
+ m.minimumSequenceNumber = minSeq;
687
+ });
688
+
689
+ return this.client.summarize(
690
+ this.runtime,
691
+ this.handle,
692
+ serializer,
693
+ this.messagesSinceMSNChange,
694
+ );
695
+ }
696
+
697
+ private processMergeTreeMsg(rawMessage: ISequencedDocumentMessage, local?: boolean) {
698
+ const message = parseHandles(rawMessage, this.serializer);
699
+
700
+ const ops: IMergeTreeDeltaOp[] = [];
701
+ function transformOps(event: SequenceDeltaEvent) {
702
+ ops.push(...SharedSegmentSequence.createOpsFromDelta(event));
703
+ }
704
+ const needsTransformation = message.referenceSequenceNumber !== message.sequenceNumber - 1;
705
+ let stashMessage: Readonly<ISequencedDocumentMessage> = message;
706
+ if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
707
+ if (needsTransformation) {
708
+ this.on("sequenceDelta", transformOps);
709
+ }
710
+ }
711
+
712
+ this.client.applyMsg(message, local);
713
+
714
+ if (this.runtime.options?.newMergeTreeSnapshotFormat !== true) {
715
+ if (needsTransformation) {
716
+ this.removeListener("sequenceDelta", transformOps);
717
+ // shallow clone the message as we only overwrite top level properties,
718
+ // like referenceSequenceNumber and content only
719
+ stashMessage = {
720
+ ...message,
721
+ referenceSequenceNumber: stashMessage.sequenceNumber - 1,
722
+ contents: ops.length !== 1 ? createGroupOp(...ops) : ops[0],
723
+ };
724
+ }
725
+
726
+ this.messagesSinceMSNChange.push(stashMessage);
727
+
728
+ // Do GC every once in a while...
729
+ if (
730
+ this.messagesSinceMSNChange.length > 20 &&
731
+ this.messagesSinceMSNChange[20].sequenceNumber < message.minimumSequenceNumber
732
+ ) {
733
+ this.processMinSequenceNumberChanged(message.minimumSequenceNumber);
734
+ }
735
+ }
736
+ }
737
+
738
+ private processMinSequenceNumberChanged(minSeq: number) {
739
+ let index = 0;
740
+ for (; index < this.messagesSinceMSNChange.length; index++) {
741
+ if (this.messagesSinceMSNChange[index].sequenceNumber > minSeq) {
742
+ break;
743
+ }
744
+ }
745
+ if (index !== 0) {
746
+ this.messagesSinceMSNChange = this.messagesSinceMSNChange.slice(index);
747
+ }
748
+ }
749
+
750
+ private loadFinished(error?: any) {
751
+ if (!this.loadedDeferred.isCompleted) {
752
+ // Initialize the interval collections
753
+ this.initializeIntervalCollections();
754
+ if (error) {
755
+ this.loadedDeferred.reject(error);
756
+ throw error;
757
+ } else {
758
+ // it is important this series remains synchronous
759
+ // first we stop deferring incoming ops, and apply then all
760
+ this.deferIncomingOps = false;
761
+ for (const message of this.loadedDeferredIncomingOps) {
762
+ this.processCore(message, false, undefined);
763
+ }
764
+ this.loadedDeferredIncomingOps.length = 0;
765
+
766
+ // then resolve the loaded promise
767
+ // and resubmit all the outstanding ops, as the snapshot
768
+ // is fully loaded, and all outstanding ops are applied
769
+ this.loadedDeferred.resolve();
770
+
771
+ for (const [messageContent, opMetadata] of this.loadedDeferredOutgoingOps) {
772
+ this.reSubmitCore(messageContent, opMetadata);
773
+ }
774
+ this.loadedDeferredOutgoingOps.length = 0;
775
+ }
776
+ }
777
+ }
778
+
779
+ private initializeIntervalCollections() {
780
+ // Listen and initialize new SharedIntervalCollections
781
+ this.intervalCollections.eventEmitter.on(
782
+ "create",
783
+ ({ key, previousValue }: IValueChanged, local: boolean) => {
784
+ const intervalCollection = this.intervalCollections.get(key);
785
+ if (!intervalCollection.attached) {
786
+ intervalCollection.attachGraph(this.client, key);
787
+ }
788
+ assert(
789
+ previousValue === undefined,
790
+ 0x2c1 /* "Creating an interval collection that already exists?" */,
791
+ );
792
+ this.emit("createIntervalCollection", key, local, this);
793
+ },
794
+ );
795
+
796
+ // Initialize existing SharedIntervalCollections
797
+ for (const key of this.intervalCollections.keys()) {
798
+ const intervalCollection = this.intervalCollections.get(key);
799
+ intervalCollection.attachGraph(this.client, key);
800
+ }
801
+ }
774
802
  }