@fluidframework/matrix 2.0.0-internal.7.3.0 → 2.0.0-internal.8.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +32 -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 +22 -12
  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 +74 -14
  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 +74 -14
  13. package/dist/matrix.cjs +178 -48
  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 +6 -2
  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/permutationvector.cjs +1 -1
  25. package/dist/permutationvector.cjs.map +1 -1
  26. package/dist/permutationvector.d.ts.map +1 -1
  27. package/dist/runtime.cjs +1 -1
  28. package/dist/runtime.cjs.map +1 -1
  29. package/dist/runtime.d.ts +1 -1
  30. package/dist/serialization.cjs.map +1 -1
  31. package/dist/serialization.d.ts +1 -1
  32. package/dist/serialization.d.ts.map +1 -1
  33. package/dist/types.cjs.map +1 -1
  34. package/dist/types.d.ts +5 -5
  35. package/dist/types.d.ts.map +1 -1
  36. package/lib/handlecache.d.ts +2 -2
  37. package/lib/handlecache.d.ts.map +1 -1
  38. package/lib/index.d.ts +3 -3
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.mjs.map +1 -1
  41. package/lib/matrix-alpha.d.ts +74 -14
  42. package/lib/matrix-beta.d.ts +41 -134
  43. package/lib/matrix-public.d.ts +41 -134
  44. package/lib/matrix-untrimmed.d.ts +74 -14
  45. package/lib/matrix.d.ts +69 -11
  46. package/lib/matrix.d.ts.map +1 -1
  47. package/lib/matrix.mjs +178 -47
  48. package/lib/matrix.mjs.map +1 -1
  49. package/lib/ops.d.ts +6 -2
  50. package/lib/ops.d.ts.map +1 -1
  51. package/lib/ops.mjs +1 -0
  52. package/lib/ops.mjs.map +1 -1
  53. package/lib/packageVersion.d.ts +1 -1
  54. package/lib/packageVersion.mjs +1 -1
  55. package/lib/packageVersion.mjs.map +1 -1
  56. package/lib/permutationvector.d.ts +3 -3
  57. package/lib/permutationvector.d.ts.map +1 -1
  58. package/lib/permutationvector.mjs +4 -1
  59. package/lib/permutationvector.mjs.map +1 -1
  60. package/lib/runtime.d.ts +1 -1
  61. package/lib/runtime.d.ts.map +1 -1
  62. package/lib/runtime.mjs +1 -1
  63. package/lib/runtime.mjs.map +1 -1
  64. package/lib/serialization.d.ts +1 -1
  65. package/lib/serialization.d.ts.map +1 -1
  66. package/lib/serialization.mjs.map +1 -1
  67. package/lib/sparsearray2d.d.ts.map +1 -1
  68. package/lib/types.d.ts +5 -5
  69. package/lib/types.d.ts.map +1 -1
  70. package/lib/types.mjs.map +1 -1
  71. package/lib/undoprovider.d.ts +4 -4
  72. package/lib/undoprovider.d.ts.map +1 -1
  73. package/matrix.test-files.tar +0 -0
  74. package/package.json +26 -31
  75. package/src/index.ts +1 -1
  76. package/src/matrix.ts +284 -62
  77. package/src/ops.ts +6 -1
  78. package/src/packageVersion.ts +1 -1
  79. package/src/permutationvector.ts +2 -2
  80. package/src/runtime.ts +1 -1
  81. package/src/serialization.ts +2 -2
  82. package/src/types.ts +5 -5
  83. package/tsconfig.json +1 -0
package/src/matrix.ts CHANGED
@@ -3,9 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- /* eslint-disable import/no-deprecated */
7
-
8
6
  import { assert } from "@fluidframework/core-utils";
