@fluidframework/matrix 2.41.0 → 2.42.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/matrix",
3
- "version": "2.41.0",
3
+ "version": "2.42.0",
4
4
  "description": "Distributed matrix",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -81,17 +81,17 @@
81
81
  "temp-directory": "nyc/.nyc_output"
82
82
  },
83
83
  "dependencies": {
84
- "@fluid-internal/client-utils": "~2.41.0",
85
- "@fluidframework/core-interfaces": "~2.41.0",
86
- "@fluidframework/core-utils": "~2.41.0",
87
- "@fluidframework/datastore-definitions": "~2.41.0",
88
- "@fluidframework/driver-definitions": "~2.41.0",
89
- "@fluidframework/driver-utils": "~2.41.0",
90
- "@fluidframework/merge-tree": "~2.41.0",
91
- "@fluidframework/runtime-definitions": "~2.41.0",
92
- "@fluidframework/runtime-utils": "~2.41.0",
93
- "@fluidframework/shared-object-base": "~2.41.0",
94
- "@fluidframework/telemetry-utils": "~2.41.0",
84
+ "@fluid-internal/client-utils": "~2.42.0",
85
+ "@fluidframework/core-interfaces": "~2.42.0",
86
+ "@fluidframework/core-utils": "~2.42.0",
87
+ "@fluidframework/datastore-definitions": "~2.42.0",
88
+ "@fluidframework/driver-definitions": "~2.42.0",
89
+ "@fluidframework/driver-utils": "~2.42.0",
90
+ "@fluidframework/merge-tree": "~2.42.0",
91
+ "@fluidframework/runtime-definitions": "~2.42.0",
92
+ "@fluidframework/runtime-utils": "~2.42.0",
93
+ "@fluidframework/shared-object-base": "~2.42.0",
94
+ "@fluidframework/telemetry-utils": "~2.42.0",
95
95
  "@tiny-calc/nano": "0.0.0-alpha.5",
96
96
  "double-ended-queue": "^2.1.0-0",
97
97
  "tslib": "^1.10.0"
@@ -99,17 +99,17 @@
99
99
  "devDependencies": {
100
100
  "@arethetypeswrong/cli": "^0.17.1",
101
101
  "@biomejs/biome": "~1.9.3",
102
- "@fluid-internal/mocha-test-setup": "~2.41.0",
103
- "@fluid-private/stochastic-test-utils": "~2.41.0",
104
- "@fluid-private/test-dds-utils": "~2.41.0",
102
+ "@fluid-internal/mocha-test-setup": "~2.42.0",
103
+ "@fluid-private/stochastic-test-utils": "~2.42.0",
104
+ "@fluid-private/test-dds-utils": "~2.42.0",
105
105
  "@fluid-tools/benchmark": "^0.51.0",
106
106
  "@fluid-tools/build-cli": "^0.55.0",
107
107
  "@fluidframework/build-common": "^2.0.3",
108
108
  "@fluidframework/build-tools": "^0.55.0",
109
- "@fluidframework/container-definitions": "~2.41.0",
109
+ "@fluidframework/container-definitions": "~2.42.0",
110
110
  "@fluidframework/eslint-config-fluid": "^5.7.4",
111
- "@fluidframework/matrix-previous": "npm:@fluidframework/matrix@2.40.0",
112
- "@fluidframework/test-runtime-utils": "~2.41.0",
111
+ "@fluidframework/matrix-previous": "npm:@fluidframework/matrix@2.41.0",
112
+ "@fluidframework/test-runtime-utils": "~2.42.0",
113
113
  "@microsoft/api-extractor": "7.52.8",
114
114
  "@tiny-calc/micro": "0.0.0-alpha.5",
115
115
  "@types/double-ended-queue": "^2.1.0",
@@ -172,6 +172,7 @@
172
172
  "lint:fix": "fluid-build . --task eslint:fix --task format",
173
173
  "pack:tests": "tar -cf ./matrix.test-files.tar ./src/test ./dist/test ./lib/test",
174
174
  "test": "npm run test:mocha",
175
+ "test:benchmark:report": "mocha --config src/test/time/.mocharc.cjs",
175
176
  "test:coverage": "c8 npm test",
176
177
  "test:memory": "mocha --config src/test/memory/.mocharc.cjs",
177
178
  "test:memory-profiling:report": "mocha --config src/test/memory/.mocharc.cjs",
package/src/matrix.ts CHANGED
@@ -213,6 +213,22 @@ type FirstWriterWinsPolicy =
213
213
  cellLastWriteTracker: SparseArray2D<CellLastWriteTrackerItem>;
214
214
  };
215
215
 
216
+ /**
217
+ * Tracks pending local changes for a cell.
218
+ */
219
+ interface PendingCellChanges<T> {
220
+ /**
221
+ * The local changes including the local seq, and the value set at that local seq.
222
+ */
223
+ local: { localSeq: number; value: MatrixItem<T> }[];
224
+ /**
225
+ * The latest consensus value across all clients.
226
+ * this will either be a remote value or ack'd local
227
+ * value.
228
+ */
229
+ consensus?: MatrixItem<T>;
230
+ }
231
+
216
232
  /**
217
233
  * A SharedMatrix holds a rectangular 2D array of values. Supported operations
218
234
  * include setting values and inserting/removing rows and columns.
@@ -252,7 +268,7 @@ export class SharedMatrix<T = any>
252
268
  private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
253
269
 
254
270
  private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
255
- private readonly pending = new SparseArray2D<number>(); // Tracks pending writes.
271
+ private readonly pending = new SparseArray2D<PendingCellChanges<T>>(); // Tracks pending writes.
256
272
 
257
273
  private fwwPolicy: FirstWriterWinsPolicy = {
258
274
  state: "off",
@@ -418,21 +434,22 @@ export class SharedMatrix<T = any>
418
434
  value: MatrixItem<T>,
419
435
  rowHandle = this.rows.getAllocatedHandle(row),
420
436
  colHandle = this.cols.getAllocatedHandle(col),
437
+ rollback?: boolean,
421
438
  ): void {
422
439
  this.protectAgainstReentrancy(() => {
423
- if (this.undo !== undefined) {
424
- let oldValue = this.cells.getCell(rowHandle, colHandle);
425
- if (oldValue === null) {
426
- oldValue = undefined;
427
- }
440
+ const oldValue = this.cells.getCell(rowHandle, colHandle) ?? undefined;
428
441
 
442
+ if (this.undo !== undefined) {
429
443
  this.undo.cellSet(rowHandle, colHandle, oldValue);
430
444
  }
431
445
 
432
446
  this.cells.setCell(rowHandle, colHandle, value);
433
447
 
434
- if (this.isAttached()) {
435
- this.sendSetCellOp(row, col, value, rowHandle, colHandle);
448
+ if (this.isAttached() && rollback !== true) {
449
+ const pending = this.sendSetCellOp(row, col, value, rowHandle, colHandle);
450
+ if (pending.local.length === 1) {
451
+ pending.consensus ??= oldValue;
452
+ }
436
453
  }
437
454
 
438
455
  // Avoid reentrancy by raising change notifications after the op is queued.
@@ -467,7 +484,7 @@ export class SharedMatrix<T = any>
467
484
  rowHandle: Handle,
468
485
  colHandle: Handle,
469
486
  localSeq = this.nextLocalSeq(),
470
- ): void {
487
+ ): PendingCellChanges<T> {
471
488
  assert(
472
489
  this.isAttached(),
473
490
  0x1e2 /* "Caller must ensure 'isAttached()' before calling 'sendSetCellOp'." */,
@@ -493,7 +510,12 @@ export class SharedMatrix<T = any>
493
510
  };
