@fluidframework/matrix 2.0.0-internal.7.3.0 → 2.0.0-internal.7.4.1

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/src/matrix.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  /* eslint-disable import/no-deprecated */
7
7
 
8
8
  import { assert } from "@fluidframework/core-utils";
9
+ import { IEventThisPlaceHolder } from "@fluidframework/core-interfaces";
9
10
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
10
11
  import {
11
12
  IFluidDataStoreRuntime,
@@ -15,6 +16,7 @@ import {
15
16
  } from "@fluidframework/datastore-definitions";
16
17
  import {
17
18
  IFluidSerializer,
19
+ ISharedObjectEvents,
18
20
  makeHandlesSerializable,
19
21
  parseHandles,
20
22
  SharedObject,
@@ -51,6 +53,7 @@ interface ISetOp<T> {
51
53
  row: number;
52
54
  col: number;
53
55
  value: MatrixItem<T>;
56
+ fwwMode?: boolean;
54
57
  }
55
58
 
56
59
  interface ISetOpMetadata {
@@ -59,12 +62,57 @@ interface ISetOpMetadata {
59
62
  localSeq: number;
60
63
  rowsRefSeq: number;
61
64
  colsRefSeq: number;
65
+ referenceSeqNumber: number;
66
+ }
67
+
68
+ /**
69
+ * Events emitted by Shared Matrix.
70
+ * @alpha
71
+ */
72
+ export interface ISharedMatrixEvents<T> extends ISharedObjectEvents {
73
+ /**
74
+ * This event is only emitted when the SetCell Resolution Policy is First Write Win(FWW).
75
+ * This is emitted when two clients race and send changes without observing each other changes,
76
+ * the changes that gets sequenced last would be rejected, and only client who's changes rejected
77
+ * would be notified via this event, with expectation that it will merge its changes back by
78
+ * accounting new information (state from winner of the race).
79
+ *
80
+ * @remarks Listener parameters:
81
+ *
82
+ * - `row` - Row number at which conflict happened.
83
+ *
84
+ * - `col` - Col number at which conflict happened.
85
+ *
86
+ * - `currentValue` - The current value of the cell.
87
+ *
88
+ * - `conflictingValue` - The value that this client tried to set in the cell and got ignored due to conflict.
89
+ *
90
+ * - `target` - The {@link SharedMatrix} itself.
91
+ */
92
+ (
93
+ event: "conflict",
94
+ listener: (
95
+ row: number,
96
+ col: number,
97
+ currentValue: MatrixItem<T>,
98
+ conflictingValue: MatrixItem<T>,
99
+ target: IEventThisPlaceHolder,
100
+ ) => void,
101
+ );
102
+ }
103
+
104
+ /**
105
+ * This represents the item which is used to track the client which modified the cell last.
106
+ */
107
+ interface CellLastWriteTrackerItem {
108
+ seqNum: number; // Seq number of op which last modified this cell
109
+ clientId: string; // clientId of the client which last modified this cell
62
110
  }
63
111
 
64
112
  /**
65
113
  * A matrix cell value may be undefined (indicating an empty cell) or any serializable type,
66
114
  * excluding null. (However, nulls may be embedded inside objects and arrays.)
67
- * @public
115
+ * @alpha
68
116
  */
69
117
  // eslint-disable-next-line @rushstack/no-new-null -- Using 'null' to disallow 'null'.
70
118
  export type MatrixItem<T> = Serializable<Exclude<T, null>> | undefined;
@@ -80,11 +128,10 @@ export type MatrixItem<T> = Serializable<Exclude<T, null>> | undefined;
80
128
  * matrix data and physically stores data in Z-order to leverage CPU caches and
81
129
  * prefetching when reading in either row or column major order. (See README.md
82
130
  * for more details.)
83
- *
84
- * @public
131
+ * @alpha
85
132
  */
86
133
  export class SharedMatrix<T = any>
87
- extends SharedObject
134
+ extends SharedObject<ISharedMatrixEvents<T>>
88
135
  implements
89
136
  IMatrixProducer<MatrixItem<T>>,
90
137
  IMatrixReader<MatrixItem<T>>,
@@ -100,15 +147,33 @@ export class SharedMatrix<T = any>
100
147
  private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
101
148
 
102
149
  private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
103
- private pending = new SparseArray2D<number>(); // Tracks pending writes.
150
+ private readonly pending = new SparseArray2D<number>(); // Tracks pending writes.
151
+ private cellLastWriteTracker = new SparseArray2D<CellLastWriteTrackerItem>(); // Tracks last writes sequence number and clientId in a cell.
152
+ // Tracks the seq number of Op at which policy switch happens from Last Write Win to First Write Win.
153
+ private setCellLwwToFwwPolicySwitchOpSeqNumber: number;
154
+ private userSwitchedSetCellPolicy = false; // Set to true when the user calls switchPolicy.
155
+
156
+ // Used to track if there is any reentrancy in setCell code.
157
+ private reentrantCount: number = 0;
104
158
 
159
+ /**
160
+ * Constructor for the Shared Matrix
161
+ * @param runtime - DataStore runtime.
162
+ * @param id - id of the dds
163
+ * @param attributes - channel attributes
164
+ * @param _isSetCellConflictResolutionPolicyFWW - Conflict resolution for Matrix set op is First Writer Win in case of
165
+ * race condition. Client can still overwrite values in case of no race.
166
+ */
105
167
  constructor(
106
168
  runtime: IFluidDataStoreRuntime,
107
169
  public id: string,
108
170
  attributes: IChannelAttributes,
171
+ _isSetCellConflictResolutionPolicyFWW?: boolean,
109
172
  ) {
110
173
  super(id, runtime, attributes, "fluid_matrix_");
111
174
 
175
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber =
176
+ _isSetCellConflictResolutionPolicyFWW === true ? 0 : -1;
112
177
  this.rows = new PermutationVector(
113
178
  SnapshotPath.rows,
114
179
  this.logger,
@@ -178,6 +243,10 @@ export class SharedMatrix<T = any>
178
243
  return this.cols.getLength();
179
244
  }
180
245
 
246
+ public isSetCellConflictResolutionPolicyFWW() {
247
+ return this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1 || this.userSwitchedSetCellPolicy;
248
+ }
249
+
181
250
  public getCell(row: number, col: number): MatrixItem<T> {
182
251
  // Perf: When possible, bounds checking is performed inside the implementation for
183
252
  // 'getHandle()' so that it can be elided in the case of a cache hit. This
@@ -212,11 +281,6 @@ export class SharedMatrix<T = any>
212
281
  );
213
282
 
214
283
  this.setCellCore(row, col, value);
215
-
216
- // Avoid reentrancy by raising change notifications after the op is queued.
217
- for (const consumer of this.consumers.values()) {
218
- consumer.cellsChanged(row, col, 1, 1, this);
219
- }
220
284
  }
221
285
 
222
286
  public setCells(
@@ -250,11 +314,6 @@ export class SharedMatrix<T = any>
250
314
  r++;
251
315
  }
252
316
  }
253
-
254
- // Avoid reentrancy by raising change notifications after the op is queued.
255
- for (const consumer of this.consumers.values()) {
256
- consumer.cellsChanged(rowStart, colStart, rowCount, colCount, this);
257
- }
258
317
  }
259
318
 
260
319
  private setCellCore(
@@ -264,20 +323,27 @@ export class SharedMatrix<T = any>
264
323
  rowHandle = this.rows.getAllocatedHandle(row),
265
324
  colHandle = this.cols.getAllocatedHandle(col),
266
325
  ) {
267
- if (this.undo !== undefined) {
268
- let oldValue = this.cells.getCell(rowHandle, colHandle);
269
- if (oldValue === null) {
270
- oldValue = undefined;
326
+ this.protectAgainstReentrancy(() => {
327
+ if (this.undo !== undefined) {
328
+ let oldValue = this.cells.getCell(rowHandle, colHandle);
329
+ if (oldValue === null) {
330
+ oldValue = undefined;
331
+ }
332
+
333
+ this.undo.cellSet(rowHandle, colHandle, oldValue);
271
334
  }
272
335
 
273
- this.undo.cellSet(rowHandle, colHandle, oldValue);
274
- }
336
+ this.cells.setCell(rowHandle, colHandle, value);
275
337
 
276
- this.cells.setCell(rowHandle, colHandle, value);
338
+ if (this.isAttached()) {
339
+ this.sendSetCellOp(row, col, value, rowHandle, colHandle);
340
+ }
277
341
 
278
- if (this.isAttached()) {
279
- this.sendSetCellOp(row, col, value, rowHandle, colHandle);
280
- }
342
+ // Avoid reentrancy by raising change notifications after the op is queued.
343
+ for (const consumer of this.consumers.values()) {
344
+ consumer.cellsChanged(row, col, 1, 1, this);
345
+ }
346
+ });
281
347
  }
282
348
 
283
349
  private sendSetCellOp(
@@ -300,6 +366,8 @@ export class SharedMatrix<T = any>
300
366
  row,
301
367
  col,
302
368
  value,
369
+ fwwMode:
370
+ this.userSwitchedSetCellPolicy || this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1,
303
371
  };
304
372
 
305
373
  const metadata: ISetOpMetadata = {
@@ -308,12 +376,29 @@ export class SharedMatrix<T = any>
308
376
  localSeq,
309
377
  rowsRefSeq,
310
378
  colsRefSeq,
379
+ referenceSeqNumber: this.runtime.deltaManager.lastSequenceNumber,
311
380
  };
312
381
 
313
382
  this.submitLocalMessage(op, metadata);
314
383
  this.pending.setCell(rowHandle, colHandle, localSeq);
315
384
  }
316
385
 
386
+ /**
387
+ * This makes sure that the code inside the callback is not reentrant. We need to do that because we raise notifications
388
+ * to the consumers telling about these changes and they can try to change the matrix while listening to those notifications
389
+ * which can make the shared matrix to be in bad state. For example, we are raising notification for a setCell changes and
390
+ * a consumer tries to delete that row/col on receiving that notification which can lead to this matrix trying to setCell in
391
+ * a deleted row/col.
392
+ * @param callback - code that needs to protected against reentrancy.
393
+ */
394
+ private protectAgainstReentrancy(callback: () => void) {
395
+ assert(this.reentrantCount === 0, 0x85d /* reentrant code */);
396
+ this.reentrantCount++;
397
+ callback();
398
+ this.reentrantCount--;
399
+ assert(this.reentrantCount === 0, 0x85e /* reentrant code on exit */);
400
+ }
401
+
317
402
  private submitVectorMessage(
318
403
  currentVector: PermutationVector,
319
404
  oppositeVector: PermutationVector,
@@ -355,11 +440,15 @@ export class SharedMatrix<T = any>
355
440
  }
356
441
 
357
442
  public insertCols(colStart: number, count: number) {
358
- this.submitColMessage(this.cols.insert(colStart, count));
443
+ this.protectAgainstReentrancy(() =>
444
+ this.submitColMessage(this.cols.insert(colStart, count)),
445
+ );
359
446
  }
360
447
 
361
448
  public removeCols(colStart: number, count: number) {
362
- this.submitColMessage(this.cols.remove(colStart, count));
449
+ this.protectAgainstReentrancy(() =>
450
+ this.submitColMessage(this.cols.remove(colStart, count)),
451
+ );
363
452
  }
364
453
 
365
454
  private submitRowMessage(message: any) {
@@ -367,14 +456,18 @@ export class SharedMatrix<T = any>
367
456
  }
368
457
 
369
458
  public insertRows(rowStart: number, count: number) {
370
- this.submitRowMessage(this.rows.insert(rowStart, count));
459
+ this.protectAgainstReentrancy(() =>
460
+ this.submitRowMessage(this.rows.insert(rowStart, count)),
461
+ );
371
462
  }
372
463
 
373
464
  public removeRows(rowStart: number, count: number) {
374
- this.submitRowMessage(this.rows.remove(rowStart, count));
465
+ this.protectAgainstReentrancy(() =>
466
+ this.submitRowMessage(this.rows.remove(rowStart, count)),
467
+ );
375
468
  }
376
469
 
377
- /** @internal */ public _undoRemoveRows(rowStart: number, spec: IJSONSegment) {
470
+ /***/ public _undoRemoveRows(rowStart: number, spec: IJSONSegment) {
378
471
  const { op, inserted } = reinsertSegmentIntoVector(this.rows, rowStart, spec);
379
472
  this.submitRowMessage(op);
380
473
 
@@ -397,7 +490,7 @@ export class SharedMatrix<T = any>
397
490
  }
398
491
  }
399
492
 
400
- /** @internal */ public _undoRemoveCols(colStart: number, spec: IJSONSegment) {
493
+ /***/ public _undoRemoveCols(colStart: number, spec: IJSONSegment) {
401
494
  const { op, inserted } = reinsertSegmentIntoVector(this.cols, colStart, spec);
402
495
  this.submitColMessage(op);
403
496
 
@@ -430,9 +523,19 @@ export class SharedMatrix<T = any>
430
523
  SnapshotPath.cols,
431
524
  this.cols.summarize(this.runtime, this.handle, serializer),
432
525
  );
526
+ const artifactsToSummarize = [
527
+ this.cells.snapshot(),
528
+ this.pending.snapshot(),
529
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber,
530
+ ];
531
+
532
+ // Only need to store it in the snapshot if we have switched the policy already.
533
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1) {
534
+ artifactsToSummarize.push(this.cellLastWriteTracker.snapshot());
535
+ }
433
536
  builder.addBlob(
434
537
  SnapshotPath.cells,
435
- serializer.stringify([this.cells.snapshot(), this.pending.snapshot()], this.handle),
538
+ serializer.stringify(artifactsToSummarize, this.handle),
436
539
  );
437
540
  return builder.getSummaryTree();
438
541
  }
