@fluidframework/sequence 2.50.0-345060 → 2.51.0-347100

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 (34) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/intervalCollection.d.ts +4 -5
  3. package/dist/intervalCollection.d.ts.map +1 -1
  4. package/dist/intervalCollection.js +94 -95
  5. package/dist/intervalCollection.js.map +1 -1
  6. package/dist/intervalCollectionMapInterfaces.d.ts +1 -11
  7. package/dist/intervalCollectionMapInterfaces.d.ts.map +1 -1
  8. package/dist/intervalCollectionMapInterfaces.js.map +1 -1
  9. package/dist/intervals/sequenceInterval.d.ts +7 -3
  10. package/dist/intervals/sequenceInterval.d.ts.map +1 -1
  11. package/dist/intervals/sequenceInterval.js +72 -30
  12. package/dist/intervals/sequenceInterval.js.map +1 -1
  13. package/dist/packageVersion.d.ts +1 -1
  14. package/dist/packageVersion.js +1 -1
  15. package/dist/packageVersion.js.map +1 -1
  16. package/lib/intervalCollection.d.ts +4 -5
  17. package/lib/intervalCollection.d.ts.map +1 -1
  18. package/lib/intervalCollection.js +94 -95
  19. package/lib/intervalCollection.js.map +1 -1
  20. package/lib/intervalCollectionMapInterfaces.d.ts +1 -11
  21. package/lib/intervalCollectionMapInterfaces.d.ts.map +1 -1
  22. package/lib/intervalCollectionMapInterfaces.js.map +1 -1
  23. package/lib/intervals/sequenceInterval.d.ts +7 -3
  24. package/lib/intervals/sequenceInterval.d.ts.map +1 -1
  25. package/lib/intervals/sequenceInterval.js +74 -32
  26. package/lib/intervals/sequenceInterval.js.map +1 -1
  27. package/lib/packageVersion.d.ts +1 -1
  28. package/lib/packageVersion.js +1 -1
  29. package/lib/packageVersion.js.map +1 -1
  30. package/package.json +18 -19
  31. package/src/intervalCollection.ts +106 -103
  32. package/src/intervalCollectionMapInterfaces.ts +1 -9
  33. package/src/intervals/sequenceInterval.ts +88 -32
  34. package/src/packageVersion.ts +1 -1
@@ -99,7 +99,7 @@ export class LocalIntervalCollection {
99
99
  this.removeIntervalFromIndexes(interval);
100
100
  this.removeIntervalListeners(interval);
101
101
  }
