@fluidframework/sequence 0.59.4002 → 1.0.2

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.
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
9
9
  import { IEvent } from "@fluidframework/common-definitions";
10
+ import { UsageError } from "@fluidframework/container-utils";
10
11
  import {
11
12
  addProperties,
12
13
  Client,
@@ -17,12 +18,14 @@ import {
17
18
  IntervalConflictResolver,
18
19
  IntervalNode,
19
20
  IntervalTree,
21
+ ISegment,
20
22
  LocalReference,
21
23
  MergeTreeDeltaType,
22
24
  PropertiesManager,
23
25
  PropertySet,
24
26
  RedBlackTree,
25
27
  ReferenceType,
28
+ refTypeIncludesFlag,
26
29
  reservedRangeLabelsKey,
27
30
  UnassignedSequenceNumber,
28
31
  } from "@fluidframework/merge-tree";
@@ -35,7 +38,17 @@ const reservedIntervalIdKey = "intervalId";
35
38
  export enum IntervalType {
36
39
  Simple = 0x0,
37
40
  Nest = 0x1,
38
- SlideOnRemove = 0x2,
41
+ /**
42
+ * SlideOnRemove indicates that the ends of the interval will slide if the segment
43
+ * they reference is removed and acked.
44
+ * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md` for details
45
+ * SlideOnRemove is the default interval behavior and does not need to be specified.
46
+ */
47
+ SlideOnRemove = 0x2, // SlideOnRemove is default behavior - all intervals are SlideOnRemove
48
+ /**
49
+ * @internal
50
+ * A temporary interval, used internally
51
+ */
39
52
  Transient = 0x4,
40
53
  }
41
54
 
@@ -58,8 +71,26 @@ export interface ISerializableInterval extends IInterval {
58
71
 
59
72
  export interface IIntervalHelpers<TInterval extends ISerializableInterval> {
60
73
  compareEnds(a: TInterval, b: TInterval): number;
61
- create(label: string, start: number, end: number,
62
- client: Client, intervalType?: IntervalType, op?: ISequencedDocumentMessage): TInterval;
74
+ /**
75
+ *
76
+ * @param label - label of the interval collection this interval is being added to. This parameter is
77
+ * irrelevant for transient intervals.
78
+ * @param start - numerical start position of the interval
79
+ * @param end - numberical end position of the interval
80
+ * @param client - client creating the interval
81
+ * @param intervalType - Type of interval to create. Default is SlideOnRemove
82
+ * @param op - If this create came from a remote client, op that created it. Default is undefined (i.e. local)
83
+ * @param fromSnapshot - If this create came from loading a snapshot. Default is false.
84
+ */
85
+ create(
86
+ label: string,
87
+ start: number,
88
+ end: number,
89
+ client: Client,
90
+ intervalType?: IntervalType,
91
+ op?: ISequencedDocumentMessage,
92
+ fromSnapshot?: boolean,
93
+ ): TInterval;
63
94
  }
64
95
 
65
96
  export class Interval implements ISerializableInterval {
@@ -310,20 +341,39 @@ export class SequenceInterval implements ISerializableInterval {
310
341
  }
311
342
  }
312
343
 
313
- function createPositionReference(
344
+ function createPositionReferenceFromSegoff(
314
345
  client: Client,
315
- pos: number,
346
+ segoff: { segment: ISegment | undefined; offset: number | undefined; },
316
347
  refType: ReferenceType,
317
348
  op?: ISequencedDocumentMessage): LocalReference {
318
- const segoff = client.getContainingSegment(pos, op);
319
- if (segoff?.segment) {
320
- const lref = new LocalReference(client, segoff.segment, segoff.offset, refType);
321
- if (refType !== ReferenceType.Transient) {
322
- client.addLocalReference(lref);
349
+ if (segoff.segment) {
350
+ const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
351
+ return ref as LocalReference;
352
+ } else {
353
+ if (!op && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
354
+ throw new UsageError("Non-transient references need segment");
323
355
  }
324
- return lref;
356
+ return new LocalReference(client, undefined, 0, refType);
357
+ }
358
+ }
359
+
360
+ function createPositionReference(
361
+ client: Client,
362
+ pos: number,
363
+ refType: ReferenceType,
364
+ op?: ISequencedDocumentMessage,
365
+ fromSnapshot?: boolean): LocalReference {
366
+ let segoff;
367
+ if (op) {
368
+ assert((refType & ReferenceType.SlideOnRemove) !== 0, 0x2f5 /* op create references must be SlideOnRemove */);
369
+ segoff = client.getContainingSegment(pos, op);
370
+ segoff = client.getSlideToSegment(segoff);
371
+ } else {
372
+ assert((refType & ReferenceType.SlideOnRemove) === 0 || fromSnapshot,
373
+ 0x2f6 /* SlideOnRemove references must be op created */);
374
+ segoff = client.getContainingSegment(pos);
325
375
  }
326
- return new LocalReference(client, undefined);
376
+ return createPositionReferenceFromSegoff(client, segoff, refType, op);
327
377
  }
328
378
 
329
379
  function createSequenceInterval(
@@ -331,38 +381,43 @@ function createSequenceInterval(
331
381
  start: number,
332
382
  end: number,
333
383
  client: Client,
334
- intervalType: IntervalType,
335
- op?: ISequencedDocumentMessage): SequenceInterval {
384
+ intervalType?: IntervalType,
385
+ op?: ISequencedDocumentMessage,
386
+ fromSnapshot?: boolean): SequenceInterval {
336
387
  let beginRefType = ReferenceType.RangeBegin;
337
388
  let endRefType = ReferenceType.RangeEnd;
338
- if (intervalType === IntervalType.Nest) {
339
- beginRefType = ReferenceType.NestBegin;
340
- endRefType = ReferenceType.NestEnd;
341
- } else if (intervalType === IntervalType.Transient) {
389
+ if (intervalType === IntervalType.Transient) {
342
390
  beginRefType = ReferenceType.Transient;
343
391
  endRefType = ReferenceType.Transient;
392
+ } else {
393
+ if (intervalType === IntervalType.Nest) {
394
+ beginRefType = ReferenceType.NestBegin;
395
+ endRefType = ReferenceType.NestEnd;
396
+ }
397
+ // All non-transient interval references must eventually be SlideOnRemove
398
+ // To ensure eventual consistency, they must start as StayOnRemove when
399
+ // pending (created locally and creation op is not acked)
400
+ if (op || fromSnapshot) {
401
+ beginRefType |= ReferenceType.SlideOnRemove;
402
+ endRefType |= ReferenceType.SlideOnRemove;
403
+ } else {
404
+ beginRefType |= ReferenceType.StayOnRemove;
405
+ endRefType |= ReferenceType.StayOnRemove;
406
+ }
344
407
  }
345
408
 
346
- // TODO: Should SlideOnRemove be the default behavior?
347
- if (intervalType & IntervalType.SlideOnRemove) {
348
- beginRefType |= ReferenceType.SlideOnRemove;
349
- endRefType |= ReferenceType.SlideOnRemove;
350
- }
351
-
352
- const startLref = createPositionReference(client, start, beginRefType, op);
353
- const endLref = createPositionReference(client, end, endRefType, op);
354
- if (startLref && endLref) {
355
- startLref.pairedRef = endLref;
356
- endLref.pairedRef = startLref;
357
- const rangeProp = {
358
- [reservedRangeLabelsKey]: [label],
359
- };
360
- startLref.addProperties(rangeProp);
361
- endLref.addProperties(rangeProp);
409
+ const startLref = createPositionReference(client, start, beginRefType, op, fromSnapshot);
410
+ const endLref = createPositionReference(client, end, endRefType, op, fromSnapshot);
411
+ startLref.pairedRef = endLref;
412
+ endLref.pairedRef = startLref;
413
+ const rangeProp = {
414
+ [reservedRangeLabelsKey]: [label],
415
+ };
416
+ startLref.addProperties(rangeProp);
417
+ endLref.addProperties(rangeProp);
362
418
 
363
- const ival = new SequenceInterval(startLref, endLref, intervalType, rangeProp);
364
- return ival;
365
- }
419
+ const ival = new SequenceInterval(startLref, endLref, intervalType, rangeProp);
420
+ return ival;
366
421
  }
367
422
 
368
423
  export function defaultIntervalConflictResolver(a: Interval, b: Interval) {
@@ -520,20 +575,19 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
520
575
  }
521
576
 
522
577
  public findOverlappingIntervals(startPosition: number, endPosition: number) {
523
- if (!this.intervalTree.intervals.isEmpty()) {
524
- const transientInterval =
525
- this.helpers.create(
526
- "transient",
527
- startPosition,
528
- endPosition,
529
- this.client,
530
- IntervalType.Transient);
531
-
532
- const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
533
- return overlappingIntervalNodes.map((node) => node.key);
534
- } else {
578
+ if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
535
579
  return [];
536
580
  }
581
+ const transientInterval =
582
+ this.helpers.create(
583
+ "transient",
584
+ startPosition,
585
+ endPosition,
586
+ this.client,
587
+ IntervalType.Transient);
588
+
589
+ const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
590
+ return overlappingIntervalNodes.map((node) => node.key);
537
591
  }
538
592
 
539
593
  public previousInterval(pos: number) {
@@ -855,11 +909,18 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
855
909
  if (this.savedSerializedIntervals) {
856
910
  for (const serializedInterval of this.savedSerializedIntervals) {
857
911
  this.localCollection.ensureSerializedId(serializedInterval);
858
- this.localCollection.addInterval(
859
- serializedInterval.start,
860
- serializedInterval.end,
861
- serializedInterval.intervalType,
862
- serializedInterval.properties);
912
+ const { start, end, intervalType, properties } = serializedInterval;
913
+ const interval = this.helpers.create(
914
+ label,
915
+ start,
916
+ end,
917
+ client,
918
+ intervalType,
919
+ undefined,
920
+ true,
921
+ );
922
+ interval.addProperties(properties);
923
+ this.localCollection.add(interval);
863
924
  }
864
925
  }
865
926
  this.savedSerializedIntervals = undefined;
@@ -872,6 +933,14 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
872
933
  return this.localCollection.getIntervalById(id);
873
934
  }
874
935
 
936
+ /**
937
+ * Create a new interval and add it to the collection
938
+ * @param start - interval start position
939
+ * @param end - interval end position
940
+ * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
941
+ * @param props - properties of the interval
942
+ * @returns - the created interval
943
+ */
875
944
  public add(
876
945
  start: number,
877
946
  end: number,
@@ -881,6 +950,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
881
950
  if (!this.attached) {
882
951
  throw new Error("attach must be called prior to adding intervals");
883
952
  }
953
+ if (intervalType & IntervalType.Transient) {
954
+ throw new Error("Can not add transient intervals");
955
+ }
884
956
 
885
957
  const interval: TInterval = this.localCollection.addInterval(start, end, intervalType, props);
886
958
 
@@ -1064,6 +1136,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1064
1136
  // This is an ack from the server. Remove the pending change.
1065
1137
  this.removePendingChange(serializedInterval);
1066
1138
  const id: string = serializedInterval.properties[reservedIntervalIdKey];
1139
+ // Could store the interval in the localOpMetadata to avoid the getIntervalById call
1067
1140
  interval = this.getIntervalById(id);
1068
1141
  if (interval) {
1069
1142
  // Let the propertyManager prune its pending change-properties set.
@@ -1072,6 +1145,8 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1072
1145
  type: MergeTreeDeltaType.ANNOTATE,
1073
1146
  props: serializedInterval.properties,
1074
1147
  });
1148
+
1149
+ this.ackInterval(interval, op);
1075
1150
  }
1076
1151
  } else {
1077
1152
  // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
@@ -1130,6 +1205,53 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1130
1205
  });
1131
1206
  }
1132
1207
 
1208
+ private getSlideToSegment(lref: LocalReference) {
1209
+ const segoff = { segment: lref.segment, offset: lref.offset };
1210
+ const newSegoff = this.client.getSlideToSegment(segoff);
1211
+ const value: { segment: ISegment | undefined; offset: number | undefined; } | undefined
1212
+ = (segoff === newSegoff) ? undefined : newSegoff;
1213
+ return value;
1214
+ }
1215
+
1216
+ private setSlideOnRemove(lref: LocalReference) {
1217
+ let refType = lref.refType;
1218
+ refType = refType & ~ReferenceType.StayOnRemove;
1219
+ refType = refType | ReferenceType.SlideOnRemove;
1220
+ lref.refType = refType;
1221
+ }
1222
+
1223
+ private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
1224
+ // in current usage, interval is always a SequenceInterval
1225
+ if (!(interval instanceof SequenceInterval)) {
1226
+ return;
1227
+ }
1228
+
1229
+ if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove)) {
1230
+ return;
1231
+ }
1232
+ assert(refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove),
1233
+ 0x2f7 /* start and end must both be StayOnRemove */);
1234
+ const newStart = this.getSlideToSegment(interval.start);
1235
+ const newEnd = this.getSlideToSegment(interval.end);
1236
+ this.setSlideOnRemove(interval.start);
1237
+ this.setSlideOnRemove(interval.end);
1238
+
1239
+ if (newStart || newEnd) {
1240
+ this.localCollection.removeExistingInterval(interval);
1241
+ if (newStart) {
1242
+ const props = interval.start.properties;
1243
+ interval.start = createPositionReferenceFromSegoff(this.client, newStart, interval.start.refType, op);
1244
+ interval.start.addProperties(props);
1245
+ }
1246
+ if (newEnd) {
1247
+ const props = interval.end.properties;
1248
+ interval.end = createPositionReferenceFromSegoff(this.client, newEnd, interval.end.refType, op);
1249
+ interval.end.addProperties(props);
1250
+ }
1251
+ this.localCollection.add(interval);
1252
+ }
1253
+ }
1254
+
1133
1255
  /** @deprecated - use ackAdd */