@@ -549,17 +652,34 @@ export class SharedMatrix<T = any>
549
652
  );
550
653
 
551
654
  const setOp = content as ISetOp<T>;
552
- const { rowHandle, colHandle, localSeq, rowsRefSeq, colsRefSeq } =
553
- localOpMetadata as ISetOpMetadata;
554
-
555
- // If there are more pending local writes to the same row/col handle, it is important
556
- // to skip resubmitting this op since it is possible the row/col handle has been recycled
557
- // and now refers to a different position than when this op was originally submitted.
558
- if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
559
- const row = this.rebasePosition(this.rows, setOp.row, rowsRefSeq, localSeq);
560
- const col = this.rebasePosition(this.cols, setOp.col, colsRefSeq, localSeq);
561
-
562
- if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
655
+ const {
656
+ rowHandle,
657
+ colHandle,
658
+ localSeq,
659
+ rowsRefSeq,
660
+ colsRefSeq,
661
+ referenceSeqNumber,
662
+ } = localOpMetadata as ISetOpMetadata;
663
+
664
+ // If after rebasing the op, we get a valid row/col number, that means the row/col
665
+ // handles have not been recycled and we can safely use them.
666
+ const row = this.rebasePosition(this.rows, setOp.row, rowsRefSeq, localSeq);
667
+ const col = this.rebasePosition(this.cols, setOp.col, colsRefSeq, localSeq);
668
+ if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
669
+ const lastCellModificationDetails = this.cellLastWriteTracker.getCell(
670
+ rowHandle,
671
+ colHandle,
672
+ );
673
+ // If the mode is LWW, then send the op.
674
+ // Otherwise if the current mode is FWW and if we generated this op, after seeing the
675
+ // last set op, or it is the first set op for the cell, then regenerate the op,
676
+ // otherwise raise conflict. We want to check the current mode here and not that
677
+ // whether op was made in FWW or not.
678
+ if (
679
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 ||
680
+ lastCellModificationDetails === undefined ||
681
+ referenceSeqNumber >= lastCellModificationDetails.seqNum
682
+ ) {
563
683
  this.sendSetCellOp(
564
684
  row,
565
685
  col,
@@ -570,6 +690,9 @@ export class SharedMatrix<T = any>
570
690
  rowsRefSeq,
571
691
  colsRefSeq,
572
692
  );
693
+ } else if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
694
+ // Clear the pending changes if any as we are not sending the op.
695
+ this.pending.setCell(rowHandle, colHandle, undefined);
573
696
  }
