@fluidframework/matrix 2.0.0-internal.7.3.0 → 2.0.0-internal.7.4.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 (68) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +39 -0
  3. package/api-extractor-lint.json +13 -0
  4. package/api-extractor.json +8 -3
  5. package/api-report/matrix.api.md +19 -9
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/matrix-alpha.d.ts +71 -11
  10. package/dist/matrix-beta.d.ts +41 -134
  11. package/dist/matrix-public.d.ts +41 -134
  12. package/dist/matrix-untrimmed.d.ts +71 -11
  13. package/dist/matrix.cjs +175 -46
  14. package/dist/matrix.cjs.map +1 -1
  15. package/dist/matrix.d.ts +67 -9
  16. package/dist/matrix.d.ts.map +1 -1
  17. package/dist/ops.cjs +1 -0
  18. package/dist/ops.cjs.map +1 -1
  19. package/dist/ops.d.ts +5 -1
  20. package/dist/ops.d.ts.map +1 -1
  21. package/dist/packageVersion.cjs +1 -1
  22. package/dist/packageVersion.cjs.map +1 -1
  23. package/dist/packageVersion.d.ts +1 -1
  24. package/dist/runtime.cjs +1 -1
  25. package/dist/runtime.cjs.map +1 -1
  26. package/dist/runtime.d.ts +1 -1
  27. package/dist/types.cjs.map +1 -1
  28. package/dist/types.d.ts +2 -2
  29. package/lib/handlecache.d.ts +2 -2
  30. package/lib/handlecache.d.ts.map +1 -1
  31. package/lib/index.d.ts +3 -3
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.mjs.map +1 -1
  34. package/lib/matrix-alpha.d.ts +71 -11
  35. package/lib/matrix-beta.d.ts +41 -134
  36. package/lib/matrix-public.d.ts +41 -134
  37. package/lib/matrix-untrimmed.d.ts +71 -11
  38. package/lib/matrix.d.ts +69 -11
  39. package/lib/matrix.d.ts.map +1 -1
  40. package/lib/matrix.mjs +175 -46
  41. package/lib/matrix.mjs.map +1 -1
  42. package/lib/ops.d.ts +5 -1
  43. package/lib/ops.d.ts.map +1 -1
  44. package/lib/ops.mjs +1 -0
  45. package/lib/ops.mjs.map +1 -1
  46. package/lib/packageVersion.d.ts +1 -1
  47. package/lib/packageVersion.mjs +1 -1
  48. package/lib/packageVersion.mjs.map +1 -1
  49. package/lib/permutationvector.d.ts +3 -3
  50. package/lib/permutationvector.d.ts.map +1 -1
  51. package/lib/runtime.d.ts +1 -1
  52. package/lib/runtime.d.ts.map +1 -1
  53. package/lib/runtime.mjs +1 -1
  54. package/lib/runtime.mjs.map +1 -1
  55. package/lib/serialization.d.ts.map +1 -1
  56. package/lib/sparsearray2d.d.ts.map +1 -1
  57. package/lib/types.d.ts +2 -2
  58. package/lib/types.mjs.map +1 -1
  59. package/lib/undoprovider.d.ts +4 -4
  60. package/lib/undoprovider.d.ts.map +1 -1
  61. package/matrix.test-files.tar +0 -0
  62. package/package.json +26 -31
  63. package/src/index.ts +1 -1
  64. package/src/matrix.ts +282 -60
  65. package/src/ops.ts +5 -0
  66. package/src/packageVersion.ts +1 -1
  67. package/src/runtime.ts +1 -1
  68. package/src/types.ts +2 -2
package/lib/matrix.mjs CHANGED
@@ -25,19 +25,30 @@ import { MatrixUndoProvider } from "./undoprovider.mjs";
25
25
  * matrix data and physically stores data in Z-order to leverage CPU caches and
26
26
  * prefetching when reading in either row or column major order. (See README.md
27
27
  * for more details.)
28
- *
29
- * @public
28
+ * @alpha
30
29
  */
