@fluidframework/sequence 2.0.0-internal.2.3.1 → 2.0.0-internal.3.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.
@@ -622,15 +622,20 @@ function createPositionReferenceFromSegoff(
622
622
  client: Client,
623
623
  segoff: { segment: ISegment | undefined; offset: number | undefined; },
624
624
  refType: ReferenceType,
625
- op?: ISequencedDocumentMessage): LocalReferencePosition {
625
+ op?: ISequencedDocumentMessage,
626
+ localSeq?: number): LocalReferencePosition {
626
627
  if (segoff.segment) {
627
628
  const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
628
629
  return ref;
629
630
  }
630
631
 
631
- if (!op && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
632
- // reference to segment that dne locally
633
- throw new UsageError("Non-transient references need segment");
632
+ // Creating references on detached segments is allowed for:
633
+ // - Transient segments
634
+ // - References coming from a remote client (location may have been concurrently removed)
635
+ // - References being rebased to a new sequence number
636
+ // (segment they originally referred to may have been removed with no suitable replacement)
637
+ if (!op && !localSeq && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
638
+ throw new UsageError("Non-transient references need segment");
634
639
  }
635
640
 
636
641
  return createDetachedLocalReferencePosition(refType);
@@ -647,14 +652,14 @@ function createPositionReference(
647
652
  let segoff;
648
653
  if (op) {
649
654
  assert((refType & ReferenceType.SlideOnRemove) !== 0, 0x2f5 /* op create references must be SlideOnRemove */);
650
- segoff = client.getContainingSegment(pos, op);
655
+ segoff = client.getContainingSegment(pos, { referenceSequenceNumber: op.referenceSequenceNumber, clientId: op.clientId });
651
656
  segoff = client.getSlideToSegment(segoff);
652
657
  } else {
653
658
  assert((refType & ReferenceType.SlideOnRemove) === 0 || !!fromSnapshot,
654
659
  0x2f6 /* SlideOnRemove references must be op created */);
655
660
  segoff = client.getContainingSegment(pos, undefined, localSeq);
656
661
  }
657
- return createPositionReferenceFromSegoff(client, segoff, refType, op);
662
+ return createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq);
658
663
  }
659
664
 
660
665
  export function createSequenceInterval(
@@ -1039,19 +1044,26 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1039
1044
  };
1040
1045
  if (interval instanceof SequenceInterval) {
1041
1046
  let previousInterval: TInterval & SequenceInterval | undefined;
1047
+ let pendingChanges = 0;
1042
1048
  interval.addPositionChangeListeners(
1043
1049
  () => {
1044
- assert(!previousInterval, 0x3f9 /* Invalid interleaving of before/after slide */);
1045
- previousInterval = interval.clone() as TInterval & SequenceInterval;
1046
- previousInterval.start = cloneRef(previousInterval.start);
1047
- previousInterval.end = cloneRef(previousInterval.end);
1048
- this.removeIntervalFromIndex(interval);
1050
+ pendingChanges++;
1051
+ // Note: both start and end can change and invoke beforeSlide on each endpoint before afterSlide.
1052
+ if (!previousInterval) {
1053
+ previousInterval = interval.clone() as TInterval & SequenceInterval;
1054
+ previousInterval.start = cloneRef(previousInterval.start);
1055
+ previousInterval.end = cloneRef(previousInterval.end);
1056
+ this.removeIntervalFromIndex(interval);
1057
+ }
1049
1058
  },
1050
1059
  () => {
1051
1060
  assert(previousInterval !== undefined, 0x3fa /* Invalid interleaving of before/after slide */);
1052
- this.addIntervalToIndex(interval);
1053
- this.onPositionChange?.(interval, previousInterval);
1054
- previousInterval = undefined;
1061
+ pendingChanges--;
1062
+ if (pendingChanges === 0) {
1063
+ this.addIntervalToIndex(interval);
1064
+ this.onPositionChange?.(interval, previousInterval);
1065
+ previousInterval = undefined;
1066
+ }
1055
1067
  },
1056
1068
  );
1057
1069
  }
@@ -1176,14 +1188,14 @@ export function makeOpsMap<T extends ISerializableInterval>(): Map<string, IValu
1176
1188
  [[
1177
1189
  "add",
1178
1190
  {
1179
- process: (collection, params, local, op) => {
1191
+ process: (collection, params, local, op, localOpMetadata) => {
1180
1192
  // if params is undefined, the interval was deleted during
1181
1193
  // rebasing
1182
1194
  if (!params) {
1183
1195
  return;
1184
1196
  }
1185
1197
  assert(op !== undefined, 0x3fb /* op should exist here */);
1186
- collection.ackAdd(params, local, op);
1198
+ collection.ackAdd(params, local, op, localOpMetadata);
1187
1199
  },
1188
1200
  rebase,
1189
1201
  },
@@ -1204,14 +1216,14 @@ export function makeOpsMap<T extends ISerializableInterval>(): Map<string, IValu
1204
1216
  [
1205
1217
  "change",
1206
1218
  {
1207
- process: (collection, params, local, op) => {
1219
+ process: (collection, params, local, op, localOpMetadata) => {
1208
1220
  // if params is undefined, the interval was deleted during
1209
1221
  // rebasing
1210
1222
  if (!params) {
1211
1223
  return;
1212
1224
  }
1213
1225
  assert(op !== undefined, 0x3fd /* op should exist here */);
1214
- collection.ackChange(params, local, op);
1226
+ collection.ackChange(params, local, op, localOpMetadata);
1215
1227
  },
1216
1228
  rebase,
1217
1229
  },
@@ -1312,6 +1324,8 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1312
1324
  private localCollection: LocalIntervalCollection<TInterval> | undefined;
1313
1325
  private onDeserialize: DeserializeCallback | undefined;
1314
1326
  private client: Client | undefined;
1327
+ private readonly localSeqToSerializedInterval = new Map<number, ISerializedInterval | SerializedIntervalDelta>();
1328
+ private readonly localSeqToRebasedInterval = new Map<number, ISerializedInterval | SerializedIntervalDelta>();
1315
1329
  private readonly pendingChangesStart: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
1316
1330
  private readonly pendingChangesEnd: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
1317
1331
 
@@ -1333,6 +1347,46 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1333
1347
  : serializedIntervals.intervals.map((i) => decompressInterval(i, serializedIntervals.label));
1334
1348
  }
1335
1349
 
1350
+ private rebasePositionWithSegmentSlide(
1351
+ pos: number,
1352
+ seqNumberFrom: number,
1353
+ localSeq: number
1354
+ ): number | undefined {
1355
+ if (!this.client) {
1356
+ throw new LoggingError("mergeTree client must exist");
1357
+ }
1358
+ const { clientId } = this.client.getCollabWindow();
1359
+ const { segment, offset } = this.client.getContainingSegment(pos, { referenceSequenceNumber: seqNumberFrom, clientId: this.client.getLongClientId(clientId) }, localSeq);
1360
+
1361
+ // if segment is undefined, it slid off the string
1362
+ assert(segment !== undefined, 0x54e /* No segment found */);
1363
+
1364
+ const segoff = this.client.getSlideToSegment({ segment, offset }) ?? segment;
1365
+
1366
+ // case happens when rebasing op, but concurrently entire string has been deleted
1367
+ if (segoff.segment === undefined || segoff.offset === undefined) {
1368
+ return DetachedReferencePosition;
1369
+ }
1370
+
1371
+ assert(offset !== undefined && 0 <= offset && offset < segment.cachedLength, 0x54f /* Invalid offset */);
1372
+ return this.client.findReconnectionPosition(segoff.segment, localSeq) + segoff.offset;
1373
+ }
1374
+
1375
+ private computeRebasedPositions(localSeq: number): ISerializedInterval | SerializedIntervalDelta {
1376
+ assert(this.client !== undefined, 0x550 /* Client should be defined when computing rebased position */);
1377
+ const original = this.localSeqToSerializedInterval.get(localSeq);
1378
+ assert(original !== undefined, 0x551 /* Failed to store pending serialized interval info for this localSeq. */);
1379
+ const rebased = { ...original };
1380
+ const { start, end, sequenceNumber } = original;
1381
+ if (start !== undefined) {
1382
+ rebased.start = this.rebasePositionWithSegmentSlide(start, sequenceNumber, localSeq);
1383
+ }
1384
+ if (end !== undefined) {
1385
+ rebased.end = this.rebasePositionWithSegmentSlide(end, sequenceNumber, localSeq);
1386
+ }
1387
+ return rebased;
1388
+ }
1389
+
1336
1390
  /** @internal */
1337
1391
  public attachGraph(client: Client, label: string) {
1338
1392
  if (this.attached) {
@@ -1345,6 +1399,14 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1345
1399
 
1346
1400
  // Instantiate the local interval collection based on the saved intervals
1347
1401
  this.client = client;
1402
+ if (client) {
1403
+ client.on("normalize", () => {
1404
+ for (const localSeq of this.localSeqToSerializedInterval.keys()) {
1405
+ this.localSeqToRebasedInterval.set(localSeq, this.computeRebasedPositions(localSeq));
1406
+ }
1407
+ });
1408
+ }
1409
+
1348
1410
  this.localCollection = new LocalIntervalCollection<TInterval>(
1349
1411
  client,
1350
1412
  label,
@@ -1450,8 +1512,10 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1450
1512
  sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1451
1513
  start,
1452
1514
  };
1515
+ const localSeq = this.getNextLocalSeq();
1516
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1453
1517
  // Local ops get submitted to the server. Remote ops have the deserializer run.
1454
- this.emitter.emit("add", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
1518
+ this.emitter.emit("add", undefined, serializedInterval, { localSeq });
1455
1519
  }
1456
1520
 
1457
1521
  this.emit("addInterval", interval, true, undefined);
@@ -1531,7 +1595,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1531
1595
 
1532
1596
  serializedInterval.properties = props;
1533
1597
  serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
1534
- this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
1598
+ const localSeq = this.getNextLocalSeq();
1599
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1600
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1535
1601
  this.emit("propertyChanged", interval, deltaProps, true, undefined);
1536
1602
  }
1537
1603
  }
@@ -1567,7 +1633,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1567
1633
  {
1568
1634
  [reservedIntervalIdKey]: interval.getIntervalId(),
1569
1635
  };
1570
- this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
1636
+ const localSeq = this.getNextLocalSeq();
1637
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1638
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1571
1639
  this.addPendingChange(id, serializedInterval);
1572
1640
  this.emitChange(newInterval, interval, true);
1573
1641
  return newInterval;
@@ -1638,12 +1706,19 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1638
1706
  }
1639
1707
 
1640
1708
  /** @internal */
1641
- public ackChange(serializedInterval: ISerializedInterval, local: boolean, op: ISequencedDocumentMessage) {
1709
+ public ackChange(
1710
+ serializedInterval: ISerializedInterval,
1711
+ local: boolean,
1712
+ op: ISequencedDocumentMessage,
1713
+ localOpMetadata: IMapMessageLocalMetadata | undefined,
1714
+ ) {
1642
1715
  if (!this.localCollection) {
1643
1716
  throw new LoggingError("Attach must be called before accessing intervals");
1644
1717
  }
1645
1718
 
1646
1719
  if (local) {
1720
+ assert(localOpMetadata !== undefined, 0x552 /* op metadata should be defined for local op */);
1721
+ this.localSeqToSerializedInterval.delete(localOpMetadata?.localSeq);
1647
1722
  // This is an ack from the server. Remove the pending change.
1648
1723
  this.removePendingChange(serializedInterval);
1649
1724
  }
@@ -1745,11 +1820,10 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1745
1820
  throw new LoggingError("attachSequence must be called");
1746
1821
  }
1747
1822
 
1748
- const { start, end, intervalType, properties, sequenceNumber } = serializedInterval;
1749
- const startRebased = start === undefined ? undefined :
1750
- this.client.rebasePosition(start, sequenceNumber, localSeq);
1751
- const endRebased = end === undefined ? undefined :
1752
- this.client.rebasePosition(end, sequenceNumber, localSeq);
1823
+ const { intervalType, properties } = serializedInterval;
1824
+
1825
+ const { start: startRebased, end: endRebased } = this.localSeqToRebasedInterval.get(localSeq)
1826
+ ?? this.computeRebasedPositions(localSeq);
1753
1827
 
1754
1828
  const intervalId = properties?.[reservedIntervalIdKey];
1755
1829
  const localInterval = this.localCollection?.getIntervalById(intervalId);
@@ -1767,8 +1841,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1767
1841
  this.addPendingChange(intervalId, rebased);
1768
1842
  }
1769
1843
 
1770
- // if the interval slid off the string, rebase the op to be a noop and
1771
- // delete the interval
1844
+ // if the interval slid off the string, rebase the op to be a noop and delete the interval.
1772
1845
  if (startRebased === DetachedReferencePosition || endRebased === DetachedReferencePosition) {
1773
1846
  if (localInterval) {
1774
1847
  this.localCollection?.removeExistingInterval(localInterval);
@@ -1776,27 +1849,15 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1776
1849
  return undefined;
1777
1850
  }
1778
1851
 
1779
- if (!localInterval) {
1780
- return rebased;
1781
- }
1782
-
1783
- // we know we must be using `SequenceInterval` because `this.client` exists
1784
- assert(
1785
- localInterval instanceof SequenceInterval,
1786
- 0x3a0 /* localInterval must be `SequenceInterval` when used with client */,
1787
- );
1788
-
1789
- const startSegment = this.getSlideToSegment(localInterval.start);
1790
- const endSegment = this.getSlideToSegment(localInterval.end);
1791
-
1792
- // we need to slide because the reference has been removed
1793
- if (startSegment || endSegment) {
1794
- const newStart =
1795
- startSegment && this.client.getPosition(startSegment.segment, localSeq) + (startSegment.offset ?? 0);
1796
- const newEnd =
1797
- endSegment && this.client.getPosition(endSegment.segment, localSeq) + (endSegment.offset ?? 0);
1798
-
1799
- this.localCollection?.changeInterval(localInterval, newStart, newEnd, undefined, localSeq);
1852
+ if (localInterval !== undefined) {
1853
+ // we know we must be using `SequenceInterval` because `this.client` exists
1854
+ assert(
1855
+ localInterval instanceof SequenceInterval,
1856
+ 0x3a0 /* localInterval must be `SequenceInterval` when used with client */,
1857
+ );
1858
+ // The rebased op may place this interval's endpoints on different segments. Calling `changeInterval` here
1859
+ // updates the local client's state to be consistent with the emitted op.
1860
+ this.localCollection?.changeInterval(localInterval, startRebased, endRebased, undefined, localSeq);
1800
1861
  }
1801
1862
 
1802
1863
  return rebased;
@@ -1905,8 +1966,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1905
1966
  public ackAdd(
1906
1967
  serializedInterval: ISerializedInterval,
1907
1968
  local: boolean,
1908
- op: ISequencedDocumentMessage) {
1969
+ op: ISequencedDocumentMessage,
1970
+ localOpMetadata: IMapMessageLocalMetadata | undefined,
1971
+ ) {
1909
1972
  if (local) {
1973
+ assert(localOpMetadata !== undefined, 0x553 /* op metadata should be defined for local op */);
1974
+ this.localSeqToSerializedInterval.delete(localOpMetadata.localSeq);
1910
1975
  const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1911
1976
  const localInterval = this.getIntervalById(id);
1912
1977
  if (localInterval) {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/sequence";
9
- export const pkgVersion = "2.0.0-internal.2.3.1";
9
+ export const pkgVersion = "2.0.0-internal.3.0.0";
package/src/sequence.ts CHANGED
@@ -200,40 +200,12 @@ export abstract class SharedSegmentSequence<T extends ISegment>
200
200
  ChildLogger.create(this.logger, "SharedSegmentSequence.MergeTreeClient"),
201
201
  mergeTreeOptions);
202
202
 
203
- super.on("newListener", (event) => {
204
- switch (event) {
205
- case "sequenceDelta":
206
- if (!this.client.mergeTreeDeltaCallback) {
207
- this.client.mergeTreeDeltaCallback = (opArgs, deltaArgs) => {
208
- this.emit("sequenceDelta", new SequenceDeltaEvent(opArgs, deltaArgs, this.client), this);
209
- };
210
- }
211
- break;
212
- case "maintenance":
213
- if (!this.client.mergeTreeMaintenanceCallback) {
214
- this.client.mergeTreeMaintenanceCallback = (args, opArgs) => {
215
- this.emit("maintenance", new SequenceMaintenanceEvent(opArgs, args, this.client), this);
216
- };
217
- }
218
- break;
219
- default:
220
- }
203
+ this.client.on("delta", (opArgs, deltaArgs) => {
204
+ this.emit("sequenceDelta", new SequenceDeltaEvent(opArgs, deltaArgs, this.client), this);
221
205
  });
222
- super.on("removeListener", (event: string | symbol) => {
223
- switch (event) {
224
- case "sequenceDelta":
225
- if (super.listenerCount(event) === 0) {
226
- this.client.mergeTreeDeltaCallback = undefined;
227
- }
228
- break;
229
- case "maintenance":
230
- if (super.listenerCount(event) === 0) {
231
- this.client.mergeTreeMaintenanceCallback = undefined;
232
- }
233
- break;
234
- default:
235
- break;
236
- }
206
+
207
+ this.client.on("maintenance", (args, opArgs) => {
208
+ this.emit("maintenance", new SequenceMaintenanceEvent(opArgs, args, this.client), this);
237
209
  });
238
210
 
239
211
  this.intervalCollections = new DefaultMap(