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