7
+ import { IEventThisPlaceHolder } from "@fluidframework/core-interfaces";
9
8
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
10
9
  import {
11
10
  IFluidDataStoreRuntime,
@@ -15,6 +14,7 @@ import {
15
14
  } from "@fluidframework/datastore-definitions";
16
15
  import {
17
16
  IFluidSerializer,
17
+ ISharedObjectEvents,
18
18
  makeHandlesSerializable,
19
19
  parseHandles,
20
20
  SharedObject,
@@ -27,6 +27,7 @@ import {
27
27
  MergeTreeDeltaType,
28
28
  IMergeTreeOp,
29
29
  SegmentGroup,
30
+ // eslint-disable-next-line import/no-deprecated
30
31
  Client,
31
32
  IJSONSegment,
32
33
  } from "@fluidframework/merge-tree";
@@ -51,6 +52,7 @@ interface ISetOp<T> {
51
52
  row: number;
52
53
  col: number;
53
54
  value: MatrixItem<T>;
55
+ fwwMode?: boolean;
54
56
  }
55
57
 
56
58
  interface ISetOpMetadata {
@@ -59,12 +61,57 @@ interface ISetOpMetadata {
59
61
  localSeq: number;
60
62
  rowsRefSeq: number;
61
63
  colsRefSeq: number;
64
+ referenceSeqNumber: number;
65
+ }
66
+
67
+ /**
68
+ * Events emitted by Shared Matrix.
69
+ * @alpha
70
+ */
71
+ export interface ISharedMatrixEvents<T> extends ISharedObjectEvents {
72
+ /**
73
+ * This event is only emitted when the SetCell Resolution Policy is First Write Win(FWW).
74
+ * This is emitted when two clients race and send changes without observing each other changes,
75
+ * the changes that gets sequenced last would be rejected, and only client who's changes rejected
76
+ * would be notified via this event, with expectation that it will merge its changes back by
77
+ * accounting new information (state from winner of the race).
78
+ *
79
+ * @remarks Listener parameters:
80
+ *
81
+ * - `row` - Row number at which conflict happened.
82
+ *
83
+ * - `col` - Col number at which conflict happened.
84
+ *
85
+ * - `currentValue` - The current value of the cell.
86
+ *
87
+ * - `conflictingValue` - The value that this client tried to set in the cell and got ignored due to conflict.
88
+ *
89
+ * - `target` - The {@link SharedMatrix} itself.
90
+ */
91
+ (
92
+ event: "conflict",
93
+ listener: (
94
+ row: number,
95
+ col: number,
96
+ currentValue: MatrixItem<T>,
97
+ conflictingValue: MatrixItem<T>,
98
+ target: IEventThisPlaceHolder,
99
+ ) => void,
100
+ ): void;
101
+ }
102
+
103
+ /**
104
+ * This represents the item which is used to track the client which modified the cell last.
105
+ */
106
+ interface CellLastWriteTrackerItem {
107
+ seqNum: number; // Seq number of op which last modified this cell
108
+ clientId: string; // clientId of the client which last modified this cell
62
109
  }
63
110
 
64
111
  /**
65
112
  * A matrix cell value may be undefined (indicating an empty cell) or any serializable type,
66
113
  * excluding null. (However, nulls may be embedded inside objects and arrays.)
67
- * @public
114
+ * @alpha
68
115
  */
69
116
  // eslint-disable-next-line @rushstack/no-new-null -- Using 'null' to disallow 'null'.
70
117
  export type MatrixItem<T> = Serializable<Exclude<T, null>> | undefined;
@@ -80,11 +127,10 @@ export type MatrixItem<T> = Serializable<Exclude<T, null>> | undefined;
80
127
  * matrix data and physically stores data in Z-order to leverage CPU caches and
81
128
  * prefetching when reading in either row or column major order. (See README.md
82
129
  * for more details.)
83
- *
84
- * @public
130
+ * @alpha
85
131
  */
86
132
  export class SharedMatrix<T = any>
87
- extends SharedObject
133
+ extends SharedObject<ISharedMatrixEvents<T>>
88
134
  implements
89
135
  IMatrixProducer<MatrixItem<T>>,
90
136
  IMatrixReader<MatrixItem<T>>,
@@ -100,15 +146,33 @@ export class SharedMatrix<T = any>
100
146
  private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
101
147
 
102
148
  private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
103
- private pending = new SparseArray2D<number>(); // Tracks pending writes.
149
+ private readonly pending = new SparseArray2D<number>(); // Tracks pending writes.
150
+ private cellLastWriteTracker = new SparseArray2D<CellLastWriteTrackerItem>(); // Tracks last writes sequence number and clientId in a cell.
151
+ // Tracks the seq number of Op at which policy switch happens from Last Write Win to First Write Win.
152
+ private setCellLwwToFwwPolicySwitchOpSeqNumber: number;
153
+ private userSwitchedSetCellPolicy = false; // Set to true when the user calls switchPolicy.
104
154
 
155
+ // Used to track if there is any reentrancy in setCell code.
156
+ private reentrantCount: number = 0;
157
+
158
+ /**
159
+ * Constructor for the Shared Matrix
160
+ * @param runtime - DataStore runtime.
161
+ * @param id - id of the dds
162
+ * @param attributes - channel attributes
163
+ * @param _isSetCellConflictResolutionPolicyFWW - Conflict resolution for Matrix set op is First Writer Win in case of
164
+ * race condition. Client can still overwrite values in case of no race.
165
+ */
105
166
  constructor(
106
167
  runtime: IFluidDataStoreRuntime,
107
168
  public id: string,
108
169
  attributes: IChannelAttributes,
170
+ _isSetCellConflictResolutionPolicyFWW?: boolean,
109
171
  ) {
110
172
  super(id, runtime, attributes, "fluid_matrix_");
111
173
 
174
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber =
175
+ _isSetCellConflictResolutionPolicyFWW === true ? 0 : -1;
112
176
  this.rows = new PermutationVector(
113
177
  SnapshotPath.rows,
114
178
  this.logger,
@@ -178,6 +242,10 @@ export class SharedMatrix<T = any>
178
242
  return this.cols.getLength();
179
243
  }
180
244
 
245
+ public isSetCellConflictResolutionPolicyFWW() {
246
+ return this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1 || this.userSwitchedSetCellPolicy;
247
+ }
248
+
181
249
  public getCell(row: number, col: number): MatrixItem<T> {
182
250
  // Perf: When possible, bounds checking is performed inside the implementation for
183
251
  // 'getHandle()' so that it can be elided in the case of a cache hit. This
@@ -212,11 +280,6 @@ export class SharedMatrix<T = any>
212
280
  );
213
281
 
214
282
  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
283
  }
221
284
 
222
285
  public setCells(
@@ -250,11 +313,6 @@ export class SharedMatrix<T = any>
250
313
  r++;
251
314
  }
252
315
  }
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
316
  }
259
317
 
260
318
  private setCellCore(
@@ -264,20 +322,27 @@ export class SharedMatrix<T = any>
264
322
  rowHandle = this.rows.getAllocatedHandle(row),
265
323
  colHandle = this.cols.getAllocatedHandle(col),
266
324
  ) {
267
- if (this.undo !== undefined) {
268
- let oldValue = this.cells.getCell(rowHandle, colHandle);
269
- if (oldValue === null) {
270
- oldValue = undefined;
325
+ this.protectAgainstReentrancy(() => {
326
+ if (this.undo !== undefined) {
327
+ let oldValue = this.cells.getCell(rowHandle, colHandle);
328
+ if (oldValue === null) {
329
+ oldValue = undefined;
330
+ }
331
+
332
+ this.undo.cellSet(rowHandle, colHandle, oldValue);
271
333
  }
272
334
 
273
- this.undo.cellSet(rowHandle, colHandle, oldValue);
274
- }
335
+ this.cells.setCell(rowHandle, colHandle, value);
275
336
 
276
- this.cells.setCell(rowHandle, colHandle, value);
337
+ if (this.isAttached()) {
338
+ this.sendSetCellOp(row, col, value, rowHandle, colHandle);
339
+ }
277
340
 
278
- if (this.isAttached()) {
279
- this.sendSetCellOp(row, col, value, rowHandle, colHandle);
280
- }
341
+ // Avoid reentrancy by raising change notifications after the op is queued.
342
+ for (const consumer of this.consumers.values()) {
343
+ consumer.cellsChanged(row, col, 1, 1, this);
344
+ }
345
+ });
281
346
  }
282
347
 
283
348
  private sendSetCellOp(
@@ -300,6 +365,8 @@ export class SharedMatrix<T = any>
300
365
  row,
301
366
  col,
302
367
  value,
368
+ fwwMode:
369
+ this.userSwitchedSetCellPolicy || this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1,
303
370
  };
304
371
 
305
372
  const metadata: ISetOpMetadata = {
@@ -308,12 +375,29 @@ export class SharedMatrix<T = any>
308
375
  localSeq,
309
376
  rowsRefSeq,
310
377
  colsRefSeq,
378
+ referenceSeqNumber: this.runtime.deltaManager.lastSequenceNumber,
311
379
  };
312
380
 
313
381
  this.submitLocalMessage(op, metadata);
314
382
  this.pending.setCell(rowHandle, colHandle, localSeq);
315
383
  }
316
384
 
385
+ /**
386
+ * This makes sure that the code inside the callback is not reentrant. We need to do that because we raise notifications
387
+ * to the consumers telling about these changes and they can try to change the matrix while listening to those notifications
388
+ * which can make the shared matrix to be in bad state. For example, we are raising notification for a setCell changes and
389
+ * a consumer tries to delete that row/col on receiving that notification which can lead to this matrix trying to setCell in
390
+ * a deleted row/col.
391
+ * @param callback - code that needs to protected against reentrancy.
392
+ */
393
+ private protectAgainstReentrancy(callback: () => void) {
394
+ assert(this.reentrantCount === 0, 0x85d /* reentrant code */);
395
+ this.reentrantCount++;
396
+ callback();
397
+ this.reentrantCount--;
398
+ assert(this.reentrantCount === 0, 0x85e /* reentrant code on exit */);
399
+ }
400
+
317
401
  private submitVectorMessage(
318
402
  currentVector: PermutationVector,
319
403
  oppositeVector: PermutationVector,
@@ -355,11 +439,15 @@ export class SharedMatrix<T = any>
355
439
  }
356
440
 
357
441
  public insertCols(colStart: number, count: number) {
358
- this.submitColMessage(this.cols.insert(colStart, count));
442
+ this.protectAgainstReentrancy(() =>
443
+ this.submitColMessage(this.cols.insert(colStart, count)),
444
+ );
359
445
  }
360
446
 
361
447
  public removeCols(colStart: number, count: number) {
362
- this.submitColMessage(this.cols.remove(colStart, count));
448
+ this.protectAgainstReentrancy(() =>
449
+ this.submitColMessage(this.cols.remove(colStart, count)),
450
+ );
363
451
  }
364
452
 
365
453
  private submitRowMessage(message: any) {
@@ -367,14 +455,18 @@ export class SharedMatrix<T = any>
367
455
  }
368
456
 
369
457
  public insertRows(rowStart: number, count: number) {
370
- this.submitRowMessage(this.rows.insert(rowStart, count));
458
+ this.protectAgainstReentrancy(() =>
459
+ this.submitRowMessage(this.rows.insert(rowStart, count)),
460
+ );
371
461
  }
372
462
 
373
463
  public removeRows(rowStart: number, count: number) {
374
- this.submitRowMessage(this.rows.remove(rowStart, count));
464
+ this.protectAgainstReentrancy(() =>
465
+ this.submitRowMessage(this.rows.remove(rowStart, count)),
466
+ );
375
467
  }
376
468
 
377
- /** @internal */ public _undoRemoveRows(rowStart: number, spec: IJSONSegment) {
469
+ /***/ public _undoRemoveRows(rowStart: number, spec: IJSONSegment) {
378
470
  const { op, inserted } = reinsertSegmentIntoVector(this.rows, rowStart, spec);
379
471
  this.submitRowMessage(op);
380
472
 
@@ -397,7 +489,7 @@ export class SharedMatrix<T = any>
397
489
  }
398
490
  }
399
491
 
400
- /** @internal */ public _undoRemoveCols(colStart: number, spec: IJSONSegment) {
492
+ /***/ public _undoRemoveCols(colStart: number, spec: IJSONSegment) {
401
493
  const { op, inserted } = reinsertSegmentIntoVector(this.cols, colStart, spec);
402
494
  this.submitColMessage(op);
403
495
 
@@ -430,9 +522,19 @@ export class SharedMatrix<T = any>
430
522
  SnapshotPath.cols,
431
523
  this.cols.summarize(this.runtime, this.handle, serializer),
432
524
  );
525
+ const artifactsToSummarize = [
526
+ this.cells.snapshot(),
527
+ this.pending.snapshot(),
528
+ this.setCellLwwToFwwPolicySwitchOpSeqNumber,
529
+ ];
530
+
531
+ // Only need to store it in the snapshot if we have switched the policy already.
532
+ if (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1) {
533
+ artifactsToSummarize.push(this.cellLastWriteTracker.snapshot());
534
+ }
433
535
  builder.addBlob(
434
536
  SnapshotPath.cells,
435
- serializer.stringify([this.cells.snapshot(), this.pending.snapshot()], this.handle),
537
+ serializer.stringify(artifactsToSummarize, this.handle),
436
538
  );
437
539
  return builder.getSummaryTree();
438
540
  }
@@ -506,6 +608,7 @@ export class SharedMatrix<T = any>
506
608
  }
