@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.
- package/CHANGELOG.md +4 -0
- package/README.md +39 -0
- package/api-extractor-lint.json +13 -0
- package/api-extractor.json +8 -3
- package/api-report/matrix.api.md +19 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/matrix-alpha.d.ts +71 -11
- package/dist/matrix-beta.d.ts +41 -134
- package/dist/matrix-public.d.ts +41 -134
- package/dist/matrix-untrimmed.d.ts +71 -11
- package/dist/matrix.cjs +175 -46
- package/dist/matrix.cjs.map +1 -1
- package/dist/matrix.d.ts +67 -9
- package/dist/matrix.d.ts.map +1 -1
- package/dist/ops.cjs +1 -0
- package/dist/ops.cjs.map +1 -1
- package/dist/ops.d.ts +5 -1
- package/dist/ops.d.ts.map +1 -1
- package/dist/packageVersion.cjs +1 -1
- package/dist/packageVersion.cjs.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +2 -2
- package/lib/handlecache.d.ts +2 -2
- package/lib/handlecache.d.ts.map +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.mjs.map +1 -1
- package/lib/matrix-alpha.d.ts +71 -11
- package/lib/matrix-beta.d.ts +41 -134
- package/lib/matrix-public.d.ts +41 -134
- package/lib/matrix-untrimmed.d.ts +71 -11
- package/lib/matrix.d.ts +69 -11
- package/lib/matrix.d.ts.map +1 -1
- package/lib/matrix.mjs +175 -46
- package/lib/matrix.mjs.map +1 -1
- package/lib/ops.d.ts +5 -1
- package/lib/ops.d.ts.map +1 -1
- package/lib/ops.mjs +1 -0
- package/lib/ops.mjs.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.mjs +1 -1
- package/lib/packageVersion.mjs.map +1 -1
- package/lib/permutationvector.d.ts +3 -3
- package/lib/permutationvector.d.ts.map +1 -1
- package/lib/runtime.d.ts +1 -1
- package/lib/runtime.d.ts.map +1 -1
- package/lib/runtime.mjs +1 -1
- package/lib/runtime.mjs.map +1 -1
- package/lib/serialization.d.ts.map +1 -1
- package/lib/sparsearray2d.d.ts.map +1 -1
- package/lib/types.d.ts +2 -2
- package/lib/types.mjs.map +1 -1
- package/lib/undoprovider.d.ts +4 -4
- package/lib/undoprovider.d.ts.map +1 -1
- package/matrix.test-files.tar +0 -0
- package/package.json +26 -31
- package/src/index.ts +1 -1
- package/src/matrix.ts +282 -60
- package/src/ops.ts +5 -0
- package/src/packageVersion.ts +1 -1
- package/src/runtime.ts +1 -1
- 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
|
-
* @
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
oldValue
|
|
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.
|
|
274
|
-
}
|
|
336
|
+
this.cells.setCell(rowHandle, colHandle, value);
|
|
275
337
|
|
|
276
|
-
|
|
338
|
+
if (this.isAttached()) {
|
|
339
|
+
this.sendSetCellOp(row, col, value, rowHandle, colHandle);
|
|
340
|
+
}
|
|
277
341
|
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
465
|
+
this.protectAgainstReentrancy(() =>
|
|
466
|
+
this.submitRowMessage(this.rows.remove(rowStart, count)),
|
|
467
|
+
);
|
|
375
468
|
}
|
|
376
469
|
|
|
377
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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 [
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
+
}
|
package/src/packageVersion.ts
CHANGED
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
18
|
+
* @alpha
|
|
19
19
|
*/
|
|
20
20
|
export interface IUndoConsumer {
|
|
21
21
|
pushToCurrentOperation(revertible: IRevertible);
|