@fluidframework/matrix 2.41.0-338401 → 2.42.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 +8 -0
- package/dist/matrix.d.ts +2 -12
- package/dist/matrix.d.ts.map +1 -1
- package/dist/matrix.js +169 -115
- package/dist/matrix.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/permutationvector.d.ts +4 -1
- package/dist/permutationvector.d.ts.map +1 -1
- package/dist/permutationvector.js +34 -13
- package/dist/permutationvector.js.map +1 -1
- package/lib/matrix.d.ts +2 -12
- package/lib/matrix.d.ts.map +1 -1
- package/lib/matrix.js +169 -115
- package/lib/matrix.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/permutationvector.d.ts +4 -1
- package/lib/permutationvector.d.ts.map +1 -1
- package/lib/permutationvector.js +34 -13
- package/lib/permutationvector.js.map +1 -1
- package/package.json +20 -19
- package/src/matrix.ts +249 -148
- package/src/packageVersion.ts +1 -1
- package/src/permutationvector.ts +45 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/matrix",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.42.0",
|
|
4
4
|
"description": "Distributed matrix",
|
|
5
5
|
"homepage": "https://fluidframework.com",
|
|
6
6
|
"repository": {
|
|
@@ -81,17 +81,17 @@
|
|
|
81
81
|
"temp-directory": "nyc/.nyc_output"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@fluid-internal/client-utils": "2.
|
|
85
|
-
"@fluidframework/core-interfaces": "2.
|
|
86
|
-
"@fluidframework/core-utils": "2.
|
|
87
|
-
"@fluidframework/datastore-definitions": "2.
|
|
88
|
-
"@fluidframework/driver-definitions": "2.
|
|
89
|
-
"@fluidframework/driver-utils": "2.
|
|
90
|
-
"@fluidframework/merge-tree": "2.
|
|
91
|
-
"@fluidframework/runtime-definitions": "2.
|
|
92
|
-
"@fluidframework/runtime-utils": "2.
|
|
93
|
-
"@fluidframework/shared-object-base": "2.
|
|
94
|
-
"@fluidframework/telemetry-utils": "2.
|
|
84
|
+
"@fluid-internal/client-utils": "~2.42.0",
|
|
85
|
+
"@fluidframework/core-interfaces": "~2.42.0",
|
|
86
|
+
"@fluidframework/core-utils": "~2.42.0",
|
|
87
|
+
"@fluidframework/datastore-definitions": "~2.42.0",
|
|
88
|
+
"@fluidframework/driver-definitions": "~2.42.0",
|
|
89
|
+
"@fluidframework/driver-utils": "~2.42.0",
|
|
90
|
+
"@fluidframework/merge-tree": "~2.42.0",
|
|
91
|
+
"@fluidframework/runtime-definitions": "~2.42.0",
|
|
92
|
+
"@fluidframework/runtime-utils": "~2.42.0",
|
|
93
|
+
"@fluidframework/shared-object-base": "~2.42.0",
|
|
94
|
+
"@fluidframework/telemetry-utils": "~2.42.0",
|
|
95
95
|
"@tiny-calc/nano": "0.0.0-alpha.5",
|
|
96
96
|
"double-ended-queue": "^2.1.0-0",
|
|
97
97
|
"tslib": "^1.10.0"
|
|
@@ -99,17 +99,17 @@
|
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@arethetypeswrong/cli": "^0.17.1",
|
|
101
101
|
"@biomejs/biome": "~1.9.3",
|
|
102
|
-
"@fluid-internal/mocha-test-setup": "2.
|
|
103
|
-
"@fluid-private/stochastic-test-utils": "2.
|
|
104
|
-
"@fluid-private/test-dds-utils": "2.
|
|
102
|
+
"@fluid-internal/mocha-test-setup": "~2.42.0",
|
|
103
|
+
"@fluid-private/stochastic-test-utils": "~2.42.0",
|
|
104
|
+
"@fluid-private/test-dds-utils": "~2.42.0",
|
|
105
105
|
"@fluid-tools/benchmark": "^0.51.0",
|
|
106
106
|
"@fluid-tools/build-cli": "^0.55.0",
|
|
107
107
|
"@fluidframework/build-common": "^2.0.3",
|
|
108
108
|
"@fluidframework/build-tools": "^0.55.0",
|
|
109
|
-
"@fluidframework/container-definitions": "2.
|
|
110
|
-
"@fluidframework/eslint-config-fluid": "^5.7.
|
|
111
|
-
"@fluidframework/matrix-previous": "npm:@fluidframework/matrix@2.
|
|
112
|
-
"@fluidframework/test-runtime-utils": "2.
|
|
109
|
+
"@fluidframework/container-definitions": "~2.42.0",
|
|
110
|
+
"@fluidframework/eslint-config-fluid": "^5.7.4",
|
|
111
|
+
"@fluidframework/matrix-previous": "npm:@fluidframework/matrix@2.41.0",
|
|
112
|
+
"@fluidframework/test-runtime-utils": "~2.42.0",
|
|
113
113
|
"@microsoft/api-extractor": "7.52.8",
|
|
114
114
|
"@tiny-calc/micro": "0.0.0-alpha.5",
|
|
115
115
|
"@types/double-ended-queue": "^2.1.0",
|
|
@@ -172,6 +172,7 @@
|
|
|
172
172
|
"lint:fix": "fluid-build . --task eslint:fix --task format",
|
|
173
173
|
"pack:tests": "tar -cf ./matrix.test-files.tar ./src/test ./dist/test ./lib/test",
|
|
174
174
|
"test": "npm run test:mocha",
|
|
175
|
+
"test:benchmark:report": "mocha --config src/test/time/.mocharc.cjs",
|
|
175
176
|
"test:coverage": "c8 npm test",
|
|
176
177
|
"test:memory": "mocha --config src/test/memory/.mocharc.cjs",
|
|
177
178
|
"test:memory-profiling:report": "mocha --config src/test/memory/.mocharc.cjs",
|
package/src/matrix.ts
CHANGED
|
@@ -204,6 +204,31 @@ export interface ISharedMatrix<T = any>
|
|
|
204
204
|
switchSetCellPolicy(): void;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
type FirstWriterWinsPolicy =
|
|
208
|
+
| { state: "off" }
|
|
209
|
+
| { state: "local" }
|
|
210
|
+
| {
|
|
211
|
+
state: "on";
|
|
212
|
+
switchOpSeqNumber: number;
|
|
213
|
+
cellLastWriteTracker: SparseArray2D<CellLastWriteTrackerItem>;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Tracks pending local changes for a cell.
|
|
218
|
+
*/
|
|
219
|
+
interface PendingCellChanges<T> {
|
|
220
|
+
/**
|
|
221
|
+
* The local changes including the local seq, and the value set at that local seq.
|
|
222
|
+
*/
|
|
223
|
+
local: { localSeq: number; value: MatrixItem<T> }[];
|
|
224
|
+
/**
|
|
225
|
+
* The latest consensus value across all clients.
|
|
226
|
+
* this will either be a remote value or ack'd local
|
|
227
|
+
* value.
|
|
228
|
+
*/
|
|
229
|
+
consensus?: MatrixItem<T>;
|
|
230
|
+
}
|
|
231
|
+
|
|
207
232
|
/**
|
|
208
233
|
* A SharedMatrix holds a rectangular 2D array of values. Supported operations
|
|
209
234
|
* include setting values and inserting/removing rows and columns.
|
|
@@ -243,11 +268,11 @@ export class SharedMatrix<T = any>
|
|
|
243
268
|
private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
|
|
244
269
|
|
|
245
270
|
private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
|
|
246
|
-
private readonly pending = new SparseArray2D<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
271
|
+
private readonly pending = new SparseArray2D<PendingCellChanges<T>>(); // Tracks pending writes.
|
|
272
|
+
|
|
273
|
+
private fwwPolicy: FirstWriterWinsPolicy = {
|
|
274
|
+
state: "off",
|
|
275
|
+
};
|
|
251
276
|
|
|
252
277
|
// Used to track if there is any reentrancy in setCell code.
|
|
253
278
|
private reentrantCount: number = 0;
|
|
@@ -267,7 +292,6 @@ export class SharedMatrix<T = any>
|
|
|
267
292
|
) {
|
|
268
293
|
super(id, runtime, attributes, "fluid_matrix_");
|
|
269
294
|
|
|
270
|
-
this.setCellLwwToFwwPolicySwitchOpSeqNumber = -1;
|
|
271
295
|
this.rows = new PermutationVector(
|
|
272
296
|
SnapshotPath.rows,
|
|
273
297
|
this.logger,
|
|
@@ -333,7 +357,7 @@ export class SharedMatrix<T = any>
|
|
|
333
357
|
}
|
|
334
358
|
|
|
335
359
|
public isSetCellConflictResolutionPolicyFWW(): boolean {
|
|
336
|
-
return this.
|
|
360
|
+
return this.fwwPolicy.state !== "off";
|
|
337
361
|
}
|
|
338
362
|
|
|
339
363
|
public getCell(row: number, col: number): MatrixItem<T> {
|
|
@@ -410,21 +434,22 @@ export class SharedMatrix<T = any>
|
|
|
410
434
|
value: MatrixItem<T>,
|
|
411
435
|
rowHandle = this.rows.getAllocatedHandle(row),
|
|
412
436
|
colHandle = this.cols.getAllocatedHandle(col),
|
|
437
|
+
rollback?: boolean,
|
|
413
438
|
): void {
|
|
414
439
|
this.protectAgainstReentrancy(() => {
|
|
415
|
-
|
|
416
|
-
let oldValue = this.cells.getCell(rowHandle, colHandle);
|
|
417
|
-
if (oldValue === null) {
|
|
418
|
-
oldValue = undefined;
|
|
419
|
-
}
|
|
440
|
+
const oldValue = this.cells.getCell(rowHandle, colHandle) ?? undefined;
|
|
420
441
|
|
|
442
|
+
if (this.undo !== undefined) {
|
|
421
443
|
this.undo.cellSet(rowHandle, colHandle, oldValue);
|
|
422
444
|
}
|
|
423
445
|
|
|
424
446
|
this.cells.setCell(rowHandle, colHandle, value);
|
|
425
447
|
|
|
426
|
-
if (this.isAttached()) {
|
|
427
|
-
this.sendSetCellOp(row, col, value, rowHandle, colHandle);
|
|
448
|
+
if (this.isAttached() && rollback !== true) {
|
|
449
|
+
const pending = this.sendSetCellOp(row, col, value, rowHandle, colHandle);
|
|
450
|
+
if (pending.local.length === 1) {
|
|
451
|
+
pending.consensus ??= oldValue;
|
|
452
|
+
}
|
|
428
453
|
}
|
|
429
454
|
|
|
430
455
|
// Avoid reentrancy by raising change notifications after the op is queued.
|
|
@@ -459,7 +484,7 @@ export class SharedMatrix<T = any>
|
|
|
459
484
|
rowHandle: Handle,
|
|
460
485
|
colHandle: Handle,
|
|
461
486
|
localSeq = this.nextLocalSeq(),
|
|
462
|
-
):
|
|
487
|
+
): PendingCellChanges<T> {
|
|
463
488
|
assert(
|
|
464
489
|
this.isAttached(),
|
|
465
490
|
0x1e2 /* "Caller must ensure 'isAttached()' before calling 'sendSetCellOp'." */,
|
|
@@ -470,8 +495,7 @@ export class SharedMatrix<T = any>
|
|
|
470
495
|
row,
|
|
471
496
|
col,
|
|
472
497
|
value,
|
|
473
|
-
fwwMode:
|
|
474
|
-
this.userSwitchedSetCellPolicy || this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1,
|
|
498
|
+
fwwMode: this.fwwPolicy.state !== "off",
|
|
475
499
|
};
|
|
476
500
|
|
|
477
501
|
const rowsRef = this.createOpMetadataLocalRef(this.rows, row, localSeq);
|
|
@@ -486,7 +510,12 @@ export class SharedMatrix<T = any>
|
|
|
486
510
|
};
|
|
487
511
|
|
|
488
512
|
this.submitLocalMessage(op, metadata);
|
|
489
|
-
this.pending.
|
|
513
|
+
const pendingCell: PendingCellChanges<T> = this.pending.getCell(rowHandle, colHandle) ?? {
|
|
514
|
+
local: [],
|
|
515
|
+
};
|
|
516
|
+
pendingCell.local.push({ localSeq, value });
|
|
517
|
+
this.pending.setCell(rowHandle, colHandle, pendingCell);
|
|
518
|
+
return pendingCell;
|
|
490
519
|
}
|
|
491
520
|
|
|
492
521
|
/**
|
|
@@ -668,15 +697,38 @@ export class SharedMatrix<T = any>
|
|
|
668
697
|
SnapshotPath.cols,
|
|
669
698
|
this.cols.summarize(this.runtime, this.handle, serializer),
|
|
670
699
|
);
|
|
671
|
-
const artifactsToSummarize
|
|
700
|
+
const artifactsToSummarize: (
|
|
701
|
+
| undefined
|
|
702
|
+
| number
|
|
703
|
+
| ReturnType<SparseArray2D<MatrixItem<T> | number>["snapshot"]>
|
|
704
|
+
)[] = [
|
|
672
705
|
this.cells.snapshot(),
|
|
673
|
-
|
|
674
|
-
|
|
706
|
+
/**
|
|
707
|
+
* we used to write this.pending.snapshot(). this should have never been done, as pending is only for local
|
|
708
|
+
* changes, and there should never be local changes in the summarizer. This was also never used on load
|
|
709
|
+
* as there is no way to understand a previous clients pending changes. so we just set this to a constant
|
|
710
|
+
* which matches an empty this.pending.snapshot() for back-compat in terms of the array length
|
|
711
|
+
*/
|
|
712
|
+
[undefined],
|
|
675
713
|
];
|
|
676
714
|
|
|
677
715
|
// Only need to store it in the snapshot if we have switched the policy already.
|
|
678
|
-
if (this.
|
|
679
|
-
artifactsToSummarize.push(
|
|
716
|
+
if (this.fwwPolicy.state === "on") {
|
|
717
|
+
artifactsToSummarize.push(
|
|
718
|
+
this.fwwPolicy.switchOpSeqNumber,
|
|
719
|
+
this.fwwPolicy.cellLastWriteTracker.snapshot(),
|
|
720
|
+
);
|
|
721
|
+
} else {
|
|
722
|
+
// back-compat: used -1 for disabled
|
|
723
|
+
artifactsToSummarize.push(
|
|
724
|
+
-1,
|
|
725
|
+
/*
|
|
726
|
+
* we should set undefined in place of cellLastWriteTracker to ensure the number of array entries is consistent.
|
|
727
|
+
* Doing that currently breaks snapshot tests. Its is probably fine, but if new elements are ever added, we need
|
|
728
|
+
* ensure undefined is also set.
|
|
729
|
+
*/
|
|
730
|
+
// undefined
|
|
731
|
+
);
|
|
680
732
|
}
|
|
681
733
|
builder.addBlob(
|
|
682
734
|
SnapshotPath.cells,
|
|
@@ -785,35 +837,43 @@ export class SharedMatrix<T = any>
|
|
|
785
837
|
const col = this.rebasePosition(this.cols, colsRef, localSeq);
|
|
786
838
|
this.rows.removeLocalReferencePosition(rowsRef);
|
|
787
839
|
this.cols.removeLocalReferencePosition(colsRef);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
840
|
+
|
|
841
|
+
const pendingCell = this.pending.getCell(rowHandle, colHandle);
|
|
842
|
+
assert(pendingCell !== undefined, 0xba4 /* local operation must have a pending array */);
|
|
843
|
+
const { local } = pendingCell;
|
|
844
|
+
assert(local !== undefined, 0xba5 /* local operation must have a pending array */);
|
|
845
|
+
const localSeqIndex = local.findIndex((p) => p.localSeq === localSeq);
|
|
846
|
+
assert(localSeqIndex >= 0, 0xba6 /* local operation must have a pending entry */);
|
|
847
|
+
const [change] = local.splice(localSeqIndex, 1);
|
|
848
|
+
assert(change.localSeq === localSeq, 0xba7 /* must match */);
|
|
849
|
+
|
|
850
|
+
if (
|
|
851
|
+
row !== undefined &&
|
|
852
|
+
col !== undefined &&
|
|
853
|
+
row >= 0 &&
|
|
854
|
+
col >= 0 && // If the mode is LWW, then send the op.
|
|
794
855
|
// Otherwise if the current mode is FWW and if we generated this op, after seeing the
|
|
795
856
|
// last set op, or it is the first set op for the cell, then regenerate the op,
|
|
796
857
|
// otherwise raise conflict. We want to check the current mode here and not that
|
|
797
858
|
// whether op was made in FWW or not.
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
)
|
|
803
|
-
this.sendSetCellOp(row, col, setOp.value, rowHandle, colHandle, localSeq);
|
|
804
|
-
} else if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
|
|
805
|
-
// Clear the pending changes if any as we are not sending the op.
|
|
806
|
-
this.pending.setCell(rowHandle, colHandle, undefined);
|
|
807
|
-
}
|
|
859
|
+
(this.fwwPolicy.state !== "on" ||
|
|
860
|
+
referenceSeqNumber >=
|
|
861
|
+
(this.fwwPolicy.cellLastWriteTracker.getCell(rowHandle, colHandle)?.seqNum ?? 0))
|
|
862
|
+
) {
|
|
863
|
+
this.sendSetCellOp(row, col, setOp.value, rowHandle, colHandle, localSeq);
|
|
808
864
|
}
|
|
809
865
|
} else {
|
|
810
866
|
switch (content.target) {
|
|
811
867
|
case SnapshotPath.cols: {
|
|
812
|
-
this.submitColMessage(
|
|
868
|
+
this.submitColMessage(
|
|
869
|
+
this.cols.regeneratePendingOp(content, localOpMetadata, false),
|
|
870
|
+
);
|
|
813
871
|
break;
|
|
814
872
|
}
|
|
815
873
|
case SnapshotPath.rows: {
|
|
816
|
-
this.submitRowMessage(
|
|
874
|
+
this.submitRowMessage(
|
|
875
|
+
this.rows.regeneratePendingOp(content, localOpMetadata, false),
|
|
876
|
+
);
|
|
817
877
|
break;
|
|
818
878
|
}
|
|
819
879
|
default: {
|
|
@@ -823,6 +883,47 @@ export class SharedMatrix<T = any>
|
|
|
823
883
|
}
|
|
824
884
|
}
|
|
825
885
|
|
|
886
|
+
protected rollback(content: unknown, localOpMetadata: unknown): void {
|
|
887
|
+
const contents = content as MatrixSetOrVectorOp<T>;
|
|
888
|
+
const target = contents.target;
|
|
889
|
+
|
|
890
|
+
switch (target) {
|
|
891
|
+
case SnapshotPath.cols: {
|
|
892
|
+
this.cols.rollback(content, localOpMetadata);
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
case SnapshotPath.rows: {
|
|
896
|
+
this.rows.rollback(content, localOpMetadata);
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
case undefined: {
|
|
900
|
+
assert(contents.type === MatrixOp.set, 0xba8 /* only sets supported */);
|
|
901
|
+
const setMetadata = localOpMetadata as ISetOpMetadata;
|
|
902
|
+
|
|
903
|
+
const pendingCell = this.pending.getCell(setMetadata.rowHandle, setMetadata.colHandle);
|
|
904
|
+
assert(pendingCell !== undefined, 0xba9 /* must have pending */);
|
|
905
|
+
|
|
906
|
+
const change = pendingCell.local.pop();
|
|
907
|
+
assert(change?.localSeq === setMetadata.localSeq, 0xbaa /* must have change */);
|
|
908
|
+
|
|
909
|
+
const previous =
|
|
910
|
+
pendingCell.local.length > 0
|
|
911
|
+
? pendingCell.local[pendingCell.local.length - 1].value
|
|
912
|
+
: pendingCell.consensus;
|
|
913
|
+
|
|
914
|
+
this.setCellCore(
|
|
915
|
+
contents.row,
|
|
916
|
+
contents.col,
|
|
917
|
+
previous,
|
|
918
|
+
setMetadata.rowHandle,
|
|
919
|
+
setMetadata.colHandle,
|
|
920
|
+
true,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
default:
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
826
927
|
protected onDisconnect(): void {}
|
|
827
928
|
|
|
828
929
|
/**
|
|
@@ -854,11 +955,21 @@ export class SharedMatrix<T = any>
|
|
|
854
955
|
];
|
|
855
956
|
|
|
856
957
|
this.cells = SparseArray2D.load(cellData);
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
958
|
+
// back-compat: used -1 for disabled, also may not exist
|
|
959
|
+
const switchOpSeqNumber =
|
|
960
|
+
setCellLwwToFwwPolicySwitchOpSeqNumber === -1
|
|
961
|
+
? undefined
|
|
962
|
+
: (setCellLwwToFwwPolicySwitchOpSeqNumber ?? undefined);
|
|
963
|
+
this.fwwPolicy =
|
|
964
|
+
switchOpSeqNumber === undefined
|
|
965
|
+
? {
|
|
966
|
+
state: "off",
|
|
967
|
+
}
|
|
968
|
+
: {
|
|
969
|
+
state: "on",
|
|
970
|
+
switchOpSeqNumber,
|
|
971
|
+
cellLastWriteTracker: SparseArray2D.load(cellLastWriteTracker),
|
|
972
|
+
};
|
|
862
973
|
} catch (error) {
|
|
863
974
|
this.logger.sendErrorEvent({ eventName: "MatrixLoadFailed" }, error);
|
|
864
975
|
}
|
|
@@ -874,11 +985,11 @@ export class SharedMatrix<T = any>
|
|
|
874
985
|
message: ISequencedDocumentMessage,
|
|
875
986
|
): boolean {
|
|
876
987
|
assert(
|
|
877
|
-
this.
|
|
988
|
+
this.fwwPolicy.state === "on",
|
|
878
989
|
0x85f /* should be in Fww mode when calling this method */,
|
|
879
990
|
);
|
|
880
991
|
assert(message.clientId !== null, 0x860 /* clientId should not be null */);
|
|
881
|
-
const lastCellModificationDetails = this.cellLastWriteTracker.getCell(
|
|
992
|
+
const lastCellModificationDetails = this.fwwPolicy.cellLastWriteTracker.getCell(
|
|
882
993
|
rowHandle,
|
|
883
994
|
colHandle,
|
|
884
995
|
);
|
|
@@ -927,11 +1038,13 @@ export class SharedMatrix<T = any>
|
|
|
927
1038
|
);
|
|
928
1039
|
|
|
929
1040
|
const { row, col, value, fwwMode } = contents;
|
|
930
|
-
const isPreviousSetCellPolicyModeFWW =
|
|
931
|
-
this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1;
|
|
932
1041
|
// If this is the first op notifying us of the policy change, then set the policy change seq number.
|
|
933
|
-
if (
|
|
934
|
-
this.
|
|
1042
|
+
if (fwwMode === true && this.fwwPolicy.state !== "on") {
|
|
1043
|
+
this.fwwPolicy = {
|
|
1044
|
+
state: "on",
|
|
1045
|
+
switchOpSeqNumber: msg.sequenceNumber,
|
|
1046
|
+
cellLastWriteTracker: new SparseArray2D(),
|
|
1047
|
+
};
|
|
935
1048
|
}
|
|
936
1049
|
|
|
937
1050
|
assert(msg.clientId !== null, 0x861 /* clientId should not be null!! */);
|
|
@@ -939,81 +1052,86 @@ export class SharedMatrix<T = any>
|
|
|
939
1052
|
// We are receiving the ACK for a local pending set operation.
|
|
940
1053
|
const { rowHandle, colHandle, localSeq, rowsRef, colsRef } =
|
|
941
1054
|
localOpMetadata as ISetOpMetadata;
|
|
942
|
-
const isLatestPendingOp = this.isLatestPendingWrite(rowHandle, colHandle, localSeq);
|
|
943
1055
|
this.rows.removeLocalReferencePosition(rowsRef);
|
|
944
1056
|
this.cols.removeLocalReferencePosition(colsRef);
|
|
1057
|
+
|
|
1058
|
+
const pendingCell = this.pending.getCell(rowHandle, colHandle);
|
|
1059
|
+
const ackedChange = pendingCell?.local.shift();
|
|
1060
|
+
assert(ackedChange?.localSeq === localSeq, 0xbab /* must match */);
|
|
1061
|
+
if (pendingCell?.local.length === 0) {
|
|
1062
|
+
this.pending.setCell(rowHandle, colHandle, undefined);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
945
1065
|
// If policy is switched and cell should be modified too based on policy, then update the tracker.
|
|
946
1066
|
// If policy is not switched, then also update the tracker in case it is the latest.
|
|
947
1067
|
if (
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
(this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 && isLatestPendingOp)
|
|
1068
|
+
this.fwwPolicy.state === "on" &&
|
|
1069
|
+
this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)
|
|
951
1070
|
) {
|
|
952
|
-
|
|
1071
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1072
|
+
pendingCell!.consensus = ackedChange.value;
|
|
1073
|
+
this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
|
|
953
1074
|
seqNum: msg.sequenceNumber,
|
|
954
1075
|
clientId: msg.clientId,
|
|
955
1076
|
});
|
|
956
1077
|
}
|
|
957
|
-
|
|
958
|
-
if (isLatestPendingOp) {
|
|
959
|
-
this.pending.setCell(rowHandle, colHandle, undefined);
|
|
960
|
-
}
|
|
961
1078
|
} else {
|
|
962
1079
|
const adjustedRow = this.rows.adjustPosition(row, msg);
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
}
|
|
1080
|
+
const adjustedCol = this.cols.adjustPosition(col, msg);
|
|
1081
|
+
|
|
1082
|
+
const rowHandle = adjustedRow.handle;
|
|
1083
|
+
const colHandle = adjustedCol.handle;
|
|
1084
|
+
|
|
1085
|
+
assert(
|
|
1086
|
+
isHandleValid(rowHandle) && isHandleValid(colHandle),
|
|
1087
|
+
0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
|
|
1088
|
+
);
|
|
1089
|
+
const pendingCell = this.pending.getCell(rowHandle, colHandle);
|
|
1090
|
+
if (this.fwwPolicy.state === "on") {
|
|
1091
|
+
// If someone tried to Overwrite the cell value or first write on this cell or
|
|
1092
|
+
// same client tried to modify the cell or if the previous mode was LWW, then we need to still
|
|
1093
|
+
// overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
|
|
1094
|
+
if (this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)) {
|
|
1095
|
+
const previousValue = this.cells.getCell(rowHandle, colHandle);
|
|
1096
|
+
this.cells.setCell(rowHandle, colHandle, value);
|
|
1097
|
+
this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
|
|
1098
|
+
seqNum: msg.sequenceNumber,
|
|
1099
|
+
clientId: msg.clientId,
|
|
1100
|
+
});
|
|
1101
|
+
if (pendingCell !== undefined) {
|
|
1102
|
+
pendingCell.consensus = value;
|
|
1103
|
+
}
|
|
1104
|
+
if (adjustedRow.pos !== undefined && adjustedCol.pos !== undefined) {
|
|
1105
|
+
for (const consumer of this.consumers.values()) {
|
|
1106
|
+
consumer.cellsChanged(adjustedRow.pos, adjustedCol.pos, 1, 1, this);
|
|
1107
|
+
}
|
|
1108
|
+
// Check is there are any pending changes, which will be rejected. If so raise conflict.
|
|
1109
|
+
if (pendingCell !== undefined && pendingCell.local.length > 0) {
|
|
1110
|
+
// Don't reset the pending value yet, as there maybe more fww op from same client, so we want
|
|
1111
|
+
// to raise conflict event for that op also.
|
|
1112
|
+
this.emit(
|
|
1113
|
+
"conflict",
|
|
1114
|
+
row,
|
|
1115
|
+
col,
|
|
1116
|
+
value, // Current value
|
|
1117
|
+
previousValue, // Ignored local value
|
|
1118
|
+
this,
|
|
1119
|
+
);
|
|
1004
1120
|
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} else {
|
|
1124
|
+
if (pendingCell === undefined || pendingCell.local.length === 0) {
|
|
1125
|
+
// If there is a pending (unACKed) local write to the same cell, skip the current op
|
|
1126
|
+
// since it "happened before" the pending write.
|
|
1127
|
+
this.cells.setCell(rowHandle, colHandle, value);
|
|
1128
|
+
if (adjustedRow.pos !== undefined && adjustedCol.pos !== undefined) {
|
|
1013
1129
|
for (const consumer of this.consumers.values()) {
|
|
1014
|
-
consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
|
|
1130
|
+
consumer.cellsChanged(adjustedRow.pos, adjustedCol.pos, 1, 1, this);
|
|
1015
1131
|
}
|
|
1016
1132
|
}
|
|
1133
|
+
} else {
|
|
1134
|
+
pendingCell.consensus = value;
|
|
1017
1135
|
}
|
|
1018
1136
|
}
|
|
1019
1137
|
}
|
|
@@ -1051,7 +1169,12 @@ export class SharedMatrix<T = any>
|
|
|
1051
1169
|
for (const rowHandle of rowHandles) {
|
|
1052
1170
|
this.cells.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
|
|
1053
1171
|
this.pending.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
|
|
1054
|
-
this.
|
|
1172
|
+
if (this.fwwPolicy.state === "on") {
|
|
1173
|
+
this.fwwPolicy.cellLastWriteTracker?.clearRows(
|
|
1174
|
+
/* rowStart: */ rowHandle,
|
|
1175
|
+
/* rowCount: */ 1,
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1055
1178
|
}
|
|
1056
1179
|
};
|
|
1057
1180
|
|
|
@@ -1059,49 +1182,27 @@ export class SharedMatrix<T = any>
|
|
|
1059
1182
|
for (const colHandle of colHandles) {
|
|
1060
1183
|
this.cells.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
|
|
1061
1184
|
this.pending.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
|
|
1062
|
-
this.
|
|
1185
|
+
if (this.fwwPolicy.state === "on") {
|
|
1186
|
+
this.fwwPolicy.cellLastWriteTracker?.clearCols(
|
|
1187
|
+
/* colStart: */ colHandle,
|
|
1188
|
+
/* colCount: */ 1,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1063
1191
|
}
|
|
1064
1192
|
};
|
|
1065
1193
|
|
|
1066
1194
|
public switchSetCellPolicy(): void {
|
|
1067
|
-
if (this.
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1195
|
+
if (this.fwwPolicy.state === "off") {
|
|
1196
|
+
this.fwwPolicy = this.isAttached()
|
|
1197
|
+
? { state: "local" }
|
|
1198
|
+
: {
|
|
1199
|
+
state: "on",
|
|
1200
|
+
switchOpSeqNumber: 0,
|
|
1201
|
+
cellLastWriteTracker: new SparseArray2D(),
|
|
1202
|
+
};
|
|
1073
1203
|
}
|
|
1074
1204
|
}
|
|
1075
1205
|
|
|
1076
|
-
/**
|
|
1077
|
-
* Returns true if the latest pending write to the cell indicated by the given row/col handles
|
|
1078
|
-
* matches the given 'localSeq'.
|
|
1079
|
-
*
|
|
1080
|
-
* A return value of `true` indicates that there are no later local operations queued that will
|
|
1081
|
-
* clobber the write op at the given 'localSeq'. This includes later ops that overwrite the cell
|
|
1082
|
-
* with a different value as well as row/col removals that might recycled the given row/col handles.
|
|
1083
|
-
*/
|
|
1084
|
-
private isLatestPendingWrite(
|
|
1085
|
-
rowHandle: Handle,
|
|
1086
|
-
colHandle: Handle,
|
|
1087
|
-
localSeq: number,
|
|
1088
|
-
): boolean {
|
|
1089
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1090
|
-
const pendingLocalSeq = this.pending.getCell(rowHandle, colHandle)!;
|
|
1091
|
-
|
|
1092
|
-
// Note while we're awaiting the ACK for a local set, it's possible for the row/col to be
|
|
1093
|
-
// locally removed and the row/col handles recycled. If this happens, the pendingLocalSeq will
|
|
1094
|
-
// be 'undefined' or > 'localSeq'.
|
|
1095
|
-
assert(
|
|
1096
|
-
!(pendingLocalSeq < localSeq),
|
|
1097
|
-
0x023 /* "The 'localSeq' of pending write (if any) must be <= the localSeq of the currently processed op." */,
|
|
1098
|
-
);
|
|
1099
|
-
|
|
1100
|
-
// If this is the most recent write to the cell by the local client, the stored localSeq
|
|
1101
|
-
// will be an exact match for the given 'localSeq'.
|
|
1102
|
-
return pendingLocalSeq === localSeq;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
1206
|
public toString(): string {
|
|
1106
1207
|
let s = `client:${
|
|
1107
1208
|
this.runtime.clientId
|
package/src/packageVersion.ts
CHANGED