@fluidframework/sequence 2.0.0-dev.2.2.0.111723 → 2.0.0-dev.3.1.0.125672

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