574
697
  }
575
698
  break;
@@ -594,19 +717,48 @@ export class SharedMatrix<T = any>
594
717
  new ObjectStoragePartition(storage, SnapshotPath.cols),
595
718
  this.serializer,
596
719
  );
597
- const [cellData, pendingCliSeqData] = await deserializeBlob(
598
- storage,
599
- SnapshotPath.cells,
600
- this.serializer,
601
- );
720
+ const [
721
+ cellData,
722
+ _pendingCliSeqData,
723
+ setCellLwwToFwwPolicySwitchOpSeqNumber,
724
+ cellLastWriteTracker,
725
+ ] = await deserializeBlob(storage, SnapshotPath.cells, this.serializer);
602
726
 
603
727
  this.cells = SparseArray2D.load(cellData);
604
- this.pending = SparseArray2D.load(pendingCliSeqData);
728
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber =
729
+ setCellLwwToFwwPolicySwitchOpSeqNumber ?? -1;
730
+ if (cellLastWriteTracker !== undefined) {
731
+ this.cellLastWriteTracker = SparseArray2D.load(cellLastWriteTracker);
732
+ }
605
733
  } catch (error) {
606
734
  this.logger.sendErrorEvent({ eventName: "MatrixLoadFailed" }, error);
607
735
  }