507
609
 
508
610
  private rebasePosition(
611
+ // eslint-disable-next-line import/no-deprecated
509
612
  client: Client,
510
613
  pos: number,
511
614
  referenceSequenceNumber: number,
@@ -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 {
@@ -25,5 +26,9 @@ export interface IMatrixCellMsg extends IMatrixMsg {
25
26
  type: MatrixOp.set;
26
27
  row: number;
27
28
  col: number;
28
- value: Serializable;
29
+ value: Serializable<unknown>;
30
+ }
31
+
32
+ export interface IMatrixSwitchSetCellPolicy extends IMatrixMsg {
33
+ type: MatrixOp.changeSetCellPolicy;
29
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.8.0.0";
@@ -3,8 +3,6 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- /* eslint-disable import/no-deprecated */
7
-
8
6
  import { assert } from "@fluidframework/core-utils";
9
7
  import { createChildLogger } from "@fluidframework/telemetry-utils";
10
8
  import {
@@ -14,6 +12,7 @@ import {
14
12
  import {
15
13
  BaseSegment,
16
14
  ISegment,
15
+ // eslint-disable-next-line import/no-deprecated
17
16
  Client,
18
17
  IMergeTreeDeltaOpArgs,
19
18
  IMergeTreeDeltaCallbackArgs,
@@ -120,6 +119,7 @@ export class PermutationSegment extends BaseSegment {
120
119
  }
121
120
  }
122
121
 
122
+ // eslint-disable-next-line import/no-deprecated
123
123
  export class PermutationVector extends Client {
124
124
  private handleTable = new HandleTable<never>(); // Tracks available storage handles for rows.
125
125
  public readonly handleCache = new HandleCache(this);
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";
@@ -9,10 +9,10 @@ import { BlobTreeEntry } from "@fluidframework/driver-utils";
9
9
  import { IFluidSerializer } from "@fluidframework/shared-object-base";
10
10
  import { bufferToString } from "@fluid-internal/client-utils";
11
11
 
12
- export const serializeBlob = (
12
+ export const serializeBlob = <T>(
13
13
  handle: IFluidHandle,
14
14
  path: string,
15
- snapshot: Serializable,
15
+ snapshot: Serializable<T>,
16
16
  serializer: IFluidSerializer,
17
17
  ) => new BlobTreeEntry(path, serializer.stringify(snapshot, handle));
18
18
 
package/src/types.ts CHANGED
@@ -7,16 +7,16 @@
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
- revert();
14
- discard();
13
+ revert(): void;
14
+ discard(): void;
15
15
  }
16
16
 
17
17
  /**
18
- * @public
18
+ * @alpha
19
19
  */
20
20
  export interface IUndoConsumer {
21
- pushToCurrentOperation(revertible: IRevertible);
21
+ pushToCurrentOperation(revertible: IRevertible): void;
22
22
  }
package/tsconfig.json CHANGED
@@ -8,5 +8,6 @@
8
8
  "compilerOptions": {
9
9
  "rootDir": "./src",
10
10
  "outDir": "./dist",
11
+ "noImplicitAny": true,
11
12
  },
12
13
  }