494
511
 
495
512
  this.submitLocalMessage(op, metadata);
496
- this.pending.setCell(rowHandle, colHandle, localSeq);
513
+ const pendingCell: PendingCellChanges<T> = this.pending.getCell(rowHandle, colHandle) ?? {
514
+ local: [],
515
+ };
516
+ pendingCell.local.push({ localSeq, value });
517
+ this.pending.setCell(rowHandle, colHandle, pendingCell);
518
+ return pendingCell;
497
519
  }
498
520
 
499
521
  /**
@@ -679,7 +701,16 @@ export class SharedMatrix<T = any>
679
701
  | undefined
680
702
  | number
681
703
  | ReturnType<SparseArray2D<MatrixItem<T> | number>["snapshot"]>
682
- )[] = [this.cells.snapshot(), this.pending.snapshot()];
704
+ )[] = [
705
+ this.cells.snapshot(),
706
+ /**
707
+ * we used to write this.pending.snapshot(). this should have never been done, as pending is only for local
708
+ * changes, and there should never be local changes in the summarizer. This was also never used on load
709
+ * as there is no way to understand a previous clients pending changes. so we just set this to a constant
710
+ * which matches an empty this.pending.snapshot() for back-compat in terms of the array length
711
+ */
712
+ [undefined],
713
+ ];
683
714
 