31
30
  export class SharedMatrix extends SharedObject {
32
31
  static getFactory() {
33
32
  return new SharedMatrixFactory();
34
33
  }
35
- constructor(runtime, id, attributes) {
34
+ /**
35
+ * Constructor for the Shared Matrix
36
+ * @param runtime - DataStore runtime.
37
+ * @param id - id of the dds
38
+ * @param attributes - channel attributes
39
+ * @param _isSetCellConflictResolutionPolicyFWW - Conflict resolution for Matrix set op is First Writer Win in case of
40
+ * race condition. Client can still overwrite values in case of no race.
41
+ */
42
+ constructor(runtime, id, attributes, _isSetCellConflictResolutionPolicyFWW) {
36
43
  super(id, runtime, attributes, "fluid_matrix_");
37
44
  this.id = id;
38
45
  this.consumers = new Set();
39
46
  this.cells = new SparseArray2D(); // Stores cell values.
40
47
  this.pending = new SparseArray2D(); // Tracks pending writes.
48
+ this.cellLastWriteTracker = new SparseArray2D(); // Tracks last writes sequence number and clientId in a cell.
49
+ this.userSwitchedSetCellPolicy = false; // Set to true when the user calls switchPolicy.
50
+ // Used to track if there is any reentrancy in setCell code.
51
+ this.reentrantCount = 0;
41
52
  // Invoked by PermutationVector to notify IMatrixConsumers of row insertion/deletions.
42
53
  this.onRowDelta = (position, removedCount, insertedCount) => {
43
54
  for (const consumer of this.consumers) {
@@ -54,14 +65,18 @@ export class SharedMatrix extends SharedObject {
54
65
  for (const rowHandle of rowHandles) {
55
66
  this.cells.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
56
67
  this.pending.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
68
+ this.cellLastWriteTracker.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
57
69
  }
58
70
  };
59
71
  this.onColHandlesRecycled = (colHandles) => {
60
72
  for (const colHandle of colHandles) {
61
73
  this.cells.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
62
74
  this.pending.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
75
+ this.cellLastWriteTracker.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
63
76
  }
64
77
  };
78
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber =
79
+ _isSetCellConflictResolutionPolicyFWW === true ? 0 : -1;
65
80
  this.rows = new PermutationVector("rows" /* SnapshotPath.rows */, this.logger, runtime, this.onRowDelta, this.onRowHandlesRecycled);
66
81
  this.cols = new PermutationVector("cols" /* SnapshotPath.cols */, this.logger, runtime, this.onColDelta, this.onColHandlesRecycled);
67
82
  }
@@ -101,6 +116,9 @@ export class SharedMatrix extends SharedObject {
101
116
  get colCount() {
102
117
  return this.cols.getLength();
103
118
  }
119
+ isSetCellConflictResolutionPolicyFWW() {
120
+ return this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1 || this.userSwitchedSetCellPolicy;
121
+ }
104
122
  getCell(row, col) {
105
123
  // Perf: When possible, bounds checking is performed inside the implementation for
106
124
  // 'getHandle()' so that it can be elided in the case of a cache hit. This
@@ -127,10 +145,6 @@ export class SharedMatrix extends SharedObject {
127
145
  setCell(row, col, value) {
128
146
  assert(0 <= row && row < this.rowCount && 0 <= col && col < this.colCount, 0x01a /* "Trying to set out-of-bounds cell!" */);
129
147
  this.setCellCore(row, col, value);
130
- // Avoid reentrancy by raising change notifications after the op is queued.
131
- for (const consumer of this.consumers.values()) {
132
- consumer.cellsChanged(row, col, 1, 1, this);
133
- }
134
148
  }
135
149
  setCells(rowStart, colStart, colCount, values) {
136
150
  const rowCount = Math.ceil(values.length / colCount);
@@ -151,23 +165,25 @@ export class SharedMatrix extends SharedObject {
151
165
  r++;
152
166
  }
153
167
  }
154
- // Avoid reentrancy by raising change notifications after the op is queued.
155
- for (const consumer of this.consumers.values()) {
156
- consumer.cellsChanged(rowStart, colStart, rowCount, colCount, this);
157
- }
158
168
  }
159
169
  setCellCore(row, col, value, rowHandle = this.rows.getAllocatedHandle(row), colHandle = this.cols.getAllocatedHandle(col)) {
160
- if (this.undo !== undefined) {
161
- let oldValue = this.cells.getCell(rowHandle, colHandle);
162
- if (oldValue === null) {
163
- oldValue = undefined;
170
+ this.protectAgainstReentrancy(() => {
171
+ if (this.undo !== undefined) {
172
+ let oldValue = this.cells.getCell(rowHandle, colHandle);
173
+ if (oldValue === null) {
174
+ oldValue = undefined;
175
+ }
176
+ this.undo.cellSet(rowHandle, colHandle, oldValue);
164
177
  }
165
- this.undo.cellSet(rowHandle, colHandle, oldValue);
166
- }
167
- this.cells.setCell(rowHandle, colHandle, value);
168
- if (this.isAttached()) {
169
- this.sendSetCellOp(row, col, value, rowHandle, colHandle);
170
- }
178
+ this.cells.setCell(rowHandle, colHandle, value);
179
+ if (this.isAttached()) {
180
+ this.sendSetCellOp(row, col, value, rowHandle, colHandle);
181
+ }
182
+ // Avoid reentrancy by raising change notifications after the op is queued.
183
+ for (const consumer of this.consumers.values()) {
184
+ consumer.cellsChanged(row, col, 1, 1, this);
185
+ }
186
+ });
171
187
  }
172
188
  sendSetCellOp(row, col, value, rowHandle, colHandle, localSeq = this.nextLocalSeq(), rowsRefSeq = this.rows.getCollabWindow().currentSeq, colsRefSeq = this.cols.getCollabWindow().currentSeq) {
173
189
  assert(this.isAttached(), 0x1e2 /* "Caller must ensure 'isAttached()' before calling 'sendSetCellOp'." */);
@@ -176,6 +192,7 @@ export class SharedMatrix extends SharedObject {
176
192
  row,
177
193
  col,
178
194
  value,
195
+ fwwMode: this.userSwitchedSetCellPolicy || this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1,
179
196
  };
180
197
  const metadata = {
181
198
  rowHandle,
@@ -183,10 +200,26 @@ export class SharedMatrix extends SharedObject {
183
200
  localSeq,
184
201
  rowsRefSeq,
185
202
  colsRefSeq,
203
+ referenceSeqNumber: this.runtime.deltaManager.lastSequenceNumber,
186
204
  };
187
205
  this.submitLocalMessage(op, metadata);
188
206
  this.pending.setCell(rowHandle, colHandle, localSeq);
189
207
  }
208
+ /**
209
+ * This makes sure that the code inside the callback is not reentrant. We need to do that because we raise notifications
210
+ * to the consumers telling about these changes and they can try to change the matrix while listening to those notifications
211
+ * which can make the shared matrix to be in bad state. For example, we are raising notification for a setCell changes and
212
+ * a consumer tries to delete that row/col on receiving that notification which can lead to this matrix trying to setCell in
213
+ * a deleted row/col.
214
+ * @param callback - code that needs to protected against reentrancy.
215
+ */
216
+ protectAgainstReentrancy(callback) {
217
+ assert(this.reentrantCount === 0, 0x85d /* reentrant code */);
218
+ this.reentrantCount++;
219
+ callback();
220
+ this.reentrantCount--;
221
+ assert(this.reentrantCount === 0, 0x85e /* reentrant code on exit */);
222
+ }
190
223
  submitVectorMessage(currentVector, oppositeVector, dimension, message) {
191
224
  // Ideally, we would have a single 'localSeq' counter that is shared between both PermutationVectors
192
225
  // and the SharedMatrix's cell data. Instead, we externally advance each MergeTree's 'localSeq' counter
@@ -209,21 +242,21 @@ export class SharedMatrix extends SharedObject {
209
242
  this.submitVectorMessage(this.cols, this.rows, "cols" /* SnapshotPath.cols */, message);
210
243
  }
211
244
  insertCols(colStart, count) {
212
- this.submitColMessage(this.cols.insert(colStart, count));
245
+ this.protectAgainstReentrancy(() => this.submitColMessage(this.cols.insert(colStart, count)));
213
246
  }
214
247
  removeCols(colStart, count) {
215
- this.submitColMessage(this.cols.remove(colStart, count));
248
+ this.protectAgainstReentrancy(() => this.submitColMessage(this.cols.remove(colStart, count)));
216
249
  }
217
250
  submitRowMessage(message) {
218
251
  this.submitVectorMessage(this.rows, this.cols, "rows" /* SnapshotPath.rows */, message);
219
252
  }
220
253
  insertRows(rowStart, count) {
221
- this.submitRowMessage(this.rows.insert(rowStart, count));
254
+ this.protectAgainstReentrancy(() => this.submitRowMessage(this.rows.insert(rowStart, count)));
222
255
  }
223
256
  removeRows(rowStart, count) {
224
- this.submitRowMessage(this.rows.remove(rowStart, count));
257
+ this.protectAgainstReentrancy(() => this.submitRowMessage(this.rows.remove(rowStart, count)));
225
258
  }
226
- /** @internal */ _undoRemoveRows(rowStart, spec) {
259
+ /***/ _undoRemoveRows(rowStart, spec) {
227
260
  const { op, inserted } = reinsertSegmentIntoVector(this.rows, rowStart, spec);
228
261
  this.submitRowMessage(op);
229
262
  // Generate setCell ops for each populated cell in the reinserted rows.
@@ -243,7 +276,7 @@ export class SharedMatrix extends SharedObject {
243
276
  consumer.cellsChanged(rowStart, /* colStart: */ 0, rowCount, this.colCount, this);
244
277
  }
245
278
  }
246
- /** @internal */ _undoRemoveCols(colStart, spec) {
279
+ /***/ _undoRemoveCols(colStart, spec) {
247
280
  const { op, inserted } = reinsertSegmentIntoVector(this.cols, colStart, spec);
248
281
  this.submitColMessage(op);
249
282
  // Generate setCell ops for each populated cell in the reinserted cols.
@@ -267,7 +300,16 @@ export class SharedMatrix extends SharedObject {
267
300
  const builder = new SummaryTreeBuilder();
268
301
  builder.addWithStats("rows" /* SnapshotPath.rows */, this.rows.summarize(this.runtime, this.handle, serializer));
269
302
  builder.addWithStats("cols" /* SnapshotPath.cols */, this.cols.summarize(this.runtime, this.handle, serializer));
270
- builder.addBlob("cells" /* SnapshotPath.cells */, serializer.stringify([this.cells.snapshot(), this.pending.snapshot()], this.handle));
303
+ const artifactsToSummarize = [
304
+ this.cells.snapshot(),
305
+ this.pending.snapshot(),
306
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber,
307
+ ];
308
+ // Only need to store it in the snapshot if we have switched the policy already.
309
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1) {
310
+ artifactsToSummarize.push(this.cellLastWriteTracker.snapshot());
311
+ }
312
+ builder.addBlob("cells" /* SnapshotPath.cells */, serializer.stringify(artifactsToSummarize, this.handle));
271
313
  return builder.getSummaryTree();
272
314
  }
273
315
  /**
@@ -336,16 +378,27 @@ export class SharedMatrix extends SharedObject {
336
378
  default: {
337
379
  assert(content.type === MatrixOp.set, 0x020 /* "Unknown SharedMatrix 'op' type." */);
338
380
  const setOp = content;
339
- const { rowHandle, colHandle, localSeq, rowsRefSeq, colsRefSeq } = localOpMetadata;
340
- // If there are more pending local writes to the same row/col handle, it is important
341
- // to skip resubmitting this op since it is possible the row/col handle has been recycled
342
- // and now refers to a different position than when this op was originally submitted.
343
- if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
344
- const row = this.rebasePosition(this.rows, setOp.row, rowsRefSeq, localSeq);
345
- const col = this.rebasePosition(this.cols, setOp.col, colsRefSeq, localSeq);
346
- if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
381
+ const { rowHandle, colHandle, localSeq, rowsRefSeq, colsRefSeq, referenceSeqNumber, } = localOpMetadata;
382
+ // If after rebasing the op, we get a valid row/col number, that means the row/col
383
+ // handles have not been recycled and we can safely use them.
384
+ const row = this.rebasePosition(this.rows, setOp.row, rowsRefSeq, localSeq);
385
+ const col = this.rebasePosition(this.cols, setOp.col, colsRefSeq, localSeq);
386
+ if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
387
+ const lastCellModificationDetails = this.cellLastWriteTracker.getCell(rowHandle, colHandle);
388
+ // If the mode is LWW, then send the op.
389
+ // Otherwise if the current mode is FWW and if we generated this op, after seeing the
390
+ // last set op, or it is the first set op for the cell, then regenerate the op,
391
+ // otherwise raise conflict. We want to check the current mode here and not that
392
+ // whether op was made in FWW or not.
393
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 ||
394
+ lastCellModificationDetails === undefined ||
395
+ referenceSeqNumber >= lastCellModificationDetails.seqNum) {
347
396
  this.sendSetCellOp(row, col, setOp.value, rowHandle, colHandle, localSeq, rowsRefSeq, colsRefSeq);
348
397
  }
398
+ else if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
399
+ // Clear the pending changes if any as we are not sending the op.
400
+ this.pending.setCell(rowHandle, colHandle, undefined);
401
+ }
349
402
  }
350
403
  break;
351
404
  }
@@ -359,14 +412,32 @@ export class SharedMatrix extends SharedObject {
359
412
  try {
360
413
  await this.rows.load(this.runtime, new ObjectStoragePartition(storage, "rows" /* SnapshotPath.rows */), this.serializer);
361
414
  await this.cols.load(this.runtime, new ObjectStoragePartition(storage, "cols" /* SnapshotPath.cols */), this.serializer);
362
- const [cellData, pendingCliSeqData] = await deserializeBlob(storage, "cells" /* SnapshotPath.cells */, this.serializer);
415
+ const [cellData, _pendingCliSeqData, setCellLwwToFwwPolicySwitchOpSeqNumber, cellLastWriteTracker,] = await deserializeBlob(storage, "cells" /* SnapshotPath.cells */, this.serializer);
363
416
  this.cells = SparseArray2D.load(cellData);
364
- this.pending = SparseArray2D.load(pendingCliSeqData);
417
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber =
418
+ setCellLwwToFwwPolicySwitchOpSeqNumber ?? -1;
419
+ if (cellLastWriteTracker !== undefined) {
420
+ this.cellLastWriteTracker = SparseArray2D.load(cellLastWriteTracker);
421
+ }
365
422
  }
366
423
  catch (error) {
367
424
  this.logger.sendErrorEvent({ eventName: "MatrixLoadFailed" }, error);
368
425
  }
369
426
  }
427
+ /**
428
+ * Tells whether the setCell op should be applied or not based on First Write Win policy. It assumes
429
+ * we are in FWW mode.
430
+ */
431
+ shouldSetCellBasedOnFWW(rowHandle, colHandle, message) {
432
+ assert(this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1, 0x85f /* should be in Fww mode when calling this method */);
433
+ assert(message.clientId !== null, 0x860 /* clientId should not be null */);
434
+ const lastCellModificationDetails = this.cellLastWriteTracker.getCell(rowHandle, colHandle);
435
+ // If someone tried to Overwrite the cell value or first write on this cell or
436
+ // same client tried to modify the cell.
437
+ return (lastCellModificationDetails === undefined ||
438
+ lastCellModificationDetails.clientId === message.clientId ||
439
+ message.referenceSequenceNumber >= lastCellModificationDetails.seqNum);
440
+ }
370
441
  processCore(rawMessage, local, localOpMetadata) {
371
442
  const msg = parseHandles(rawMessage, this.serializer);
372
443
  const contents = msg.contents;
@@ -379,13 +450,28 @@ export class SharedMatrix extends SharedObject {
379
450
  break;
380
451
  default: {
381
452
  assert(contents.type === MatrixOp.set, 0x021 /* "SharedMatrix message contents have unexpected type!" */);
382
- const { row, col } = contents;
453
+ const { row, col, value, fwwMode } = contents;
454
+ const isPreviousSetCellPolicyModeFWW = this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1;
455
+ // If this is the first op notifying us of the policy change, then set the policy change seq number.
456
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 && fwwMode === true) {
457
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber = rawMessage.sequenceNumber;
458
+ }
459
+ assert(rawMessage.clientId !== null, 0x861 /* clientId should not be null!! */);
383
460
  if (local) {
384
461
  // We are receiving the ACK for a local pending set operation.
385
462
  const { rowHandle, colHandle, localSeq } = localOpMetadata;
386
- // If this is the most recent write to the cell by the local client, remove our
387
- // entry from 'pendingCliSeqs' to resume allowing remote writes.
388
- if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
463
+ const isLatestPendingOp = this.isLatestPendingWrite(rowHandle, colHandle, localSeq);
464
+ // If policy is switched and cell should be modified too based on policy, then update the tracker.
465
+ // If policy is not switched, then also update the tracker in case it is the latest.
466
+ if ((this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1 &&
467
+ this.shouldSetCellBasedOnFWW(rowHandle, colHandle, rawMessage)) ||
468
+ (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 && isLatestPendingOp)) {
469
+ this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
470
+ seqNum: rawMessage.sequenceNumber,
471
+ clientId: rawMessage.clientId,
472
+ });
473
+ }
474
+ if (isLatestPendingOp) {
389
475
  this.pending.setCell(rowHandle, colHandle, undefined);
390
476
  }
391
477
  }
@@ -397,11 +483,39 @@ export class SharedMatrix extends SharedObject {
397
483
  const rowHandle = this.rows.getAllocatedHandle(adjustedRow);
398
484
  const colHandle = this.cols.getAllocatedHandle(adjustedCol);
399
485
  assert(isHandleValid(rowHandle) && isHandleValid(colHandle), 0x022 /* "SharedMatrix row and/or col handles are invalid!" */);
400
- // If there is a pending (unACKed) local write to the same cell, skip the current op
401
- // since it "happened before" the pending write.
402
- if (this.pending.getCell(rowHandle, colHandle) === undefined) {
403
- const { value } = contents;
486
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1) {
487
+ // If someone tried to Overwrite the cell value or first write on this cell or
488
+ // same client tried to modify the cell or if the previous mode was LWW, then we need to still
489
+ // overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
490
+ if (!isPreviousSetCellPolicyModeFWW ||
491
+ this.shouldSetCellBasedOnFWW(rowHandle, colHandle, rawMessage)) {
492
+ const previousValue = this.cells.getCell(rowHandle, colHandle);
493
+ this.cells.setCell(rowHandle, colHandle, value);
494
+ this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
495
+ seqNum: rawMessage.sequenceNumber,
496
+ clientId: rawMessage.clientId,
497
+ });
498
+ for (const consumer of this.consumers.values()) {
499
+ consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
500
+ }
501
+ // Check is there are any pending changes, which will be rejected. If so raise conflict.
502
+ if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
503
+ // Don't reset the pending value yet, as there maybe more fww op from same client, so we want
504
+ // to raise conflict event for that op also.
505
+ this.emit("conflict", row, col, value, // Current value
506
+ previousValue, // Ignored local value
507
+ this);
508
+ }
509
+ }
510
+ }
511
+ else if (this.pending.getCell(rowHandle, colHandle) === undefined) {
512
+ // If there is a pending (unACKed) local write to the same cell, skip the current op
513
+ // since it "happened before" the pending write.
404
514
  this.cells.setCell(rowHandle, colHandle, value);
515
+ this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
516
+ seqNum: rawMessage.sequenceNumber,
517
+ clientId: rawMessage.clientId,
518
+ });
405
519
  for (const consumer of this.consumers.values()) {
406
520
  consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
407
521
  }
@@ -412,6 +526,20 @@ export class SharedMatrix extends SharedObject {
412
526
  }
413
527
  }
414
528
  }
529
+ /**
530
+ * Api to switch Set Op policy from Last Writer Win to First Writer Win. It only switches from LWW to FWW
531
+ * and not from FWW to LWW. The next SetOp which is sent will communicate this policy to other clients.
532
+ */
533
+ switchSetCellPolicy() {
534
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1) {
535
+ if (this.isAttached()) {
536
+ this.userSwitchedSetCellPolicy = true;
537
+ }
538
+ else {
539
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber = 0;
540
+ }
541
+ }
542
+ }
415
543
  /**
416
544
  * Returns true if the latest pending write to the cell indicated by the given row/col handles
417
545
  * matches the given 'localSeq'.
@@ -484,6 +612,7 @@ export class SharedMatrix extends SharedObject {
484
612
  localSeq,
485
613
  rowsRefSeq,
486
614
  colsRefSeq,
615
+ referenceSeqNumber: this.runtime.deltaManager.lastSequenceNumber,
487
616
  };
488
617
  this.pending.setCell(rowHandle, colHandle, localSeq);
489
618
  return metadata;