608
736
  }
609
737
 
738
+ /**
739
+ * Tells whether the setCell op should be applied or not based on First Write Win policy. It assumes
740
+ * we are in FWW mode.
741
+ */
742
+ private shouldSetCellBasedOnFWW(
743
+ rowHandle: Handle,
744
+ colHandle: Handle,
745
+ message: ISequencedDocumentMessage,
746
+ ) {
747
+ assert(
748
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1,
749
+ 0x85f /* should be in Fww mode when calling this method */,
750
+ );
751
+ assert(message.clientId !== null, 0x860 /* clientId should not be null */);
752
+ const lastCellModificationDetails = this.cellLastWriteTracker.getCell(rowHandle, colHandle);
753
+ // If someone tried to Overwrite the cell value or first write on this cell or
754
+ // same client tried to modify the cell.
755
+ return (
756
+ lastCellModificationDetails === undefined ||
757
+ lastCellModificationDetails.clientId === message.clientId ||
758
+ message.referenceSequenceNumber >= lastCellModificationDetails.seqNum
759
+ );
760
+ }
761
+
610
762
  protected processCore(
611
763
  rawMessage: ISequencedDocumentMessage,
612
764
  local: boolean,
@@ -629,20 +781,41 @@ export class SharedMatrix<T = any>
629
781
  0x021 /* "SharedMatrix message contents have unexpected type!" */,
630
782
  );