684
715
  // Only need to store it in the snapshot if we have switched the policy already.
685
716
  if (this.fwwPolicy.state === "on") {
@@ -806,31 +837,43 @@ export class SharedMatrix<T = any>
806
837
  const col = this.rebasePosition(this.cols, colsRef, localSeq);
807
838
  this.rows.removeLocalReferencePosition(rowsRef);
808
839
  this.cols.removeLocalReferencePosition(colsRef);
809
- if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
810
- // If the mode is LWW, then send the op.
840
+
841
+ const pendingCell = this.pending.getCell(rowHandle, colHandle);
842
+ assert(pendingCell !== undefined, 0xba4 /* local operation must have a pending array */);
843
+ const { local } = pendingCell;
844
+ assert(local !== undefined, 0xba5 /* local operation must have a pending array */);
845
+ const localSeqIndex = local.findIndex((p) => p.localSeq === localSeq);
846
+ assert(localSeqIndex >= 0, 0xba6 /* local operation must have a pending entry */);
847
+ const [change] = local.splice(localSeqIndex, 1);
848
+ assert(change.localSeq === localSeq, 0xba7 /* must match */);
849
+
850
+ if (
851
+ row !== undefined &&
852
+ col !== undefined &&
853
+ row >= 0 &&
854
+ col >= 0 && // If the mode is LWW, then send the op.
811
855
  // Otherwise if the current mode is FWW and if we generated this op, after seeing the
812
856
  // last set op, or it is the first set op for the cell, then regenerate the op,
813
857
  // otherwise raise conflict. We want to check the current mode here and not that
814
858
  // whether op was made in FWW or not.
815
- if (
816
- this.fwwPolicy.state !== "on" ||
859
+ (this.fwwPolicy.state !== "on" ||
817
860
  referenceSeqNumber >=
818
- (this.fwwPolicy.cellLastWriteTracker.getCell(rowHandle, colHandle)?.seqNum ?? 0)
819
- ) {
820
- this.sendSetCellOp(row, col, setOp.value, rowHandle, colHandle, localSeq);
821
- } else if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
822
- // Clear the pending changes if any as we are not sending the op.
823
- this.pending.setCell(rowHandle, colHandle, undefined);
824
- }
861
+ (this.fwwPolicy.cellLastWriteTracker.getCell(rowHandle, colHandle)?.seqNum ?? 0))
862
+ ) {
863
+ this.sendSetCellOp(row, col, setOp.value, rowHandle, colHandle, localSeq);
825
864
  }
