@fluidframework/sequence 2.43.0 → 2.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +93 -97
  5. package/dist/intervalCollection.js.map +1 -1
  6. package/dist/intervalCollectionMapInterfaces.d.ts +2 -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 +93 -97
  19. package/lib/intervalCollection.js.map +1 -1
  20. package/lib/intervalCollectionMapInterfaces.d.ts +2 -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 +17 -17
  31. package/src/intervalCollection.ts +109 -106
  32. package/src/intervalCollectionMapInterfaces.ts +2 -13
  33. package/src/intervals/sequenceInterval.ts +88 -32
  34. package/src/packageVersion.ts +1 -1
@@ -4,8 +4,8 @@
4
4
  */
5
5
  /* eslint-disable no-bitwise */
6
6
  import { TypedEventEmitter } from "@fluid-internal/client-utils";
7
- import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
8
- import { ReferenceType, getSlideToSegoff, refTypeIncludesFlag, reservedRangeLabelsKey, Side, endpointPosAndSide, createLocalReconnectingPerspective, DoublyLinkedList, SlidingPreference, } from "@fluidframework/merge-tree/internal";
7
+ import { assert, DoublyLinkedList, unreachableCase, } from "@fluidframework/core-utils/internal";
8
+ import { ReferenceType, getSlideToSegoff, refTypeIncludesFlag, reservedRangeLabelsKey, Side, endpointPosAndSide, createLocalReconnectingPerspective, SlidingPreference, } from "@fluidframework/merge-tree/internal";
9
9
  import { LoggingError, UsageError } from "@fluidframework/telemetry-utils/internal";
10
10
  import { v4 as uuid } from "uuid";
11
11
  import { createIdIntervalIndex, EndpointIndex, OverlappingIntervalsIndex, } from "./intervalIndex/index.js";
@@ -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,57 @@ 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
+ this.localCollection?.removeExistingInterval(interval);
297
+ assert(previous !== undefined, 0xbd2 /* must have existed to change */);
298
+ this.localCollection?.add(previous);
299
+ this.emitChange(previous, interval, true, true);
300
+ }
301
+ if (previous !== interval) {
302
+ interval.dispose();
303
+ }
304
+ if (changeProperties) {
305
+ this.emit("propertyChanged", previous, deltaProps, true, undefined);
306
+ }
303
307
  break;
304
308
  }
305
309
  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
- });
310
+ assert(previous !== undefined, 0xbd3 /* must have existed to delete */);
311
+ this.localCollection?.add(previous);
312
+ this.emit("addInterval", previous, true, undefined);
314
313
  break;
315
314
  }
316
315
  default:
317
316
  unreachableCase(type);
318
317
  }
319
- clearEmptyPendingEntry(this.pending, id);
320
318
  }