102
- addInterval(id, start, end, props, op, rollback) {
102
+ addInterval(id, start, end, props, op) {
103
103
  // This check is intended to prevent scenarios where a random interval is created and then
104
104
  // inserted into a collection. The aim is to ensure that the collection is created first
105
105
  // then the user can create/add intervals based on the collection
@@ -107,7 +107,7 @@ export class LocalIntervalCollection {
107
107
  props[reservedRangeLabelsKey][0] !== this.label) {
108
108
  throw new LoggingError("Adding an interval that belongs to another interval collection is not permitted");
109
109
  }
110
- const interval = createSequenceInterval(this.label, id, start, end, this.client, IntervalType.SlideOnRemove, op, undefined, this.options.mergeTreeReferencesCanSlideToEndpoint, props, rollback);
110
+ const interval = createSequenceInterval(this.label, id, start, end, this.client, IntervalType.SlideOnRemove, op, undefined, this.options.mergeTreeReferencesCanSlideToEndpoint, props, false);
111
111
  this.add(interval);
112
112
  return interval;
113
113
  }
@@ -207,12 +207,12 @@ function removeMetadataFromPendingChanges(localOpMetadataNode) {
207
207
  }
208
208
  function clearEmptyPendingEntry(pendingChanges, id) {
209
209
  const pending = pendingChanges[id];
210
- assert(pending !== undefined, 0xbbf /* pending must exist for local process */);
211
- if (pending.local.empty) {
210
+ if (pending !== undefined && pending.local.empty) {
212
211
  assert(pending.endpointChanges?.empty !== false, 0xbc0 /* endpointChanges must be empty if not pending changes */);
213
212
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
214
213
  delete pendingChanges[id];
215
214
  }
215
+ return pending;
216
216
  }
217
217
  function hasEndpointChanges(serialized) {
218
218
  return serialized.start !== undefined && serialized.end !== undefined;
@@ -228,15 +228,15 @@ export class IntervalCollection extends TypedEventEmitter {
228
228
  super();
229
229
  this.options = options;
230
230
  this.pending = {};
231
- this.submitDelta = (op, md) => {
231
+ this.submitDelta = (op, md, consensus) => {
232
232
  const { id } = getSerializedProperties(op.value);
233
233
  const pending = (this.pending[id] ??= {
234
234
  local: new DoublyLinkedList(),
235
+ consensus,
235
236
  });
236
237
  if (md.type === "add" || (md.type === "change" && hasEndpointChanges(op.value))) {
237
238
  const endpointChanges = (pending.endpointChanges ??= new DoublyLinkedList());
238
239
  md.endpointChangesNode = endpointChanges.push(md).last;
239
- md.rebased = undefined;
240
240
  }
241
241
  submitDelta(op, pending.local.push(md).last);
242
242
  };
@@ -276,57 +276,60 @@ export class IntervalCollection extends TypedEventEmitter {
276
276
  const localOpMetadata = removeMetadataFromPendingChanges(maybeMetadata);
277
277
  const { value } = op;
278
278
  const { id, properties } = getSerializedProperties(value);
279
- const { type } = localOpMetadata;
279
+ const pending = clearEmptyPendingEntry(this.pending, id);
280
+ const previous = pending?.local.empty
281
+ ? pending.consensus
282
+ : pending?.local.last?.data.interval;
283
+ const { type, interval } = localOpMetadata;
280
284
  switch (type) {
281
285
  case "add": {
282
- const interval = this.getIntervalById(id);
283
- if (interval) {
284
- this.deleteExistingInterval({ interval, local: true, rollback: true });
285
- }
286
+ this.deleteExistingInterval({ interval, local: true, rollback: true });
287
+ interval.dispose();
286
288
  break;
287
289
  }
288
290
  case "change": {
289
- const { previous } = localOpMetadata;
290
- const endpointsChanged = hasEndpointChanges(value);
291
- const start = endpointsChanged
292
- ? toOptionalSequencePlace(previous.start, previous.startSide)
293
- : undefined;
294
- const end = endpointsChanged
295
- ? toOptionalSequencePlace(previous.end, previous.endSide)
291
+ const changeProperties = Object.keys(properties).length > 0;
292
+ const deltaProps = changeProperties
293
+ ? interval.changeProperties(properties, undefined, true)
296
294
  : undefined;
297
- this.change(id, {
298
- start,
299
- end,
300
- props: Object.keys(properties).length > 0 ? properties : undefined,
301
- rollback: true,
302
- });
295
+ if (localOpMetadata.endpointChangesNode !== undefined) {
296
+ if (previous !== undefined) {
297
+ this.localCollection?.removeExistingInterval(interval);
298
+ this.localCollection?.add(previous);
299
+ this.emitChange(previous, interval, true, true);
300
+ }
301
+ if (previous !== interval) {
302
+ interval.dispose();
303
+ }
304
+ }
305
+ if (changeProperties) {
306
+ this.emit("propertyChanged", previous, deltaProps, true, undefined);
307
+ }
303
308
  break;
304
309
  }
305
310
  case "delete": {
306
- const { previous } = localOpMetadata;
307
- this.add({
308
- id,
309
- start: toSequencePlace(previous.start, previous.startSide),
310
- end: toSequencePlace(previous.end, previous.endSide),
311
- props: Object.keys(properties).length > 0 ? properties : undefined,
312
- rollback: true,
313
- });
311
+ // a remote could delete the same interval, so it may not exist to re-add
312
+ if (previous !== undefined) {
313
+ this.localCollection?.add(previous);
314
+ this.emit("addInterval", previous, true, undefined);
315
+ }
314
316
  break;
315
317
  }
316
318
  default:
317
319
  unreachableCase(type);
318
320
  }
319
- clearEmptyPendingEntry(this.pending, id);
320
321
  }
321
322
  process(op, local, message, maybeMetadata) {
322
323
  const localOpMetadata = local
323
324
  ? removeMetadataFromPendingChanges(maybeMetadata)
324
325
  : undefined;
325
326
  const { opName, value } = op;
327
+ const { id } = getSerializedProperties(value);
326
328
  assert((local === false && localOpMetadata === undefined) || opName === localOpMetadata?.type, 0xbc1 /* must be same type */);
329
+ let newConsensus = localOpMetadata?.interval;
327
330
  switch (opName) {
328
331
  case "add": {
329
- this.ackAdd(value, local, message,
332
+ newConsensus = this.ackAdd(value, local, message,
330
333
  // this cast is safe because of the above assert which
331
334
  // validates the op and metadata types match for local changes
332
335
  localOpMetadata);
@@ -337,15 +340,17 @@ export class IntervalCollection extends TypedEventEmitter {
337
340
  break;
338
341
  }
339
342
  case "change": {
340
- this.ackChange(value, local, message);
343
+ newConsensus = this.ackChange(value, local, message, // this cast is safe because of the above assert which
344
+ // validates the op and metadata types match for local changes
345
+ localOpMetadata);
341
346
  break;
342
347
  }
343
348
  default:
344
349
  unreachableCase(opName);
345
350
  }
346
- if (local) {
347
- const { id } = getSerializedProperties(value);
348
- clearEmptyPendingEntry(this.pending, id);
351
+ const pending = clearEmptyPendingEntry(this.pending, id);
352
+ if (pending !== undefined) {
353
+ pending.consensus = newConsensus;
349
354
  }
350
355
  }
351
356
  resubmitMessage(op, maybeMetadata, squash) {
@@ -443,7 +448,7 @@ export class IntervalCollection extends TypedEventEmitter {
443
448
  for (const pending of Object.values(this.pending)) {
444
449
  if (pending?.endpointChanges !== undefined) {
445
450
  for (const local of pending.endpointChanges) {
446
- local.data.rebased = this.computeRebasedPositions(local.data, squash);
451
+ this.rebaseLocalInterval(local.data.interval.serialize(), local.data, squash);
447
452
  }
448
453
  }
449
454
  }
@@ -506,7 +511,7 @@ export class IntervalCollection extends TypedEventEmitter {
506
511
  /**
507
512
  * {@inheritdoc IIntervalCollection.add}
508
513
  */
509
- add({ id, start, end, props, rollback, }) {
514
+ add({ id, start, end, props, }) {
510
515
  if (!this.localCollection) {
511
516
  throw new LoggingError("attach must be called prior to adding intervals");
512
517
  }
@@ -517,7 +522,7 @@ export class IntervalCollection extends TypedEventEmitter {
517
522
  endSide !== undefined, 0x793 /* start and end cannot be undefined because they were not passed in as undefined */);
518
523
  this.assertStickinessEnabled(start, end);
519
524
  const intervalId = id ?? uuid();
520
- const interval = this.localCollection.addInterval(intervalId, toSequencePlace(startPos, startSide), toSequencePlace(endPos, endSide), props, undefined, rollback);
525
+ const interval = this.localCollection.addInterval(intervalId, toSequencePlace(startPos, startSide), toSequencePlace(endPos, endSide), props, undefined);
521
526
  if (interval) {
522
527
  if (!this.isCollaborating) {
523
528
  setSlideOnRemove(interval.start);
@@ -525,7 +530,7 @@ export class IntervalCollection extends TypedEventEmitter {
525
530
  }
526
531
  const serializedInterval = interval.serialize();
527
532
  const localSeq = this.getNextLocalSeq();
528
- if (this.isCollaborating && rollback !== true) {
533
+ if (this.isCollaborating) {
529
534
  this.submitDelta({
530
535
  opName: "add",
531
536
  value: serializedInterval,
@@ -555,8 +560,7 @@ export class IntervalCollection extends TypedEventEmitter {
555
560
  }, {
556
561
  type: "delete",
557
562
  localSeq: this.getNextLocalSeq(),
558
- previous: value,
559
- });
563
+ }, interval);
560
564
  }
561
565
  else {
562
566
  if (this.onDeserialize) {
@@ -573,7 +577,7 @@ export class IntervalCollection extends TypedEventEmitter {
573
577
  if (!this.localCollection) {
574
578
  throw new LoggingError("Attach must be called before accessing intervals");
575
579
  }
576
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
580
+ const interval = this.getIntervalById(id);
577
581
  if (interval) {
578
582
  this.deleteExistingInterval({ interval, local: true });
579
583
  }
@@ -621,13 +625,12 @@ export class IntervalCollection extends TypedEventEmitter {
621
625
  const metadata = {
622
626
  type: "change",
623
627
  localSeq,
624
- previous: interval.serialize(),
625
628
  interval: newInterval ?? interval,
626
629
  };
627
630
  this.submitDelta({
628
631
  opName: "change",
629
632
  value: serializedInterval,
630
- }, metadata);
633
+ }, metadata, interval);
631
634
  }
632
635
  if (deltaProps !== undefined) {
633
636
  this.emit("propertyChanged", interval, deltaProps, true, undefined);
@@ -651,7 +654,7 @@ export class IntervalCollection extends TypedEventEmitter {
651
654
  hasPendingEndpointChanges(id) {
652
655
  return this.pending[id]?.endpointChanges?.empty === false;
653
656
  }
654
- ackChange(serializedInterval, local, op) {
657
+ ackChange(serializedInterval, local, op, localOpMetadata) {
655
658
  if (!this.localCollection) {
656
659
  throw new LoggingError("Attach must be called before accessing intervals");
657
660
  }
@@ -660,44 +663,39 @@ export class IntervalCollection extends TypedEventEmitter {
660
663
  // strip it out of the properties here.
661
664
  const { id, properties } = getSerializedProperties(serializedInterval);
662
665
  assert(id !== undefined, 0x3fe /* id must exist on the interval */);
663
- const interval = this.getIntervalById(id);
664
- if (!interval) {
665
- // The interval has been removed locally; no-op.
666
- return;
667
- }
668
666
  if (local) {
667
+ assert(localOpMetadata !== undefined, 0xbd4 /* local must have metadata */);
668
+ const { interval } = localOpMetadata;
669
669
  interval.ackPropertiesChange(properties, op);
670
670
  this.ackInterval(interval, op);
671
+ return interval;
671
672
  }
672
673
  else {
673
- // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
674
- // should be the winning change.
675
- let start;
676
- let end;
677
- // Track pending start/end independently of one another.
678
- if (!this.hasPendingEndpointChanges(id)) {
679
- start = serializedInterval.start;
680
- end = serializedInterval.end;
681
- }
682
- let newInterval = interval;
683
- if (start !== undefined || end !== undefined) {
684
- // If changeInterval gives us a new interval, work with that one. Otherwise keep working with
685
- // the one we originally found in the tree.
686
- newInterval =
687
- this.localCollection.changeInterval(interval, toOptionalSequencePlace(start, serializedInterval.startSide ?? Side.Before), toOptionalSequencePlace(end, serializedInterval.endSide ?? Side.Before), op) ?? interval;
688
- }
689
- const deltaProps = newInterval.changeProperties(properties, op);
690
- if (this.onDeserialize) {
691
- this.onDeserialize(newInterval);
692
- }
693
- if (newInterval !== interval) {
694
- this.emitChange(newInterval, interval, local, false, op);
674
+ const latestInterval = this.getIntervalById(id);
675
+ const intervalToChange = this.pending[id]?.consensus ?? latestInterval;
676
+ const isLatestInterval = intervalToChange === latestInterval;
677
+ if (!intervalToChange) {
678
+ return intervalToChange;
679
+ }
680
+ const deltaProps = intervalToChange.changeProperties(properties, op);
681
+ let newInterval = intervalToChange;
682
+ if (hasEndpointChanges(serializedInterval)) {
683
+ const { start, end, startSide, endSide } = serializedInterval;
684
+ newInterval = intervalToChange.modify("", toOptionalSequencePlace(start, startSide ?? Side.Before), toOptionalSequencePlace(end, endSide ?? Side.Before), op);
685
+ if (isLatestInterval) {
686
+ this.localCollection.removeExistingInterval(intervalToChange);
687
+ this.localCollection.add(newInterval);
688
+ this.emitChange(newInterval, intervalToChange, local, false, op);
689
+ if (this.onDeserialize) {
690
+ this.onDeserialize(newInterval);
691
+ }
692
+ }
695
693
  }
696
- const changedProperties = Object.keys(properties).length > 0;
697
- if (changedProperties) {
698
- this.emit("propertyChanged", interval, deltaProps, local, op);
699
- this.emit("changed", interval, deltaProps, undefined, local, false);
694
+ if (deltaProps !== undefined && Object.keys(deltaProps).length > 0) {
695
+ this.emit("propertyChanged", latestInterval, deltaProps, local, op);
696
+ this.emit("changed", latestInterval, deltaProps, undefined, local, false);
700
697
  }
698
+ return newInterval;
701
699
  }
702
700
  }
703
701
  /**
@@ -731,8 +729,8 @@ export class IntervalCollection extends TypedEventEmitter {
731
729
  }
732
730
  const { localSeq, interval } = localOpMetadata;
733
731
  const { id } = getSerializedProperties(original);
734
- const rebasedEndpoint = (localOpMetadata.rebased ??= this.computeRebasedPositions(localOpMetadata, squash));
735
- const localInterval = this.localCollection.idIntervalIndex.getIntervalById(id);
732
+ const rebasedEndpoint = this.computeRebasedPositions(localOpMetadata, squash);
733
+ const localInterval = this.getIntervalById(id);
736
734
  // if the interval slid off the string, rebase the op to be a noop and delete the interval.
737
735
  if (rebasedEndpoint === "detached") {
738
736
  if (localInterval !== undefined &&
@@ -755,8 +753,7 @@ export class IntervalCollection extends TypedEventEmitter {
755
753
  this.localCollection.add(interval);
756
754
  this.emitChange(interval, old, true, true);
757
755
  }
758
- this.client.removeLocalReferencePosition(old.start);
759
- this.client.removeLocalReferencePosition(old.end);
756
+ old.dispose();
760
757
  }
761
758
  return {
762
759
  ...original,
@@ -805,10 +802,13 @@ export class IntervalCollection extends TypedEventEmitter {
805
802
  }
806
803
  // `interval`'s endpoints will get modified in-place, so clone it prior to doing so for event emission.
807
804
  const oldInterval = interval.clone();
808
- // In this case, where we change the start or end of an interval,
809
- // it is necessary to remove and re-add the interval listeners.
810
- // This ensures that the correct listeners are added to the LocalReferencePosition.
811
- this.localCollection.removeExistingInterval(interval);
805
+ const isLatestInterval = this.getIntervalById(id) === interval;
806
+ if (isLatestInterval) {
807
+ // In this case, where we change the start or end of an interval,
808
+ // it is necessary to remove and re-add the interval listeners.
809
+ // This ensures that the correct listeners are added to the LocalReferencePosition.
810
+ this.localCollection.removeExistingInterval(interval);
811
+ }
812
812
  if (!this.client) {
813
813
  throw new LoggingError("client does not exist");
814
814
  }
@@ -850,19 +850,18 @@ export class IntervalCollection extends TypedEventEmitter {
850
850
  oldInterval.end.refType = ReferenceType.Transient;
851
851
  oldSeg?.localRefs?.addLocalRef(oldInterval.end, oldInterval.end.getOffset());
852
852
  }
853
- this.localCollection.add(interval);
854
- this.emitChange(interval, oldInterval, true, true, op);
853
+ if (isLatestInterval) {
854
+ this.localCollection.add(interval);
855
+ this.emitChange(interval, oldInterval, true, true, op);
856
+ }
855
857
  }
856
858
  }
857
859
  ackAdd(serializedInterval, local, op, localOpMetadata) {
858
860
  const { id, properties } = getSerializedProperties(serializedInterval);
859
861
  if (local) {
860
862
  assert(localOpMetadata !== undefined, 0x553 /* op metadata should be defined for local op */);
861
- const localInterval = this.getIntervalById(id);
862
- if (localInterval) {
863
- this.ackInterval(localInterval, op);
864
- }
865
- return;
863
+ this.ackInterval(localOpMetadata.interval, op);
864
+ return localOpMetadata.interval;
866
865
  }
867
866
  if (!this.localCollection) {
868
867
  throw new LoggingError("attachSequence must be called");
@@ -887,7 +886,7 @@ export class IntervalCollection extends TypedEventEmitter {
887
886
  throw new LoggingError("attach must be called prior to deleting intervals");
888
887
  }
889
888
  const { id } = getSerializedProperties(serializedInterval);
890
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
889
+ const interval = this.getIntervalById(id);
891
890
  if (interval) {
892
891
  this.deleteExistingInterval({ interval, local, op });
893
892
  }