826
865
  } else {
827
866
  switch (content.target) {
828
867
  case SnapshotPath.cols: {
829
- this.submitColMessage(this.cols.regeneratePendingOp(content, localOpMetadata));
868
+ this.submitColMessage(
869
+ this.cols.regeneratePendingOp(content, localOpMetadata, false),
870
+ );
830
871
  break;
831
872
  }
832
873
  case SnapshotPath.rows: {
833
- this.submitRowMessage(this.rows.regeneratePendingOp(content, localOpMetadata));
874
+ this.submitRowMessage(
875
+ this.rows.regeneratePendingOp(content, localOpMetadata, false),
876
+ );
834
877
  break;
835
878
  }
836
879
  default: {
@@ -840,6 +883,47 @@ export class SharedMatrix<T = any>
840
883
  }
841
884
  }
842
885
 
886
+ protected rollback(content: unknown, localOpMetadata: unknown): void {
887
+ const contents = content as MatrixSetOrVectorOp<T>;
888
+ const target = contents.target;
889
+
890
+ switch (target) {
891
+ case SnapshotPath.cols: {
892
+ this.cols.rollback(content, localOpMetadata);
893
+ break;
894
+ }
895
+ case SnapshotPath.rows: {
896
+ this.rows.rollback(content, localOpMetadata);
897
+ break;
898
+ }
899
+ case undefined: {
900
+ assert(contents.type === MatrixOp.set, 0xba8 /* only sets supported */);
901
+ const setMetadata = localOpMetadata as ISetOpMetadata;
902
+
903
+ const pendingCell = this.pending.getCell(setMetadata.rowHandle, setMetadata.colHandle);
904
+ assert(pendingCell !== undefined, 0xba9 /* must have pending */);
905
+
906
+ const change = pendingCell.local.pop();
907
+ assert(change?.localSeq === setMetadata.localSeq, 0xbaa /* must have change */);
908
+
909
+ const previous =
910
+ pendingCell.local.length > 0
911
+ ? pendingCell.local[pendingCell.local.length - 1].value
912
+ : pendingCell.consensus;
913
+
914
+ this.setCellCore(
915
+ contents.row,
916
+ contents.col,
917
+ previous,
918
+ setMetadata.rowHandle,
919
+ setMetadata.colHandle,
920
+ true,
921
+ );
922
+ }
923
+ default:
924
+ }
925
+ }
926
+
843
927
  protected onDisconnect(): void {}
844
928
 
845
929
  /**
@@ -968,73 +1052,86 @@ export class SharedMatrix<T = any>
968
1052
  // We are receiving the ACK for a local pending set operation.
969
1053
  const { rowHandle, colHandle, localSeq, rowsRef, colsRef } =
970
1054
  localOpMetadata as ISetOpMetadata;
971
- const isLatestPendingOp = this.isLatestPendingWrite(rowHandle, colHandle, localSeq);
972
1055
  this.rows.removeLocalReferencePosition(rowsRef);
973
1056
  this.cols.removeLocalReferencePosition(colsRef);
1057
+
1058
+ const pendingCell = this.pending.getCell(rowHandle, colHandle);
1059
+ const ackedChange = pendingCell?.local.shift();
1060
+ assert(ackedChange?.localSeq === localSeq, 0xbab /* must match */);
1061
+ if (pendingCell?.local.length === 0) {
1062
+ this.pending.setCell(rowHandle, colHandle, undefined);
1063
+ }
1064
+
974
1065
  // If policy is switched and cell should be modified too based on policy, then update the tracker.
975
1066
  // If policy is not switched, then also update the tracker in case it is the latest.
976
1067
  if (
977
1068
  this.fwwPolicy.state === "on" &&
978
1069
  this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)
979
1070
  ) {
1071
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1072
+ pendingCell!.consensus = ackedChange.value;
980
1073
  this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
981
1074
  seqNum: msg.sequenceNumber,
982
1075
  clientId: msg.clientId,
983
1076
  });
984
1077
  }
