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