@fluidframework/sequence 0.59.4000-71130 → 1.0.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.
@@ -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
 
@@ -310,20 +323,37 @@ export class SequenceInterval implements ISerializableInterval {
310
323
  }
311
324
  }
312
325
 
326
+ function createPositionReferenceFromSegoff(
327
+ client: Client,
328
+ segoff: { segment: ISegment | undefined; offset: number | undefined; },
329
+ refType: ReferenceType,
330
+ op?: ISequencedDocumentMessage): LocalReference {
331
+ if (segoff.segment) {
332
+ const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
333
+ return ref as LocalReference;
334
+ } else {
335
+ if (!op && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
336
+ throw new UsageError("Non-transient references need segment");
337
+ }
338
+ return new LocalReference(client, undefined, 0, refType);
339
+ }
340
+ }
341
+
313
342
  function createPositionReference(
314
343
  client: Client,
315
344
  pos: number,
316
345
  refType: ReferenceType,
317
346
  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);
323
- }
324
- return lref;
347
+ let segoff;
348
+ if (op) {
349
+ assert((refType & ReferenceType.SlideOnRemove) !== 0, 0x2f5 /* op create references must be SlideOnRemove */);
350
+ segoff = client.getContainingSegment(pos, op);
351
+ segoff = client.getSlideToSegment(segoff);
352
+ } else {
353
+ assert((refType & ReferenceType.SlideOnRemove) === 0, 0x2f6 /* SlideOnRemove references must be op created */);
354
+ segoff = client.getContainingSegment(pos);
325
355
  }
326
- return new LocalReference(client, undefined);
356
+ return createPositionReferenceFromSegoff(client, segoff, refType, op);
327
357
  }
328
358
 
329
359
  function createSequenceInterval(
@@ -331,38 +361,42 @@ function createSequenceInterval(
331
361
  start: number,
332
362
  end: number,
333
363
  client: Client,
334
- intervalType: IntervalType,
364
+ intervalType?: IntervalType,
335
365
  op?: ISequencedDocumentMessage): SequenceInterval {
336
366
  let beginRefType = ReferenceType.RangeBegin;
337
367
  let endRefType = ReferenceType.RangeEnd;
338
- if (intervalType === IntervalType.Nest) {
339
- beginRefType = ReferenceType.NestBegin;
340
- endRefType = ReferenceType.NestEnd;
341
- } else if (intervalType === IntervalType.Transient) {
368
+ if (intervalType === IntervalType.Transient) {
342
369
  beginRefType = ReferenceType.Transient;
343
370
  endRefType = ReferenceType.Transient;
344
- }
345
-
346
- // TODO: Should SlideOnRemove be the default behavior?
347
- if (intervalType & IntervalType.SlideOnRemove) {
348
- beginRefType |= ReferenceType.SlideOnRemove;
349
- endRefType |= ReferenceType.SlideOnRemove;
371
+ } else {
372
+ if (intervalType === IntervalType.Nest) {
373
+ beginRefType = ReferenceType.NestBegin;
374
+ endRefType = ReferenceType.NestEnd;
375
+ }
376
+ // All non-transient interval references must eventually be SlideOnRemove
377
+ // To ensure eventual consistency, they must start as StayOnRemove when
378
+ // pending (created locally and creation op is not acked)
379
+ if (op) {
380
+ beginRefType |= ReferenceType.SlideOnRemove;
381
+ endRefType |= ReferenceType.SlideOnRemove;
382
+ } else {
383
+ beginRefType |= ReferenceType.StayOnRemove;
384
+ endRefType |= ReferenceType.StayOnRemove;
385
+ }
350
386
  }
351
387
 
352
388
  const startLref = createPositionReference(client, start, beginRefType, op);
353
389
  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);
390
+ startLref.pairedRef = endLref;
391
+ endLref.pairedRef = startLref;
392
+ const rangeProp = {
393
+ [reservedRangeLabelsKey]: [label],
394
+ };
395
+ startLref.addProperties(rangeProp);
396
+ endLref.addProperties(rangeProp);
362
397
 
363
- const ival = new SequenceInterval(startLref, endLref, intervalType, rangeProp);
364
- return ival;
365
- }
398
+ const ival = new SequenceInterval(startLref, endLref, intervalType, rangeProp);
399
+ return ival;
366
400
  }
367
401
 
368
402
  export function defaultIntervalConflictResolver(a: Interval, b: Interval) {
@@ -520,20 +554,19 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
520
554
  }
521
555
 
522
556
  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 {
557
+ if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
535
558
  return [];
536
559
  }
560
+ const transientInterval =
561
+ this.helpers.create(
562
+ "transient",
563
+ startPosition,
564
+ endPosition,
565
+ this.client,
566
+ IntervalType.Transient);
567
+
568
+ const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
569
+ return overlappingIntervalNodes.map((node) => node.key);
537
570
  }
538
571
 