631
783
 
632
- const { row, col } = contents;
784
+ const { row, col, value, fwwMode } = contents;
785
+ const isPreviousSetCellPolicyModeFWW =
786
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1;
787
+ // If this is the first op notifying us of the policy change, then set the policy change seq number.
788
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 && fwwMode === true) {
789
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber = rawMessage.sequenceNumber;
790
+ }
633
791
 
792
+ assert(rawMessage.clientId !== null, 0x861 /* clientId should not be null!! */);
634
793
  if (local) {
635
794
  // We are receiving the ACK for a local pending set operation.
636
795
  const { rowHandle, colHandle, localSeq } = localOpMetadata as ISetOpMetadata;
796
+ const isLatestPendingOp = this.isLatestPendingWrite(
797
+ rowHandle,
798
+ colHandle,
799
+ localSeq,
800
+ );
801
+ // If policy is switched and cell should be modified too based on policy, then update the tracker.
802
+ // If policy is not switched, then also update the tracker in case it is the latest.
803
+ if (
804
+ (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1 &&
805
+ this.shouldSetCellBasedOnFWW(rowHandle, colHandle, rawMessage)) ||
806
+ (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 && isLatestPendingOp)
807
+ ) {
808
+ this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
809
+ seqNum: rawMessage.sequenceNumber,
810
+ clientId: rawMessage.clientId,
811
+ });
812
+ }
637
813
 
638
- // If this is the most recent write to the cell by the local client, remove our
639
- // entry from 'pendingCliSeqs' to resume allowing remote writes.
640
- if (this.isLatestPendingWrite(rowHandle, colHandle, localSeq)) {
814
+ if (isLatestPendingOp) {
641
815
  this.pending.setCell(rowHandle, colHandle, undefined);
642
816
  }
643
817
  } else {
644
818
  const adjustedRow = this.rows.adjustPosition(row, rawMessage);
645
-
646
819
  if (adjustedRow !== undefined) {
647
820
  const adjustedCol = this.cols.adjustPosition(col, rawMessage);
648
821
 
@@ -654,13 +827,45 @@ export class SharedMatrix<T = any>
654
827
  isHandleValid(rowHandle) && isHandleValid(colHandle),
655
828
  0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
656
829
  );
657
-
658
- // If there is a pending (unACKed) local write to the same cell, skip the current op
659
- // since it "happened before" the pending write.
660
- if (this.pending.getCell(rowHandle, colHandle) === undefined) {
661
- const { value } = contents;
830
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1) {
831
+ // If someone tried to Overwrite the cell value or first write on this cell or
832
+ // same client tried to modify the cell or if the previous mode was LWW, then we need to still
833
+ // overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
834
+ if (
835
+ !isPreviousSetCellPolicyModeFWW ||
836
+ this.shouldSetCellBasedOnFWW(rowHandle, colHandle, rawMessage)
837
+ ) {
838
+ const previousValue = this.cells.getCell(rowHandle, colHandle);
839
+ this.cells.setCell(rowHandle, colHandle, value);
840
+ this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
841
+ seqNum: rawMessage.sequenceNumber,
842
+ clientId: rawMessage.clientId,
843
+ });
844
+ for (const consumer of this.consumers.values()) {
845
+ consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
846
+ }
847
+ // Check is there are any pending changes, which will be rejected. If so raise conflict.
848
+ if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
849
+ // Don't reset the pending value yet, as there maybe more fww op from same client, so we want
850
+ // to raise conflict event for that op also.
851
+ this.emit(
852
+ "conflict",
853
+ row,
854
+ col,
855
+ value, // Current value
856
+ previousValue, // Ignored local value
857
+ this,
858
+ );
859
+ }
860
+ }
861
+ } else if (this.pending.getCell(rowHandle, colHandle) === undefined) {
862
+ // If there is a pending (unACKed) local write to the same cell, skip the current op
863
+ // since it "happened before" the pending write.
662
864
  this.cells.setCell(rowHandle, colHandle, value);
663
-
865
+ this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
866
+ seqNum: rawMessage.sequenceNumber,
867
+ clientId: rawMessage.clientId,
868
+ });
664
869
  for (const consumer of this.consumers.values()) {
665
870
  consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
666
871
  }
