@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.
- package/CHANGELOG.md +32 -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 +22 -12
- 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 +74 -14
- package/dist/matrix-beta.d.ts +41 -134
- package/dist/matrix-public.d.ts +41 -134
- package/dist/matrix-untrimmed.d.ts +74 -14
- package/dist/matrix.cjs +178 -48
- 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 +6 -2
- 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/permutationvector.cjs +1 -1
- package/dist/permutationvector.cjs.map +1 -1
- package/dist/permutationvector.d.ts.map +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/serialization.cjs.map +1 -1
- package/dist/serialization.d.ts +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +5 -5
- package/dist/types.d.ts.map +1 -1
- 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 +74 -14
- package/lib/matrix-beta.d.ts +41 -134
- package/lib/matrix-public.d.ts +41 -134
- package/lib/matrix-untrimmed.d.ts +74 -14
- package/lib/matrix.d.ts +69 -11
- package/lib/matrix.d.ts.map +1 -1
- package/lib/matrix.mjs +178 -47
- package/lib/matrix.mjs.map +1 -1
- package/lib/ops.d.ts +6 -2
- 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/permutationvector.mjs +4 -1
- package/lib/permutationvector.mjs.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 +1 -1
- package/lib/serialization.d.ts.map +1 -1
- package/lib/serialization.mjs.map +1 -1
- package/lib/sparsearray2d.d.ts.map +1 -1
- package/lib/types.d.ts +5 -5
- package/lib/types.d.ts.map +1 -1
- 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 +284 -62
- package/src/ops.ts +6 -1
- package/src/packageVersion.ts +1 -1
- package/src/permutationvector.ts +2 -2
- package/src/runtime.ts +1 -1
- package/src/serialization.ts +2 -2
- package/src/types.ts +5 -5
- 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
|
-
* @
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
oldValue
|
|
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.
|
|
274
|
-
}
|
|
335
|
+
this.cells.setCell(rowHandle, colHandle, value);
|
|
275
336
|
|
|
276
|
-
|
|
337
|
+
if (this.isAttached()) {
|
|
338
|
+
this.sendSetCellOp(row, col, value, rowHandle, colHandle);
|
|
339
|
+
}
|
|
277
340
|
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
464
|
+
this.protectAgainstReentrancy(() =>
|
|
465
|
+
this.submitRowMessage(this.rows.remove(rowStart, count)),
|
|
466
|
+
);
|
|
375
467
|
}
|
|
376
468
|
|
|
377
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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 {
|
|
@@ -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
|
}
|
package/src/packageVersion.ts
CHANGED
package/src/permutationvector.ts
CHANGED
|
@@ -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
|
-
* @
|
|
18
|
+
* @alpha
|
|
19
19
|
*/
|
|
20
20
|
export class SharedMatrixFactory implements IChannelFactory {
|
|
21
21
|
public static Type = "https://graph.microsoft.com/types/sharedmatrix";
|
package/src/serialization.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
-
* @
|
|
18
|
+
* @alpha
|
|
19
19
|
*/
|
|
20
20
|
export interface IUndoConsumer {
|
|
21
|
-
pushToCurrentOperation(revertible: IRevertible);
|
|
21
|
+
pushToCurrentOperation(revertible: IRevertible): void;
|
|
22
22
|
}
|