321
319
  process(op, local, message, maybeMetadata) {
322
320
  const localOpMetadata = local
323
321
  ? removeMetadataFromPendingChanges(maybeMetadata)
324
322
  : undefined;
325
323
  const { opName, value } = op;
324
+ const { id } = getSerializedProperties(value);
326
325
  assert((local === false && localOpMetadata === undefined) || opName === localOpMetadata?.type, 0xbc1 /* must be same type */);
326
+ let newConsensus = localOpMetadata?.interval;
327
327
  switch (opName) {
328
328
  case "add": {
329
- this.ackAdd(value, local, message,
329
+ newConsensus = this.ackAdd(value, local, message,
330
330
  // this cast is safe because of the above assert which
331
331
  // validates the op and metadata types match for local changes
332
332
  localOpMetadata);
@@ -337,15 +337,17 @@ export class IntervalCollection extends TypedEventEmitter {
337
337
  break;
338
338
  }
339
339
  case "change": {
340
- this.ackChange(value, local, message);
340
+ newConsensus = this.ackChange(value, local, message, // this cast is safe because of the above assert which
341
+ // validates the op and metadata types match for local changes
342
+ localOpMetadata);
341
343
  break;
342
344
  }
343
345
  default:
344
346
  unreachableCase(opName);
345
347
  }
346
- if (local) {
347
- const { id } = getSerializedProperties(value);
348
- clearEmptyPendingEntry(this.pending, id);
348
+ const pending = clearEmptyPendingEntry(this.pending, id);
349
+ if (pending !== undefined) {
350
+ pending.consensus = newConsensus;
349
351
  }
350
352
  }
351
353
  resubmitMessage(op, maybeMetadata, squash) {
@@ -443,7 +445,7 @@ export class IntervalCollection extends TypedEventEmitter {
443
445
  for (const pending of Object.values(this.pending)) {
444
446
  if (pending?.endpointChanges !== undefined) {
445
447
  for (const local of pending.endpointChanges) {
446
- local.data.rebased = this.computeRebasedPositions(local.data, squash);
448
+ this.rebaseLocalInterval(local.data.interval.serialize(), local.data, squash);
447
449
  }
448
450
  }
449
451
  }
@@ -506,7 +508,7 @@ export class IntervalCollection extends TypedEventEmitter {
506
508
  /**
507
509
  * {@inheritdoc IIntervalCollection.add}
508
510
  */
509
- add({ id, start, end, props, rollback, }) {
511
+ add({ id, start, end, props, }) {
510
512
  if (!this.localCollection) {
511
513
  throw new LoggingError("attach must be called prior to adding intervals");
512
514
  }
@@ -517,7 +519,7 @@ export class IntervalCollection extends TypedEventEmitter {
517
519
  endSide !== undefined, 0x793 /* start and end cannot be undefined because they were not passed in as undefined */);
518
520
  this.assertStickinessEnabled(start, end);
519
521
  const intervalId = id ?? uuid();
520
- const interval = this.localCollection.addInterval(intervalId, toSequencePlace(startPos, startSide), toSequencePlace(endPos, endSide), props, undefined, rollback);
522
+ const interval = this.localCollection.addInterval(intervalId, toSequencePlace(startPos, startSide), toSequencePlace(endPos, endSide), props, undefined);
521
523
  if (interval) {
522
524
  if (!this.isCollaborating) {
523
525
  setSlideOnRemove(interval.start);
@@ -525,7 +527,7 @@ export class IntervalCollection extends TypedEventEmitter {
525
527
  }
526
528
  const serializedInterval = interval.serialize();
527
529
  const localSeq = this.getNextLocalSeq();
528
- if (this.isCollaborating && rollback !== true) {
530
+ if (this.isCollaborating) {
529
531
  this.submitDelta({
530
532
  opName: "add",
531
533
  value: serializedInterval,
@@ -555,8 +557,7 @@ export class IntervalCollection extends TypedEventEmitter {
555
557
  }, {
556
558
  type: "delete",
557
559
  localSeq: this.getNextLocalSeq(),
558
- previous: value,
559
- });
560
+ }, interval);
560
561
  }
561
562
  else {
562
563
  if (this.onDeserialize) {
@@ -573,7 +574,7 @@ export class IntervalCollection extends TypedEventEmitter {
573
574
  if (!this.localCollection) {
574
575
  throw new LoggingError("Attach must be called before accessing intervals");
575
576
  }
576
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
577
+ const interval = this.getIntervalById(id);
577
578
  if (interval) {
578
579
  this.deleteExistingInterval({ interval, local: true });
579
580
  }
@@ -621,13 +622,12 @@ export class IntervalCollection extends TypedEventEmitter {
621
622
  const metadata = {
622
623
  type: "change",
623
624
  localSeq,
624
- previous: interval.serialize(),
625
625
  interval: newInterval ?? interval,
626
626
  };
627
627
  this.submitDelta({
628
628
  opName: "change",
629
629
  value: serializedInterval,
630
- }, metadata);
630
+ }, metadata, interval);
631
631
  }
632
632
  if (deltaProps !== undefined) {
633
633
  this.emit("propertyChanged", interval, deltaProps, true, undefined);
@@ -651,7 +651,7 @@ export class IntervalCollection extends TypedEventEmitter {
651
651
  hasPendingEndpointChanges(id) {
652
652
  return this.pending[id]?.endpointChanges?.empty === false;
653
653
  }
654
- ackChange(serializedInterval, local, op) {
654
+ ackChange(serializedInterval, local, op, localOpMetadata) {
655
655
  if (!this.localCollection) {
656
656
  throw new LoggingError("Attach must be called before accessing intervals");
657
657
  }
@@ -660,44 +660,39 @@ export class IntervalCollection extends TypedEventEmitter {
660
660
  // strip it out of the properties here.
661
661
  const { id, properties } = getSerializedProperties(serializedInterval);
662
662
  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
663
  if (local) {
664
+ assert(localOpMetadata !== undefined, 0xbd4 /* local must have metadata */);
665
+ const { interval } = localOpMetadata;
669
666
  interval.ackPropertiesChange(properties, op);
670
667
  this.ackInterval(interval, op);
668
+ return interval;
671
669
  }
672
670
  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);
671
+ const latestInterval = this.getIntervalById(id);
672
+ const intervalToChange = this.pending[id]?.consensus ?? latestInterval;
673
+ const isLatestInterval = intervalToChange === latestInterval;
674
+ if (!intervalToChange) {
675
+ return intervalToChange;
676
+ }
677
+ const deltaProps = intervalToChange.changeProperties(properties, op);
678
+ let newInterval = intervalToChange;
679
+ if (hasEndpointChanges(serializedInterval)) {
680
+ const { start, end, startSide, endSide } = serializedInterval;
681
+ newInterval = intervalToChange.modify("", toOptionalSequencePlace(start, startSide ?? Side.Before), toOptionalSequencePlace(end, endSide ?? Side.Before), op);
682
+ if (isLatestInterval) {
683
+ this.localCollection.removeExistingInterval(intervalToChange);
684
+ this.localCollection.add(newInterval);
685
+ this.emitChange(newInterval, intervalToChange, local, false, op);
686
+ if (this.onDeserialize) {
687
+ this.onDeserialize(newInterval);
688
+ }
689
+ }
695
690
  }
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);
691
+ if (deltaProps !== undefined && Object.keys(deltaProps).length > 0) {
692
+ this.emit("propertyChanged", latestInterval, deltaProps, local, op);
693
+ this.emit("changed", latestInterval, deltaProps, undefined, local, false);
700
694
  }
695
+ return newInterval;
701
696
  }
702
697
  }
703
698
  /**
@@ -731,8 +726,8 @@ export class IntervalCollection extends TypedEventEmitter {
731
726
  }
732
727
  const { localSeq, interval } = localOpMetadata;
733
728
  const { id } = getSerializedProperties(original);
734
- const rebasedEndpoint = (localOpMetadata.rebased ??= this.computeRebasedPositions(localOpMetadata, squash));
735
- const localInterval = this.localCollection.idIntervalIndex.getIntervalById(id);
729
+ const rebasedEndpoint = this.computeRebasedPositions(localOpMetadata, squash);
730
+ const localInterval = this.getIntervalById(id);
736
731
  // if the interval slid off the string, rebase the op to be a noop and delete the interval.
737
732
  if (rebasedEndpoint === "detached") {
738
733
  if (localInterval !== undefined &&
@@ -755,8 +750,7 @@ export class IntervalCollection extends TypedEventEmitter {
755
750
  this.localCollection.add(interval);
756
751
  this.emitChange(interval, old, true, true);
757
752
  }
758
- this.client.removeLocalReferencePosition(old.start);
759
- this.client.removeLocalReferencePosition(old.end);
753
+ old.dispose();
760
754
  }
761
755
  return {
762
756
  ...original,
@@ -805,10 +799,13 @@ export class IntervalCollection extends TypedEventEmitter {
805
799
  }
806
800
  // `interval`'s endpoints will get modified in-place, so clone it prior to doing so for event emission.
807
801
  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);
802
+ const isLatestInterval = this.getIntervalById(id) === interval;
803
+ if (isLatestInterval) {
804
+ // In this case, where we change the start or end of an interval,
805
+ // it is necessary to remove and re-add the interval listeners.
806
+ // This ensures that the correct listeners are added to the LocalReferencePosition.
807
+ this.localCollection.removeExistingInterval(interval);
808
+ }
812
809
  if (!this.client) {
813
810
  throw new LoggingError("client does not exist");
814
811
  }
@@ -850,19 +847,18 @@ export class IntervalCollection extends TypedEventEmitter {
850
847
  oldInterval.end.refType = ReferenceType.Transient;
851
848
  oldSeg?.localRefs?.addLocalRef(oldInterval.end, oldInterval.end.getOffset());
852
849
  }
853
- this.localCollection.add(interval);
854
- this.emitChange(interval, oldInterval, true, true, op);
850
+ if (isLatestInterval) {
851
+ this.localCollection.add(interval);
852
+ this.emitChange(interval, oldInterval, true, true, op);
853
+ }
855
854
  }
856
855
  }
857
856
  ackAdd(serializedInterval, local, op, localOpMetadata) {
858
857
  const { id, properties } = getSerializedProperties(serializedInterval);
859
858
  if (local) {
860
859
  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;
860
+ this.ackInterval(localOpMetadata.interval, op);
861
+ return localOpMetadata.interval;
866
862
  }
867
863
  if (!this.localCollection) {
868
864
  throw new LoggingError("attachSequence must be called");
@@ -887,7 +883,7 @@ export class IntervalCollection extends TypedEventEmitter {
887
883
  throw new LoggingError("attach must be called prior to deleting intervals");
888
884
  }
889
885
  const { id } = getSerializedProperties(serializedInterval);
890
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
886
+ const interval = this.getIntervalById(id);
891
887
  if (interval) {
892
888
  this.deleteExistingInterval({ interval, local, op });
893
889
  }