1134
1256
  public addInternal(
1135
1257
  serializedInterval: ISerializedInterval,
@@ -1144,8 +1266,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1144
1266
  local: boolean,
1145
1267
  op: ISequencedDocumentMessage) {
1146
1268
  if (local) {
1147
- // Local ops were applied when the message was created and there's no "pending add"
1148
- // state to bookkeep
1269
+ const id: string = serializedInterval.properties[reservedIntervalIdKey];
1270
+ // Could store the interval in the localOpMetadata to avoid the getIntervalById call
1271
+ const localInterval = this.getIntervalById(id);
1272
+ if (localInterval) {
1273
+ this.ackInterval(localInterval, op);
1274
+ }
1149
1275
  return;
1150
1276
  }
1151
1277
 
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/sequence";
9
- export const pkgVersion = "0.59.4002";
9
+ export const pkgVersion = "1.0.2";
package/src/sequence.ts CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  SummarySerializer,
49
49
  } from "@fluidframework/shared-object-base";
50
50
  import { IEventThisPlaceHolder } from "@fluidframework/common-definitions";
51
- import { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions";
51
+ import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtime-definitions";
52
52
 
53
53
  import {
54
54
  IntervalCollection,
@@ -173,7 +173,7 @@ export abstract class SharedSegmentSequence<T extends ISegment>
173
173
  attributes: IChannelAttributes,
174
174
  public readonly segmentFromSpec: (spec: IJSONSegment) => ISegment,
175
175
  ) {
176
- super(id, dataStoreRuntime, attributes);
176
+ super(id, dataStoreRuntime, attributes, "fluid_sequence_");
177
177
 
178
178
  this.loadedDeferred.promise.catch((error) => {
179
179
  this.logger.sendErrorEvent({ eventName: "SequenceLoadFailed" }, error);
@@ -463,7 +463,10 @@ export abstract class SharedSegmentSequence<T extends ISegment>
463
463
  return this.intervalCollections.keys();
464
464
  }
465
465
 
466
- protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
466
+ protected summarizeCore(
467
+ serializer: IFluidSerializer,
468
+ telemetryContext?: ITelemetryContext,
469
+ ): ISummaryTreeWithStats {
467
470
  const builder = new SummaryTreeBuilder();
468
471
 
469
472
  // conditionally write the interval collection blob
@@ -519,7 +522,7 @@ export abstract class SharedSegmentSequence<T extends ISegment>
519
522
  this.client.startOrUpdateCollaboration(this.runtime.clientId);
520
523
  }
521
524
 
522
- protected onDisconnect() {}
525
+ protected onDisconnect() { }
523
526
 
524
527
  protected reSubmitCore(content: any, localOpMetadata: unknown) {
525
528
  if (!this.intervalCollections.trySubmitMessage(content, localOpMetadata as IMapMessageLocalMetadata)) {
@@ -559,18 +562,17 @@ export abstract class SharedSegmentSequence<T extends ISegment>
559
562
  || m.referenceSequenceNumber < collabWindow.minSeq
560
563
  || m.sequenceNumber <= collabWindow.minSeq
561
564
  || m.sequenceNumber <= collabWindow.currentSeq) {
562
- throw new Error(`Invalid catchup operations in snapshot: ${
563
- JSON.stringify({
564
- op: {
565
- seq: m.sequenceNumber,
566
- minSeq: m.minimumSequenceNumber,
567
- refSeq: m.referenceSequenceNumber,
568
- },
569
- collabWindow: {
570
- seq: collabWindow.currentSeq,
571
- minSeq: collabWindow.minSeq,
572
- },
573
- })}`);
565
+ throw new Error(`Invalid catchup operations in snapshot: ${JSON.stringify({
566
+ op: {
567
+ seq: m.sequenceNumber,
568
+ minSeq: m.minimumSequenceNumber,
569
+ refSeq: m.referenceSequenceNumber,
570
+ },
571
+ collabWindow: {
572
+ seq: collabWindow.currentSeq,
573
+ minSeq: collabWindow.minSeq,
574
+ },
575
+ })}`);
574
576
  }
575
577
  this.processMergeTreeMsg(m);
576
578
  });
@@ -666,7 +668,7 @@ export abstract class SharedSegmentSequence<T extends ISegment>
666
668
  // shallow clone the message as we only overwrite top level properties,
667
669
  // like referenceSequenceNumber and content only
668
670
  stashMessage = {
669
- ... message,
671
+ ...message,
670
672
  referenceSequenceNumber: stashMessage.sequenceNumber - 1,
671
673
  contents: ops.length !== 1 ? createGroupOp(...ops) : ops[0],
672
674
  };
@@ -117,7 +117,7 @@ export class SharedIntervalCollection
117
117
  runtime: IFluidDataStoreRuntime,
118
118
  attributes: IChannelAttributes,
119
119
  ) {
120
- super(id, runtime, attributes);
120
+ super(id, runtime, attributes, "fluid_sharedIntervalCollection_");
121
121
  this.intervalCollections = new DefaultMap(
122
122
  this.serializer,
123
123
  this.handle,