@fluidframework/sequence 2.0.0-internal.2.4.0 → 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.
@@ -423,13 +423,17 @@ export class SequenceInterval {
423
423
  }
424
424
  }
425
425
  }
426
- function createPositionReferenceFromSegoff(client, segoff, refType, op) {
426
+ function createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq) {
427
427
  if (segoff.segment) {
428
428
  const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
429
429
  return ref;
430
430
  }
431
- if (!op && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
432
- // reference to segment that dne locally
431
+ // Creating references on detached segments is allowed for:
432
+ // - Transient segments
433
+ // - References coming from a remote client (location may have been concurrently removed)
434
+ // - References being rebased to a new sequence number
435
+ // (segment they originally referred to may have been removed with no suitable replacement)
436
+ if (!op && !localSeq && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
433
437
  throw new UsageError("Non-transient references need segment");
434
438
  }
435
439
  return createDetachedLocalReferencePosition(refType);
@@ -438,14 +442,14 @@ function createPositionReference(client, pos, refType, op, fromSnapshot, localSe
438
442
  let segoff;
439
443
  if (op) {
440
444
  assert((refType & ReferenceType.SlideOnRemove) !== 0, 0x2f5 /* op create references must be SlideOnRemove */);
441
- segoff = client.getContainingSegment(pos, op);
445
+ segoff = client.getContainingSegment(pos, { referenceSequenceNumber: op.referenceSequenceNumber, clientId: op.clientId });
442
446
  segoff = client.getSlideToSegment(segoff);
443
447
  }
444
448
  else {
445
449
  assert((refType & ReferenceType.SlideOnRemove) === 0 || !!fromSnapshot, 0x2f6 /* SlideOnRemove references must be op created */);
446
450
  segoff = client.getContainingSegment(pos, undefined, localSeq);
447
451
  }
448
- return createPositionReferenceFromSegoff(client, segoff, refType, op);
452
+ return createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq);
449
453
  }
450
454
  export function createSequenceInterval(label, start, end, client, intervalType, op, fromSnapshot) {
451
455
  let beginRefType = ReferenceType.RangeBegin;
@@ -740,18 +744,25 @@ export class LocalIntervalCollection {
740
744
  };
741
745
  if (interval instanceof SequenceInterval) {
742
746
  let previousInterval;
747
+ let pendingChanges = 0;
743
748
  interval.addPositionChangeListeners(() => {
744
- assert(!previousInterval, 0x3f9 /* Invalid interleaving of before/after slide */);
745
- previousInterval = interval.clone();
746
- previousInterval.start = cloneRef(previousInterval.start);
747
- previousInterval.end = cloneRef(previousInterval.end);
748
- this.removeIntervalFromIndex(interval);
749
+ pendingChanges++;
750
+ // Note: both start and end can change and invoke beforeSlide on each endpoint before afterSlide.
751
+ if (!previousInterval) {
752
+ previousInterval = interval.clone();
753
+ previousInterval.start = cloneRef(previousInterval.start);
754
+ previousInterval.end = cloneRef(previousInterval.end);
755
+ this.removeIntervalFromIndex(interval);
756
+ }
749
757
  }, () => {
750
758
  var _a;
751
759
  assert(previousInterval !== undefined, 0x3fa /* Invalid interleaving of before/after slide */);
752
- this.addIntervalToIndex(interval);
753
- (_a = this.onPositionChange) === null || _a === void 0 ? void 0 : _a.call(this, interval, previousInterval);
754
- previousInterval = undefined;
760
+ pendingChanges--;
761
+ if (pendingChanges === 0) {
762
+ this.addIntervalToIndex(interval);
763
+ (_a = this.onPositionChange) === null || _a === void 0 ? void 0 : _a.call(this, interval, previousInterval);
764
+ previousInterval = undefined;
765
+ }
755
766
  });
756
767
  }
757
768
  }
@@ -835,14 +846,14 @@ export function makeOpsMap() {
835
846
  return new Map([[
836
847
  "add",
837
848
  {
838
- process: (collection, params, local, op) => {
849
+ process: (collection, params, local, op, localOpMetadata) => {
839
850
  // if params is undefined, the interval was deleted during
840
851
  // rebasing
841
852
  if (!params) {
842
853
  return;
843
854
  }
844
855
  assert(op !== undefined, 0x3fb /* op should exist here */);
845
- collection.ackAdd(params, local, op);
856
+ collection.ackAdd(params, local, op, localOpMetadata);
846
857
  },
847
858
  rebase,
848
859
  },
@@ -863,14 +874,14 @@ export function makeOpsMap() {
863
874
  [
864
875
  "change",
865
876
  {
866
- process: (collection, params, local, op) => {
877
+ process: (collection, params, local, op, localOpMetadata) => {
867
878
  // if params is undefined, the interval was deleted during
868
879
  // rebasing
869
880
  if (!params) {
870
881
  return;
871
882
  }
872
883
  assert(op !== undefined, 0x3fd /* op should exist here */);
873
- collection.ackChange(params, local, op);
884
+ collection.ackChange(params, local, op, localOpMetadata);
874
885
  },
875
886
  rebase,
876
887
  },
@@ -909,6 +920,8 @@ export class IntervalCollection extends TypedEventEmitter {
909
920
  this.helpers = helpers;
910
921
  this.requiresClient = requiresClient;
911
922
  this.emitter = emitter;
923
+ this.localSeqToSerializedInterval = new Map();
924
+ this.localSeqToRebasedInterval = new Map();
912
925
  this.pendingChangesStart = new Map();
913
926
  this.pendingChangesEnd = new Map();
914
927
  this.savedSerializedIntervals = Array.isArray(serializedIntervals)
@@ -918,6 +931,37 @@ export class IntervalCollection extends TypedEventEmitter {
918
931
  get attached() {
919
932
  return !!this.localCollection;
920
933
  }
934
+ rebasePositionWithSegmentSlide(pos, seqNumberFrom, localSeq) {
935
+ var _a;
936
+ if (!this.client) {
937
+ throw new LoggingError("mergeTree client must exist");
938
+ }
939
+ const { clientId } = this.client.getCollabWindow();
940
+ const { segment, offset } = this.client.getContainingSegment(pos, { referenceSequenceNumber: seqNumberFrom, clientId: this.client.getLongClientId(clientId) }, localSeq);
941
+ // if segment is undefined, it slid off the string
942
+ assert(segment !== undefined, 0x54e /* No segment found */);
943
+ const segoff = (_a = this.client.getSlideToSegment({ segment, offset })) !== null && _a !== void 0 ? _a : segment;
944
+ // case happens when rebasing op, but concurrently entire string has been deleted
945
+ if (segoff.segment === undefined || segoff.offset === undefined) {
946
+ return DetachedReferencePosition;
947
+ }
948
+ assert(offset !== undefined && 0 <= offset && offset < segment.cachedLength, 0x54f /* Invalid offset */);
949
+ return this.client.findReconnectionPosition(segoff.segment, localSeq) + segoff.offset;
950
+ }
951
+ computeRebasedPositions(localSeq) {
952
+ assert(this.client !== undefined, 0x550 /* Client should be defined when computing rebased position */);
953
+ const original = this.localSeqToSerializedInterval.get(localSeq);
954
+ assert(original !== undefined, 0x551 /* Failed to store pending serialized interval info for this localSeq. */);
955
+ const rebased = Object.assign({}, original);
956
+ const { start, end, sequenceNumber } = original;
957
+ if (start !== undefined) {
958
+ rebased.start = this.rebasePositionWithSegmentSlide(start, sequenceNumber, localSeq);
959
+ }
960
+ if (end !== undefined) {
961
+ rebased.end = this.rebasePositionWithSegmentSlide(end, sequenceNumber, localSeq);
962
+ }
963
+ return rebased;
964
+ }
921
965
  /** @internal */
922
966
  attachGraph(client, label) {
923
967
  if (this.attached) {
@@ -928,6 +972,13 @@ export class IntervalCollection extends TypedEventEmitter {
928
972
  }
929
973
  // Instantiate the local interval collection based on the saved intervals
930
974
  this.client = client;
975
+ if (client) {
976
+ client.on("normalize", () => {
977
+ for (const localSeq of this.localSeqToSerializedInterval.keys()) {
978
+ this.localSeqToRebasedInterval.set(localSeq, this.computeRebasedPositions(localSeq));
979
+ }
980
+ });
981
+ }
931
982
  this.localCollection = new LocalIntervalCollection(client, label, this.helpers, (interval, previousInterval) => this.emitChange(interval, previousInterval, true));
932
983
  if (this.savedSerializedIntervals) {
933
984
  for (const serializedInterval of this.savedSerializedIntervals) {
@@ -1005,8 +1056,10 @@ export class IntervalCollection extends TypedEventEmitter {
1005
1056
  sequenceNumber: (_b = (_a = this.client) === null || _a === void 0 ? void 0 : _a.getCurrentSeq()) !== null && _b !== void 0 ? _b : 0,
1006
1057
  start,
1007
1058
  };
1059
+ const localSeq = this.getNextLocalSeq();
1060
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1008
1061
  // Local ops get submitted to the server. Remote ops have the deserializer run.
1009
- this.emitter.emit("add", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
1062
+ this.emitter.emit("add", undefined, serializedInterval, { localSeq });
1010
1063
  }
1011
1064
  this.emit("addInterval", interval, true, undefined);
1012
1065
  return interval;
@@ -1072,7 +1125,9 @@ export class IntervalCollection extends TypedEventEmitter {
1072
1125
  serializedInterval.end = undefined;
1073
1126
  serializedInterval.properties = props;
1074
1127
  serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
1075
- this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
1128
+ const localSeq = this.getNextLocalSeq();
1129
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1130
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1076
1131
  this.emit("propertyChanged", interval, deltaProps, true, undefined);
1077
1132
  }
1078
1133
  }
@@ -1105,7 +1160,9 @@ export class IntervalCollection extends TypedEventEmitter {
1105
1160
  {
1106
1161
  [reservedIntervalIdKey]: interval.getIntervalId(),
1107
1162
  };
1108
- this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
1163
+ const localSeq = this.getNextLocalSeq();
1164
+ this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1165
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1109
1166
  this.addPendingChange(id, serializedInterval);
1110
1167
  this.emitChange(newInterval, interval, true);
1111
1168
  return newInterval;
@@ -1162,12 +1219,14 @@ export class IntervalCollection extends TypedEventEmitter {
1162
1219
  return entries && entries.length !== 0;
1163
1220
  }
1164
1221
  /** @internal */
1165
- ackChange(serializedInterval, local, op) {
1222
+ ackChange(serializedInterval, local, op, localOpMetadata) {
1166
1223
  var _a, _b, _c, _d;
1167
1224
  if (!this.localCollection) {
1168
1225
  throw new LoggingError("Attach must be called before accessing intervals");
1169
1226
  }
1170
1227
  if (local) {
1228
+ assert(localOpMetadata !== undefined, 0x552 /* op metadata should be defined for local op */);
1229
+ this.localSeqToSerializedInterval.delete(localOpMetadata === null || localOpMetadata === void 0 ? void 0 : localOpMetadata.localSeq);
1171
1230
  // This is an ack from the server. Remove the pending change.
1172
1231
  this.removePendingChange(serializedInterval);
1173
1232
  }
@@ -1247,7 +1306,7 @@ export class IntervalCollection extends TypedEventEmitter {
1247
1306
  * @internal
1248
1307
  */
1249
1308
  rebaseLocalInterval(opName, serializedInterval, localSeq) {
1250
- var _a, _b, _c, _d, _e, _f, _g;
1309
+ var _a, _b, _c, _d, _e, _f;
1251
1310
  if (!this.client) {
1252
1311
  // If there's no associated mergeTree client, the originally submitted op is still correct.
1253
1312
  return serializedInterval;
@@ -1255,44 +1314,34 @@ export class IntervalCollection extends TypedEventEmitter {
1255
1314
  if (!this.attached) {
1256
1315
  throw new LoggingError("attachSequence must be called");
1257
1316
  }
1258
- const { start, end, intervalType, properties, sequenceNumber } = serializedInterval;
1259
- const startRebased = start === undefined ? undefined :
1260
- this.client.rebasePosition(start, sequenceNumber, localSeq);
1261
- const endRebased = end === undefined ? undefined :
1262
- this.client.rebasePosition(end, sequenceNumber, localSeq);
1317
+ const { intervalType, properties } = serializedInterval;
1318
+ const { start: startRebased, end: endRebased } = (_a = this.localSeqToRebasedInterval.get(localSeq)) !== null && _a !== void 0 ? _a : this.computeRebasedPositions(localSeq);
1263
1319
  const intervalId = properties === null || properties === void 0 ? void 0 : properties[reservedIntervalIdKey];
1264
- const localInterval = (_a = this.localCollection) === null || _a === void 0 ? void 0 : _a.getIntervalById(intervalId);
1320
+ const localInterval = (_b = this.localCollection) === null || _b === void 0 ? void 0 : _b.getIntervalById(intervalId);
1265
1321
  const rebased = {
1266
1322
  start: startRebased,
1267
1323
  end: endRebased,
1268
1324
  intervalType,
1269
- sequenceNumber: (_c = (_b = this.client) === null || _b === void 0 ? void 0 : _b.getCurrentSeq()) !== null && _c !== void 0 ? _c : 0,
1325
+ sequenceNumber: (_d = (_c = this.client) === null || _c === void 0 ? void 0 : _c.getCurrentSeq()) !== null && _d !== void 0 ? _d : 0,
1270
1326
  properties,
1271
1327
  };
1272
1328
  if (opName === "change" && (this.hasPendingChangeStart(intervalId) || this.hasPendingChangeEnd(intervalId))) {
1273
1329
  this.removePendingChange(serializedInterval);
1274
1330
  this.addPendingChange(intervalId, rebased);
1275
1331
  }
1276
- // if the interval slid off the string, rebase the op to be a noop and
1277
- // delete the interval
1332
+ // if the interval slid off the string, rebase the op to be a noop and delete the interval.
1278
1333
  if (startRebased === DetachedReferencePosition || endRebased === DetachedReferencePosition) {
1279
1334
  if (localInterval) {
1280
- (_d = this.localCollection) === null || _d === void 0 ? void 0 : _d.removeExistingInterval(localInterval);
1335
+ (_e = this.localCollection) === null || _e === void 0 ? void 0 : _e.removeExistingInterval(localInterval);
1281
1336
  }
1282
1337
  return undefined;
1283
1338
  }
1284
- if (!localInterval) {
1285
- return rebased;
1286
- }
1287
- // we know we must be using `SequenceInterval` because `this.client` exists
1288
- assert(localInterval instanceof SequenceInterval, 0x3a0 /* localInterval must be `SequenceInterval` when used with client */);
1289
- const startSegment = this.getSlideToSegment(localInterval.start);
1290
- const endSegment = this.getSlideToSegment(localInterval.end);
1291
- // we need to slide because the reference has been removed
1292
- if (startSegment || endSegment) {
1293
- const newStart = startSegment && this.client.getPosition(startSegment.segment, localSeq) + ((_e = startSegment.offset) !== null && _e !== void 0 ? _e : 0);
1294
- const newEnd = endSegment && this.client.getPosition(endSegment.segment, localSeq) + ((_f = endSegment.offset) !== null && _f !== void 0 ? _f : 0);
1295
- (_g = this.localCollection) === null || _g === void 0 ? void 0 : _g.changeInterval(localInterval, newStart, newEnd, undefined, localSeq);
1339
+ if (localInterval !== undefined) {
1340
+ // we know we must be using `SequenceInterval` because `this.client` exists
1341
+ assert(localInterval instanceof SequenceInterval, 0x3a0 /* localInterval must be `SequenceInterval` when used with client */);
1342
+ // The rebased op may place this interval's endpoints on different segments. Calling `changeInterval` here
1343
+ // updates the local client's state to be consistent with the emitted op.
1344
+ (_f = this.localCollection) === null || _f === void 0 ? void 0 : _f.changeInterval(localInterval, startRebased, endRebased, undefined, localSeq);
1296
1345
  }
1297
1346
  return rebased;
1298
1347
  }
@@ -1380,9 +1429,11 @@ export class IntervalCollection extends TypedEventEmitter {
1380
1429
  }
1381
1430
  }
1382
1431
  /** @internal */
1383
- ackAdd(serializedInterval, local, op) {
1432
+ ackAdd(serializedInterval, local, op, localOpMetadata) {
1384
1433
  var _a;
1385
1434
  if (local) {
1435
+ assert(localOpMetadata !== undefined, 0x553 /* op metadata should be defined for local op */);
1436
+ this.localSeqToSerializedInterval.delete(localOpMetadata.localSeq);
1386
1437
  const id = (_a = serializedInterval.properties) === null || _a === void 0 ? void 0 : _a[reservedIntervalIdKey];
1387
1438
  const localInterval = this.getIntervalById(id);
1388
1439
  if (localInterval) {