539
572
  public previousInterval(pos: number) {
@@ -872,6 +905,14 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
872
905
  return this.localCollection.getIntervalById(id);
873
906
  }
874
907
 
908
+ /**
909
+ * Create a new interval and add it to the collection
910
+ * @param start - interval start position
911
+ * @param end - interval end position
912
+ * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
913
+ * @param props - properties of the interval
914
+ * @returns - the created interval
915
+ */
875
916
  public add(
876
917
  start: number,
877
918
  end: number,
@@ -881,6 +922,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
881
922
  if (!this.attached) {
882
923
  throw new Error("attach must be called prior to adding intervals");
883
924
  }
925
+ if (intervalType & IntervalType.Transient) {
926
+ throw new Error("Can not add transient intervals");
927
+ }
884
928
 
885
929
  const interval: TInterval = this.localCollection.addInterval(start, end, intervalType, props);
886
930
 
@@ -1064,6 +1108,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1064
1108
  // This is an ack from the server. Remove the pending change.
1065
1109
  this.removePendingChange(serializedInterval);
1066
1110
  const id: string = serializedInterval.properties[reservedIntervalIdKey];
1111
+ // Could store the interval in the localOpMetadata to avoid the getIntervalById call
1067
1112
  interval = this.getIntervalById(id);
1068
1113
  if (interval) {
1069
1114
  // Let the propertyManager prune its pending change-properties set.
@@ -1072,6 +1117,8 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1072
1117
  type: MergeTreeDeltaType.ANNOTATE,
1073
1118
  props: serializedInterval.properties,
1074
1119
  });
1120
+
1121
+ this.ackInterval(interval, op);
1075
1122
  }
1076
1123
  } else {
1077
1124
  // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
@@ -1130,6 +1177,53 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1130
1177
  });
1131
1178
  }
1132
1179
 
1180
+ private getSlideToSegment(lref: LocalReference) {
1181
+ const segoff = { segment: lref.segment, offset: lref.offset };
1182
+ const newSegoff = this.client.getSlideToSegment(segoff);
1183
+ const value: { segment: ISegment | undefined; offset: number | undefined; } | undefined
1184
+ = (segoff === newSegoff) ? undefined : newSegoff;
1185
+ return value;
1186
+ }
1187
+
1188
+ private setSlideOnRemove(lref: LocalReference) {
1189
+ let refType = lref.refType;
1190
+ refType = refType & ~ReferenceType.StayOnRemove;
1191
+ refType = refType | ReferenceType.SlideOnRemove;
1192
+ lref.refType = refType;
1193
+ }
1194
+
1195
+ private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
1196
+ // in current usage, interval is always a SequenceInterval
1197
+ if (!(interval instanceof SequenceInterval)) {
1198
+ return;
1199
+ }
1200
+
1201
+ if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove)) {
1202
+ return;
1203
+ }
1204
+ assert(refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove),
1205
+ 0x2f7 /* start and end must both be StayOnRemove */);
1206
+ const newStart = this.getSlideToSegment(interval.start);
1207
+ const newEnd = this.getSlideToSegment(interval.end);
1208
+ this.setSlideOnRemove(interval.start);
1209
+ this.setSlideOnRemove(interval.end);
1210
+
1211
+ if (newStart || newEnd) {
1212
+ this.localCollection.removeExistingInterval(interval);
1213
+ if (newStart) {
1214
+ const props = interval.start.properties;
1215
+ interval.start = createPositionReferenceFromSegoff(this.client, newStart, interval.start.refType, op);
1216
+ interval.start.addProperties(props);
1217
+ }
1218
+ if (newEnd) {
1219
+ const props = interval.end.properties;
1220
+ interval.end = createPositionReferenceFromSegoff(this.client, newEnd, interval.end.refType, op);
1221
+ interval.end.addProperties(props);
1222
+ }
1223
+ this.localCollection.add(interval);
1224
+ }
1225
+ }
1226
+
1133
1227
  /** @deprecated - use ackAdd */
1134
1228
  public addInternal(
1135
1229
  serializedInterval: ISerializedInterval,
@@ -1144,8 +1238,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1144
1238
  local: boolean,
1145
1239
  op: ISequencedDocumentMessage) {
1146
1240
  if (local) {
1147
- // Local ops were applied when the message was created and there's no "pending add"
1148
- // state to bookkeep
1241
+ const id: string = serializedInterval.properties[reservedIntervalIdKey];
1242
+ // Could store the interval in the localOpMetadata to avoid the getIntervalById call
1243
+ const localInterval = this.getIntervalById(id);
1244
+ if (localInterval) {
1245
+ this.ackInterval(localInterval, op);
1246
+ }
1149
1247
  return;
1150
1248
  }
1151
1249
 
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/sequence";
9
- export const pkgVersion = "0.59.4000-71130";
9
+ export const pkgVersion = "1.0.0";
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,