985
-
986
- if (isLatestPendingOp) {
987
- this.pending.setCell(rowHandle, colHandle, undefined);
988
- }
989
1078
  } else {
990
1079
  const adjustedRow = this.rows.adjustPosition(row, msg);
991
- if (adjustedRow !== undefined) {
992
- const adjustedCol = this.cols.adjustPosition(col, msg);
993
-
994
- if (adjustedCol !== undefined) {
995
- const rowHandle = this.rows.getAllocatedHandle(adjustedRow);
996
- const colHandle = this.cols.getAllocatedHandle(adjustedCol);
997
-
998
- assert(
999
- isHandleValid(rowHandle) && isHandleValid(colHandle),
1000
- 0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
1001
- );
1002
- if (this.fwwPolicy.state === "on") {
1003
- // If someone tried to Overwrite the cell value or first write on this cell or
1004
- // same client tried to modify the cell or if the previous mode was LWW, then we need to still
1005
- // overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
1006
- if (this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)) {
1007
- const previousValue = this.cells.getCell(rowHandle, colHandle);
1008
- this.cells.setCell(rowHandle, colHandle, value);
1009
- this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
1010
- seqNum: msg.sequenceNumber,
1011
- clientId: msg.clientId,
1012
- });
1013
- for (const consumer of this.consumers.values()) {
1014
- consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
1015
- }
1016
- // Check is there are any pending changes, which will be rejected. If so raise conflict.
1017
- if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
1018
- // Don't reset the pending value yet, as there maybe more fww op from same client, so we want
1019
- // to raise conflict event for that op also.
1020
- this.emit(
1021
- "conflict",
1022
- row,
1023
- col,
1024
- value, // Current value
1025
- previousValue, // Ignored local value
1026
- this,
1027
- );
1028
- }
1080
+ const adjustedCol = this.cols.adjustPosition(col, msg);
1081
+
1082
+ const rowHandle = adjustedRow.handle;
1083
+ const colHandle = adjustedCol.handle;
1084
+
1085
+ assert(
1086
+ isHandleValid(rowHandle) && isHandleValid(colHandle),
1087
+ 0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
1088
+ );
1089
+ const pendingCell = this.pending.getCell(rowHandle, colHandle);
1090
+ if (this.fwwPolicy.state === "on") {
1091
+ // If someone tried to Overwrite the cell value or first write on this cell or
1092
+ // same client tried to modify the cell or if the previous mode was LWW, then we need to still
1093
+ // overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
1094
+ if (this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)) {
1095
+ const previousValue = this.cells.getCell(rowHandle, colHandle);
1096
+ this.cells.setCell(rowHandle, colHandle, value);
1097
+ this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
1098
+ seqNum: msg.sequenceNumber,
1099
+ clientId: msg.clientId,
1100
+ });
1101
+ if (pendingCell !== undefined) {
1102
+ pendingCell.consensus = value;
1103
+ }
1104
+ if (adjustedRow.pos !== undefined && adjustedCol.pos !== undefined) {
1105
+ for (const consumer of this.consumers.values()) {
1106
+ consumer.cellsChanged(adjustedRow.pos, adjustedCol.pos, 1, 1, this);
1107
+ }
1108
+ // Check is there are any pending changes, which will be rejected. If so raise conflict.
1109
+ if (pendingCell !== undefined && pendingCell.local.length > 0) {
1110
+ // Don't reset the pending value yet, as there maybe more fww op from same client, so we want
1111
+ // to raise conflict event for that op also.
1112
+ this.emit(
1113
+ "conflict",
1114
+ row,
1115
+ col,
1116
+ value, // Current value
1117
+ previousValue, // Ignored local value
1118
+ this,
1119
+ );
1029
1120
  }
1030
- } else if (this.pending.getCell(rowHandle, colHandle) === undefined) {
1031
- // If there is a pending (unACKed) local write to the same cell, skip the current op
1032
- // since it "happened before" the pending write.
1033
- this.cells.setCell(rowHandle, colHandle, value);
1121
+ }
1122
+ }
1123
+ } else {
1124
+ if (pendingCell === undefined || pendingCell.local.length === 0) {
1125
+ // If there is a pending (unACKed) local write to the same cell, skip the current op
1126
+ // since it "happened before" the pending write.
1127
+ this.cells.setCell(rowHandle, colHandle, value);
1128
+ if (adjustedRow.pos !== undefined && adjustedCol.pos !== undefined) {
1034
1129
  for (const consumer of this.consumers.values()) {
1035
- consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
1130
+ consumer.cellsChanged(adjustedRow.pos, adjustedCol.pos, 1, 1, this);
1036
1131
  }
1037
1132
  }
1133
+ } else {
1134
+ pendingCell.consensus = value;
1038
1135
  }
1039
1136
  }
1040
1137
  }
@@ -1106,35 +1203,6 @@ export class SharedMatrix<T = any>
1106
1203
  }
1107
1204
  }
1108
1205
 
