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