@@ -698,6 +903,7 @@ export class SharedMatrix<T = any>
698
903
  for (const rowHandle of rowHandles) {
699
904
  this.cells.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
700
905
  this.pending.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
906
+ this.cellLastWriteTracker.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
701
907
  }
702
908
  };
703
909
 
@@ -705,9 +911,24 @@ export class SharedMatrix<T = any>
705
911
  for (const colHandle of colHandles) {
706
912
  this.cells.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
707
913
  this.pending.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
914
+ this.cellLastWriteTracker.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
708
915
  }
709
916
  };
710
917
 
918
+ /**
919
+ * Api to switch Set Op policy from Last Writer Win to First Writer Win. It only switches from LWW to FWW
920
+ * and not from FWW to LWW. The next SetOp which is sent will communicate this policy to other clients.
921
+ */
922
+ public switchSetCellPolicy() {
923
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1) {
924
+ if (this.isAttached()) {
925
+ this.userSwitchedSetCellPolicy = true;
926
+ } else {
927
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber = 0;
928
+ }
929
+ }
930
+ }
931
+
711
932
  /**
712
933
  * Returns true if the latest pending write to the cell indicated by the given row/col handles
713
934
  * matches the given 'localSeq'.
@@ -808,6 +1029,7 @@ export class SharedMatrix<T = any>
808
1029
  localSeq,
809
1030
  rowsRefSeq,
810
1031
  colsRefSeq,
1032
+ referenceSeqNumber: this.runtime.deltaManager.lastSequenceNumber,
811
1033
  };
812
1034
 
813
1035
  this.pending.setCell(rowHandle, colHandle, localSeq);
package/src/ops.ts CHANGED
@@ -9,6 +9,7 @@ export enum MatrixOp {
9
9
  spliceCols,
10
10
  spliceRows,
11
11
  set,
12
+ changeSetCellPolicy,
12
13
  }
13
14
 
14
15
  export interface IMatrixMsg {
@@ -27,3 +28,7 @@ export interface IMatrixCellMsg extends IMatrixMsg {
27
28
  col: number;
28
29
  value: Serializable;
29
30
  }
31
+
32
+ export interface IMatrixSwitchSetCellPolicy extends IMatrixMsg {
33
+ type: MatrixOp.changeSetCellPolicy;
34
+ }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/matrix";
9
- export const pkgVersion = "2.0.0-internal.7.3.0";
9
+ export const pkgVersion = "2.0.0-internal.7.4.1";
package/src/runtime.ts CHANGED
@@ -15,7 +15,7 @@ import { SharedMatrix } from "./matrix";
15
15
 
16
16
  /**
17
17
  * {@link @fluidframework/datastore-definitions#IChannelFactory} for {@link SharedMatrix}.
18
- * @public
18
+ * @alpha
19
19
  */
20
20
  export class SharedMatrixFactory implements IChannelFactory {
21
21
  public static Type = "https://graph.microsoft.com/types/sharedmatrix";
package/src/types.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // of SharedMatrix undo while we decide on the correct layering for undo.
8
8
 
9
9
  /**
10
- * @public
10
+ * @alpha
11
11
  */
12
12
  export interface IRevertible {
13
13
  revert();
@@ -15,7 +15,7 @@ export interface IRevertible {
15
15
  }
16
16
 
17
17
  /**
18
- * @public
18
+ * @alpha
19
19
  */
20
20
  export interface IUndoConsumer {
21
21
  pushToCurrentOperation(revertible: IRevertible);