1109
- /**
1110
- * Returns true if the latest pending write to the cell indicated by the given row/col handles
1111
- * matches the given 'localSeq'.
1112
- *
1113
- * A return value of `true` indicates that there are no later local operations queued that will
1114
- * clobber the write op at the given 'localSeq'. This includes later ops that overwrite the cell
1115
- * with a different value as well as row/col removals that might recycled the given row/col handles.
1116
- */
1117
- private isLatestPendingWrite(
1118
- rowHandle: Handle,
1119
- colHandle: Handle,
1120
- localSeq: number,
1121
- ): boolean {
1122
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1123
- const pendingLocalSeq = this.pending.getCell(rowHandle, colHandle)!;
1124
-
1125
- // Note while we're awaiting the ACK for a local set, it's possible for the row/col to be
1126
- // locally removed and the row/col handles recycled. If this happens, the pendingLocalSeq will
1127
- // be 'undefined' or > 'localSeq'.
1128
- assert(
1129
- !(pendingLocalSeq < localSeq),
1130
- 0x023 /* "The 'localSeq' of pending write (if any) must be <= the localSeq of the currently processed op." */,
1131
- );
1132
-
1133
- // If this is the most recent write to the cell by the local client, the stored localSeq
1134
- // will be an exact match for the given 'localSeq'.
1135
- return pendingLocalSeq === localSeq;
1136
- }
1137
-
1138
1206
  public toString(): string {
1139
1207
  let s = `client:${
1140
1208
  this.runtime.clientId
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/matrix";
9
- export const pkgVersion = "2.41.0";
9
+ export const pkgVersion = "2.42.0";
@@ -18,7 +18,6 @@ import {
18
18
  IMergeTreeDeltaOpArgs,
19
19
  IMergeTreeMaintenanceCallbackArgs,
20
20
  ISegment,
21
- ISegmentInternal,
22
21
  MergeTreeDeltaType,
23
22
  MergeTreeMaintenanceType,
24
23
  segmentIsRemoved,
@@ -199,23 +198,50 @@ export class PermutationVector extends Client {
199
198
  }
200
199
 
201
200
  public adjustPosition(
202
- pos: number,
203
- op: Pick<ISequencedDocumentMessage, "referenceSequenceNumber" | "clientId">,
204
- ): number | undefined {
205
- const { segment, offset } = this.getContainingSegment<ISegmentInternal>(pos, {
201
+ posToAdjust: number,
202
+ op: ISequencedDocumentMessage,
203
+ ): { pos: number | undefined; handle: Handle } {
204
+ const { segment, offset } = this.getContainingSegment<PermutationSegment>(posToAdjust, {
206
205
  referenceSequenceNumber: op.referenceSequenceNumber,
207
206
  clientId: op.clientId,
208
207
  });
209
208
 
210
- // Note that until the MergeTree GCs, the segment is still reachable via `getContainingSegment()` with
211
- // a `refSeq` in the past. Prevent remote ops from accidentally allocating or using recycled handles
212
- // by checking for the presence of 'removedSeq'.
213
- if (segment === undefined || segmentIsRemoved(segment)) {
214
- return undefined;
215
- }
209
+ assert(
210
+ segment !== undefined && offset !== undefined,
211
+ 0xbac /* segment must be available for operations in the collab window */,
212
+ );
216
213
 
217
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218
- return this.getPosition(segment) + offset!;
214
+ if (segmentIsRemoved(segment)) {
215
+ // this case is tricky. the segment which the row or column data is remove
216
+ // but an op before that remove references a cell. we still want to apply
217
+ // the op, as the row/col could become active again in the case where
218
+ // the remove was local and it get's rolled back. so we allocate a handle
219
+ // for the row/col if not allocated, but don't put it in the cache
220
+ // as the cache can only contain live positions.
221
+ let handle = segment.start;
222
+ if (!isHandleValid(handle)) {
223
+ this.walkSegments(
224
+ (s) => {
225
+ const asPerm = s as PermutationSegment;
226
+ asPerm.start = handle = this.handleTable.allocate();
227
+ return true;
228
+ },
229
+ posToAdjust,
230
+ posToAdjust + 1,
231
+ /* accum: */ undefined,
232
+ /* splitRange: */ true,
233
+ op,
234
+ );
235
+ }
236
+
237
+ return { handle, pos: undefined };
238
+ } else {
239
+ const pos = this.getPosition(segment) + offset;
240
+ return {
241
+ pos,
242
+ handle: this.getAllocatedHandle(pos),
243
+ };
244
+ }
219
245
  }
220
246
 
221
247
  public handleToPosition(handle: Handle, localSeq = this.getCollabWindow().localSeq): number {
@@ -342,10 +368,12 @@ export class PermutationVector extends Client {
342
368
  case MergeTreeDeltaType.INSERT: {
343
369
  // Pass 1: Perform any internal maintenance first to avoid reentrancy.
344
370
  for (const { segment, position } of ranges) {
345
- // HACK: We need to include the allocated handle in the segment's JSON representation
346
- // for snapshots, but need to ignore the remote client's handle allocations when
347
- // processing remote ops.
348
- segment.reset();
371
+ if (opArgs.rollback !== true) {
372
+ // HACK: We need to include the allocated handle in the segment's JSON representation
373
+ // for snapshots, but need to ignore the remote client's handle allocations when
374
+ // processing remote ops.
375
+ segment.reset();
376
+ }
349
377
 
350
378
  this.handleCache.itemsChanged(
351
379
  position,
@@ -364,7 +392,6 @@ export class PermutationVector extends Client {
364
392
  }
365
393
  break;
366
394
  }
367
-
368
395
  case MergeTreeDeltaType.REMOVE: {
369
396
  // Pass 1: Perform any internal maintenance first to avoid reentrancy.
370
397
  for (const { segment, position } of ranges) {
@@ -385,7 +412,6 @@ export class PermutationVector extends Client {
385
412
  }
386
413
  break;
387
414
  }
388
-
389
415
  default: {
390
416
  throw new Error("Unhandled MergeTreeDeltaType");
391
417
  }