@fluidframework/matrix 2.41.0 → 2.43.0-343119
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/dist/handlecache.js +1 -1
- package/dist/handlecache.js.map +1 -1
- package/dist/matrix.d.ts +1 -9
- package/dist/matrix.d.ts.map +1 -1
- package/dist/matrix.js +118 -78
- 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 +36 -14
- package/dist/permutationvector.js.map +1 -1
- package/lib/handlecache.js +1 -1
- package/lib/handlecache.js.map +1 -1
- package/lib/matrix.d.ts +1 -9
- package/lib/matrix.d.ts.map +1 -1
- package/lib/matrix.js +118 -78
- 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 +36 -14
- package/lib/permutationvector.js.map +1 -1
- package/package.json +19 -18
- package/src/handlecache.ts +3 -3
- package/src/matrix.ts +170 -105
- package/src/packageVersion.ts +1 -1
- package/src/permutationvector.ts +47 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/matrix",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.43.0-343119",
|
|
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": "
|
|
85
|
-
"@fluidframework/core-interfaces": "
|
|
86
|
-
"@fluidframework/core-utils": "
|
|
87
|
-
"@fluidframework/datastore-definitions": "
|
|
88
|
-
"@fluidframework/driver-definitions": "
|
|
89
|
-
"@fluidframework/driver-utils": "
|
|
90
|
-
"@fluidframework/merge-tree": "
|
|
91
|
-
"@fluidframework/runtime-definitions": "
|
|
92
|
-
"@fluidframework/runtime-utils": "
|
|
93
|
-
"@fluidframework/shared-object-base": "
|
|
94
|
-
"@fluidframework/telemetry-utils": "
|
|
84
|
+
"@fluid-internal/client-utils": "2.43.0-343119",
|
|
85
|
+
"@fluidframework/core-interfaces": "2.43.0-343119",
|
|
86
|
+
"@fluidframework/core-utils": "2.43.0-343119",
|
|
87
|
+
"@fluidframework/datastore-definitions": "2.43.0-343119",
|
|
88
|
+
"@fluidframework/driver-definitions": "2.43.0-343119",
|
|
89
|
+
"@fluidframework/driver-utils": "2.43.0-343119",
|
|
90
|
+
"@fluidframework/merge-tree": "2.43.0-343119",
|
|
91
|
+
"@fluidframework/runtime-definitions": "2.43.0-343119",
|
|
92
|
+
"@fluidframework/runtime-utils": "2.43.0-343119",
|
|
93
|
+
"@fluidframework/shared-object-base": "2.43.0-343119",
|
|
94
|
+
"@fluidframework/telemetry-utils": "2.43.0-343119",
|
|
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": "
|
|
103
|
-
"@fluid-private/stochastic-test-utils": "
|
|
104
|
-
"@fluid-private/test-dds-utils": "
|
|
102
|
+
"@fluid-internal/mocha-test-setup": "2.43.0-343119",
|
|
103
|
+
"@fluid-private/stochastic-test-utils": "2.43.0-343119",
|
|
104
|
+
"@fluid-private/test-dds-utils": "2.43.0-343119",
|
|
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": "
|
|
109
|
+
"@fluidframework/container-definitions": "2.43.0-343119",
|
|
110
110
|
"@fluidframework/eslint-config-fluid": "^5.7.4",
|
|
111
|
-
"@fluidframework/matrix-previous": "npm:@fluidframework/matrix@2.
|
|
112
|
-
"@fluidframework/test-runtime-utils": "
|
|
111
|
+
"@fluidframework/matrix-previous": "npm:@fluidframework/matrix@2.42.0",
|
|
112
|
+
"@fluidframework/test-runtime-utils": "2.43.0-343119",
|
|
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/handlecache.ts
CHANGED
|
@@ -82,10 +82,10 @@ export class HandleCache implements IVectorConsumer<Handle> {
|
|
|
82
82
|
const { vector } = this;
|
|
83
83
|
|
|
84
84
|
for (let pos = start; pos < end; pos++) {
|
|
85
|
-
const { segment, offset } = vector.getContainingSegment(pos);
|
|
86
|
-
const asPerm = segment as PermutationSegment;
|
|
87
85
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
88
|
-
|
|
86
|
+
const { segment, offset } = vector.getContainingSegment(pos)!;
|
|
87
|
+
const asPerm = segment as PermutationSegment;
|
|
88
|
+
handles.push(asPerm.start + offset);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
package/src/matrix.ts
CHANGED
|
@@ -213,6 +213,22 @@ type FirstWriterWinsPolicy =
|
|
|
213
213
|
cellLastWriteTracker: SparseArray2D<CellLastWriteTrackerItem>;
|
|
214
214
|
};
|
|
215
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
|
+
|
|
216
232
|
/**
|
|
217
233
|
* A SharedMatrix holds a rectangular 2D array of values. Supported operations
|
|
218
234
|
* include setting values and inserting/removing rows and columns.
|
|
@@ -252,7 +268,7 @@ export class SharedMatrix<T = any>
|
|
|
252
268
|
private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
|
|
253
269
|
|
|
254
270
|
private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
|
|
255
|
-
private readonly pending = new SparseArray2D<
|
|
271
|
+
private readonly pending = new SparseArray2D<PendingCellChanges<T>>(); // Tracks pending writes.
|
|
256
272
|
|
|
257
273
|
private fwwPolicy: FirstWriterWinsPolicy = {
|
|
258
274
|
state: "off",
|
|
@@ -418,21 +434,22 @@ export class SharedMatrix<T = any>
|
|
|
418
434
|
value: MatrixItem<T>,
|
|
419
435
|
rowHandle = this.rows.getAllocatedHandle(row),
|
|
420
436
|
colHandle = this.cols.getAllocatedHandle(col),
|
|
437
|
+
rollback?: boolean,
|
|
421
438
|
): void {
|
|
422
439
|
this.protectAgainstReentrancy(() => {
|
|
423
|
-
|
|
424
|
-
let oldValue = this.cells.getCell(rowHandle, colHandle);
|
|
425
|
-
if (oldValue === null) {
|
|
426
|
-
oldValue = undefined;
|
|
427
|
-
}
|
|
440
|
+
const oldValue = this.cells.getCell(rowHandle, colHandle) ?? undefined;
|
|
428
441
|
|
|
442
|
+
if (this.undo !== undefined) {
|
|
429
443
|
this.undo.cellSet(rowHandle, colHandle, oldValue);
|
|
430
444
|
}
|
|
431
445
|
|
|
432
446
|
this.cells.setCell(rowHandle, colHandle, value);
|
|
433
447
|
|
|
434
|
-
if (this.isAttached()) {
|
|
435
|
-
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
|
+
}
|
|
436
453
|
}
|
|
437
454
|
|
|
438
455
|
// Avoid reentrancy by raising change notifications after the op is queued.
|
|
@@ -448,10 +465,7 @@ export class SharedMatrix<T = any>
|
|
|
448
465
|
localSeq: number,
|
|
449
466
|
): LocalReferencePosition {
|
|
450
467
|
const segoff = vector.getContainingSegment(pos, undefined, localSeq);
|
|
451
|
-
assert(
|
|
452
|
-
segoff.segment !== undefined && segoff.offset !== undefined,
|
|
453
|
-
0x8b3 /* expected valid position */,
|
|
454
|
-
);
|
|
468
|
+
assert(segoff !== undefined, 0x8b3 /* expected valid position */);
|
|
455
469
|
return vector.createLocalReferencePosition(
|
|
456
470
|
segoff.segment,
|
|
457
471
|
segoff.offset,
|
|
@@ -467,7 +481,7 @@ export class SharedMatrix<T = any>
|
|
|
467
481
|
rowHandle: Handle,
|
|
468
482
|
colHandle: Handle,
|
|
469
483
|
localSeq = this.nextLocalSeq(),
|
|
470
|
-
):
|
|
484
|
+
): PendingCellChanges<T> {
|
|
471
485
|
assert(
|
|
472
486
|
this.isAttached(),
|
|
473
487
|
0x1e2 /* "Caller must ensure 'isAttached()' before calling 'sendSetCellOp'." */,
|
|
@@ -493,7 +507,12 @@ export class SharedMatrix<T = any>
|
|
|
493
507
|
};
|
|
494
508
|
|
|
495
509
|
this.submitLocalMessage(op, metadata);
|
|
496
|
-
this.pending.
|
|
510
|
+
const pendingCell: PendingCellChanges<T> = this.pending.getCell(rowHandle, colHandle) ?? {
|
|
511
|
+
local: [],
|
|
512
|
+
};
|
|
513
|
+
pendingCell.local.push({ localSeq, value });
|
|
514
|
+
this.pending.setCell(rowHandle, colHandle, pendingCell);
|
|
515
|
+
return pendingCell;
|
|
497
516
|
}
|
|
498
517
|
|
|
499
518
|
/**
|
|
@@ -679,7 +698,16 @@ export class SharedMatrix<T = any>
|
|
|
679
698
|
| undefined
|
|
680
699
|
| number
|
|
681
700
|
| ReturnType<SparseArray2D<MatrixItem<T> | number>["snapshot"]>
|
|
682
|
-
)[] = [
|
|
701
|
+
)[] = [
|
|
702
|
+
this.cells.snapshot(),
|
|
703
|
+
/**
|
|
704
|
+
* we used to write this.pending.snapshot(). this should have never been done, as pending is only for local
|
|
705
|
+
* changes, and there should never be local changes in the summarizer. This was also never used on load
|
|
706
|
+
* as there is no way to understand a previous clients pending changes. so we just set this to a constant
|
|
707
|
+
* which matches an empty this.pending.snapshot() for back-compat in terms of the array length
|
|
708
|
+
*/
|
|
709
|
+
[undefined],
|
|
710
|
+
];
|
|
683
711
|
|
|
684
712
|
// Only need to store it in the snapshot if we have switched the policy already.
|
|
685
713
|
if (this.fwwPolicy.state === "on") {
|
|
@@ -806,31 +834,43 @@ export class SharedMatrix<T = any>
|
|
|
806
834
|
const col = this.rebasePosition(this.cols, colsRef, localSeq);
|
|
807
835
|
this.rows.removeLocalReferencePosition(rowsRef);
|
|
808
836
|
this.cols.removeLocalReferencePosition(colsRef);
|
|
809
|
-
|
|
810
|
-
|
|
837
|
+
|
|
838
|
+
const pendingCell = this.pending.getCell(rowHandle, colHandle);
|
|
839
|
+
assert(pendingCell !== undefined, 0xba4 /* local operation must have a pending array */);
|
|
840
|
+
const { local } = pendingCell;
|
|
841
|
+
assert(local !== undefined, 0xba5 /* local operation must have a pending array */);
|
|
842
|
+
const localSeqIndex = local.findIndex((p) => p.localSeq === localSeq);
|
|
843
|
+
assert(localSeqIndex >= 0, 0xba6 /* local operation must have a pending entry */);
|
|
844
|
+
const [change] = local.splice(localSeqIndex, 1);
|
|
845
|
+
assert(change.localSeq === localSeq, 0xba7 /* must match */);
|
|
846
|
+
|
|
847
|
+
if (
|
|
848
|
+
row !== undefined &&
|
|
849
|
+
col !== undefined &&
|
|
850
|
+
row >= 0 &&
|
|
851
|
+
col >= 0 && // If the mode is LWW, then send the op.
|
|
811
852
|
// Otherwise if the current mode is FWW and if we generated this op, after seeing the
|
|
812
853
|
// last set op, or it is the first set op for the cell, then regenerate the op,
|
|
813
854
|
// otherwise raise conflict. We want to check the current mode here and not that
|
|
814
855
|
// whether op was made in FWW or not.
|
|
815
|
-
|
|
816
|
-
this.fwwPolicy.state !== "on" ||
|
|
856
|
+
(this.fwwPolicy.state !== "on" ||
|
|
817
857
|
referenceSeqNumber >=
|
|
818
|
-
(this.fwwPolicy.cellLastWriteTracker.getCell(rowHandle, colHandle)?.seqNum ?? 0)
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
} else if (this.pending.getCell(rowHandle, colHandle) !== undefined) {
|
|
822
|
-
// Clear the pending changes if any as we are not sending the op.
|
|
823
|
-
this.pending.setCell(rowHandle, colHandle, undefined);
|
|
824
|
-
}
|
|
858
|
+
(this.fwwPolicy.cellLastWriteTracker.getCell(rowHandle, colHandle)?.seqNum ?? 0))
|
|
859
|
+
) {
|
|
860
|
+
this.sendSetCellOp(row, col, setOp.value, rowHandle, colHandle, localSeq);
|
|
825
861
|
}
|
|
826
862
|
} else {
|
|
827
863
|
switch (content.target) {
|
|
828
864
|
case SnapshotPath.cols: {
|
|
829
|
-
this.submitColMessage(
|
|
865
|
+
this.submitColMessage(
|
|
866
|
+
this.cols.regeneratePendingOp(content, localOpMetadata, false),
|
|
867
|
+
);
|
|
830
868
|
break;
|
|
831
869
|
}
|
|
832
870
|
case SnapshotPath.rows: {
|
|
833
|
-
this.submitRowMessage(
|
|
871
|
+
this.submitRowMessage(
|
|
872
|
+
this.rows.regeneratePendingOp(content, localOpMetadata, false),
|
|
873
|
+
);
|
|
834
874
|
break;
|
|
835
875
|
}
|
|
836
876
|
default: {
|
|
@@ -840,6 +880,47 @@ export class SharedMatrix<T = any>
|
|
|
840
880
|
}
|
|
841
881
|
}
|
|
842
882
|
|
|
883
|
+
protected rollback(content: unknown, localOpMetadata: unknown): void {
|
|
884
|
+
const contents = content as MatrixSetOrVectorOp<T>;
|
|
885
|
+
const target = contents.target;
|
|
886
|
+
|
|
887
|
+
switch (target) {
|
|
888
|
+
case SnapshotPath.cols: {
|
|
889
|
+
this.cols.rollback(content, localOpMetadata);
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
case SnapshotPath.rows: {
|
|
893
|
+
this.rows.rollback(content, localOpMetadata);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
case undefined: {
|
|
897
|
+
assert(contents.type === MatrixOp.set, 0xba8 /* only sets supported */);
|
|
898
|
+
const setMetadata = localOpMetadata as ISetOpMetadata;
|
|
899
|
+
|
|
900
|
+
const pendingCell = this.pending.getCell(setMetadata.rowHandle, setMetadata.colHandle);
|
|
901
|
+
assert(pendingCell !== undefined, 0xba9 /* must have pending */);
|
|
902
|
+
|
|
903
|
+
const change = pendingCell.local.pop();
|
|
904
|
+
assert(change?.localSeq === setMetadata.localSeq, 0xbaa /* must have change */);
|
|
905
|
+
|
|
906
|
+
const previous =
|
|
907
|
+
pendingCell.local.length > 0
|
|
908
|
+
? pendingCell.local[pendingCell.local.length - 1].value
|
|
909
|
+
: pendingCell.consensus;
|
|
910
|
+
|
|
911
|
+
this.setCellCore(
|
|
912
|
+
contents.row,
|
|
913
|
+
contents.col,
|
|
914
|
+
previous,
|
|
915
|
+
setMetadata.rowHandle,
|
|
916
|
+
setMetadata.colHandle,
|
|
917
|
+
true,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
default:
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
843
924
|
protected onDisconnect(): void {}
|
|
844
925
|
|
|
845
926
|
/**
|
|
@@ -968,73 +1049,86 @@ export class SharedMatrix<T = any>
|
|
|
968
1049
|
// We are receiving the ACK for a local pending set operation.
|
|
969
1050
|
const { rowHandle, colHandle, localSeq, rowsRef, colsRef } =
|
|
970
1051
|
localOpMetadata as ISetOpMetadata;
|
|
971
|
-
const isLatestPendingOp = this.isLatestPendingWrite(rowHandle, colHandle, localSeq);
|
|
972
1052
|
this.rows.removeLocalReferencePosition(rowsRef);
|
|
973
1053
|
this.cols.removeLocalReferencePosition(colsRef);
|
|
1054
|
+
|
|
1055
|
+
const pendingCell = this.pending.getCell(rowHandle, colHandle);
|
|
1056
|
+
const ackedChange = pendingCell?.local.shift();
|
|
1057
|
+
assert(ackedChange?.localSeq === localSeq, 0xbab /* must match */);
|
|
1058
|
+
if (pendingCell?.local.length === 0) {
|
|
1059
|
+
this.pending.setCell(rowHandle, colHandle, undefined);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
974
1062
|
// If policy is switched and cell should be modified too based on policy, then update the tracker.
|
|
975
1063
|
// If policy is not switched, then also update the tracker in case it is the latest.
|
|
976
1064
|
if (
|
|
977
1065
|
this.fwwPolicy.state === "on" &&
|
|
978
1066
|
this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)
|
|
979
1067
|
) {
|
|
1068
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1069
|
+
pendingCell!.consensus = ackedChange.value;
|
|
980
1070
|
this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
|
|
981
1071
|
seqNum: msg.sequenceNumber,
|
|
982
1072
|
clientId: msg.clientId,
|
|
983
1073
|
});
|
|
984
1074
|
}
|
|
985
|
-
|
|
986
|
-
if (isLatestPendingOp) {
|
|
987
|
-
this.pending.setCell(rowHandle, colHandle, undefined);
|
|
988
|
-
}
|
|
989
1075
|
} else {
|
|
990
1076
|
const adjustedRow = this.rows.adjustPosition(row, msg);
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1077
|
+
const adjustedCol = this.cols.adjustPosition(col, msg);
|
|
1078
|
+
|
|
1079
|
+
const rowHandle = adjustedRow.handle;
|
|
1080
|
+
const colHandle = adjustedCol.handle;
|
|
1081
|
+
|
|
1082
|
+
assert(
|
|
1083
|
+
isHandleValid(rowHandle) && isHandleValid(colHandle),
|
|
1084
|
+
0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
|
|
1085
|
+
);
|
|
1086
|
+
const pendingCell = this.pending.getCell(rowHandle, colHandle);
|
|
1087
|
+
if (this.fwwPolicy.state === "on") {
|
|
1088
|
+
// If someone tried to Overwrite the cell value or first write on this cell or
|
|
1089
|
+
// same client tried to modify the cell or if the previous mode was LWW, then we need to still
|
|
1090
|
+
// overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
|
|
1091
|
+
if (this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)) {
|
|
1092
|
+
const previousValue = this.cells.getCell(rowHandle, colHandle);
|
|
1093
|
+
this.cells.setCell(rowHandle, colHandle, value);
|
|
1094
|
+
this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
|
|
1095
|
+
seqNum: msg.sequenceNumber,
|
|
1096
|
+
clientId: msg.clientId,
|
|
1097
|
+
});
|
|
1098
|
+
if (pendingCell !== undefined) {
|
|
1099
|
+
pendingCell.consensus = value;
|
|
1100
|
+
}
|
|
1101
|
+
if (adjustedRow.pos !== undefined && adjustedCol.pos !== undefined) {
|
|
1102
|
+
for (const consumer of this.consumers.values()) {
|
|
1103
|
+
consumer.cellsChanged(adjustedRow.pos, adjustedCol.pos, 1, 1, this);
|
|
1104
|
+
}
|
|
1105
|
+
// Check is there are any pending changes, which will be rejected. If so raise conflict.
|
|
1106
|
+
if (pendingCell !== undefined && pendingCell.local.length > 0) {
|
|
1107
|
+
// Don't reset the pending value yet, as there maybe more fww op from same client, so we want
|
|
1108
|
+
// to raise conflict event for that op also.
|
|
1109
|
+
this.emit(
|
|
1110
|
+
"conflict",
|
|
1111
|
+
row,
|
|
1112
|
+
col,
|
|
1113
|
+
value, // Current value
|
|
1114
|
+
previousValue, // Ignored local value
|
|
1115
|
+
this,
|
|
1116
|
+
);
|
|
1029
1117
|
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
if (pendingCell === undefined || pendingCell.local.length === 0) {
|
|
1122
|
+
// If there is a pending (unACKed) local write to the same cell, skip the current op
|
|
1123
|
+
// since it "happened before" the pending write.
|
|
1124
|
+
this.cells.setCell(rowHandle, colHandle, value);
|
|
1125
|
+
if (adjustedRow.pos !== undefined && adjustedCol.pos !== undefined) {
|
|
1034
1126
|
for (const consumer of this.consumers.values()) {
|
|
1035
|
-
consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
|
|
1127
|
+
consumer.cellsChanged(adjustedRow.pos, adjustedCol.pos, 1, 1, this);
|
|
1036
1128
|
}
|
|
1037
1129
|
}
|
|
1130
|
+
} else {
|
|
1131
|
+
pendingCell.consensus = value;
|
|
1038
1132
|
}
|
|
1039
1133
|
}
|
|
1040
1134
|
}
|
|
@@ -1106,35 +1200,6 @@ export class SharedMatrix<T = any>
|
|
|
1106
1200
|
}
|
|
1107
1201
|
}
|
|
1108
1202
|
|
|
1109
|
-
/**
|
|
1110
|
-
* Returns true if the latest pending write to the cell indicated by the given row/col handles
|
|
1111
|
-
* matches the given 'localSeq'.
|
|
1112
|
-
*
|
|
1113
|
-
* A return value of `true` indicates that there are no later local operations queued that will
|
|
1114
|
-
* clobber the write op at the given 'localSeq'. This includes later ops that overwrite the cell
|
|
1115
|
-
* with a different value as well as row/col removals that might recycled the given row/col handles.
|
|
1116
|
-
*/
|
|
1117
|
-
private isLatestPendingWrite(
|
|
1118
|
-
rowHandle: Handle,
|
|
1119
|
-
colHandle: Handle,
|
|
1120
|
-
localSeq: number,
|
|
1121
|
-
): boolean {
|
|
1122
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1123
|
-
const pendingLocalSeq = this.pending.getCell(rowHandle, colHandle)!;
|
|
1124
|
-
|
|
1125
|
-
// Note while we're awaiting the ACK for a local set, it's possible for the row/col to be
|
|
1126
|
-
// locally removed and the row/col handles recycled. If this happens, the pendingLocalSeq will
|
|
1127
|
-
// be 'undefined' or > 'localSeq'.
|
|
1128
|
-
assert(
|
|
1129
|
-
!(pendingLocalSeq < localSeq),
|
|
1130
|
-
0x023 /* "The 'localSeq' of pending write (if any) must be <= the localSeq of the currently processed op." */,
|
|
1131
|
-
);
|
|
1132
|
-
|
|
1133
|
-
// If this is the most recent write to the cell by the local client, the stored localSeq
|
|
1134
|
-
// will be an exact match for the given 'localSeq'.
|
|
1135
|
-
return pendingLocalSeq === localSeq;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
1203
|
public toString(): string {
|
|
1139
1204
|
let s = `client:${
|
|
1140
1205
|
this.runtime.clientId
|
package/src/packageVersion.ts
CHANGED
package/src/permutationvector.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
IMergeTreeDeltaOpArgs,
|
|
19
19
|
IMergeTreeMaintenanceCallbackArgs,
|
|
20
20
|
ISegment,
|
|
21
|
-
ISegmentInternal,
|
|
22
21
|
MergeTreeDeltaType,
|
|
23
22
|
MergeTreeMaintenanceType,
|
|
24
23
|
segmentIsRemoved,
|
|
@@ -199,23 +198,51 @@ export class PermutationVector extends Client {
|
|
|
199
198
|
}
|
|
200
199
|
|
|
201
200
|
public adjustPosition(
|
|
202
|
-
|
|
203
|
-
op:
|
|
204
|
-
): number | undefined {
|
|
205
|
-
const
|
|
201
|
+
posToAdjust: number,
|
|
202
|
+
op: ISequencedDocumentMessage,
|
|
203
|
+
): { pos: number | undefined; handle: Handle } {
|
|
204
|
+
const segOff = this.getContainingSegment<PermutationSegment>(posToAdjust, {
|
|
206
205
|
referenceSequenceNumber: op.referenceSequenceNumber,
|
|
207
206
|
clientId: op.clientId,
|
|
208
207
|
});
|
|
209
208
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
assert(
|
|
210
|
+
segOff !== undefined,
|
|
211
|
+
0xbac /* segment must be available for operations in the collab window */,
|
|
212
|
+
);
|
|
213
|
+
const { segment, offset } = segOff;
|
|
214
|
+
|
|
215
|
+
if (segmentIsRemoved(segment)) {
|
|
216
|
+
// this case is tricky. the segment which the row or column data is remove
|
|
217
|
+
// but an op before that remove references a cell. we still want to apply
|
|
218
|
+
// the op, as the row/col could become active again in the case where
|
|
219
|
+
// the remove was local and it get's rolled back. so we allocate a handle
|
|
220
|
+
// for the row/col if not allocated, but don't put it in the cache
|
|
221
|
+
// as the cache can only contain live positions.
|
|
222
|
+
let handle = segment.start;
|
|
223
|
+
if (!isHandleValid(handle)) {
|
|
224
|
+
this.walkSegments(
|
|
225
|
+
(s) => {
|
|
226
|
+
const asPerm = s as PermutationSegment;
|
|
227
|
+
asPerm.start = handle = this.handleTable.allocate();
|
|
228
|
+
return true;
|
|
229
|
+
},
|
|
230
|
+
posToAdjust,
|
|
231
|
+
posToAdjust + 1,
|
|
232
|
+
/* accum: */ undefined,
|
|
233
|
+
/* splitRange: */ true,
|
|
234
|
+
op,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
216
237
|
|
|
217
|
-
|
|
218
|
-
|
|
238
|
+
return { handle, pos: undefined };
|
|
239
|
+
} else {
|
|
240
|
+
const pos = this.getPosition(segment) + offset;
|
|
241
|
+
return {
|
|
242
|
+
pos,
|
|
243
|
+
handle: this.getAllocatedHandle(pos),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
219
246
|
}
|
|
220
247
|
|
|
221
248
|
public handleToPosition(handle: Handle, localSeq = this.getCollabWindow().localSeq): number {
|
|
@@ -342,10 +369,12 @@ export class PermutationVector extends Client {
|
|
|
342
369
|
case MergeTreeDeltaType.INSERT: {
|
|
343
370
|
// Pass 1: Perform any internal maintenance first to avoid reentrancy.
|
|
344
371
|
for (const { segment, position } of ranges) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
372
|
+
if (opArgs.rollback !== true) {
|
|
373
|
+
// HACK: We need to include the allocated handle in the segment's JSON representation
|
|
374
|
+
// for snapshots, but need to ignore the remote client's handle allocations when
|
|
375
|
+
// processing remote ops.
|
|
376
|
+
segment.reset();
|
|
377
|
+
}
|
|
349
378
|
|
|
350
379
|
this.handleCache.itemsChanged(
|
|
351
380
|
position,
|
|
@@ -364,7 +393,6 @@ export class PermutationVector extends Client {
|
|
|
364
393
|
}
|
|
365
394
|
break;
|
|
366
395
|
}
|
|
367
|
-
|
|
368
396
|
case MergeTreeDeltaType.REMOVE: {
|
|
369
397
|
// Pass 1: Perform any internal maintenance first to avoid reentrancy.
|
|
370
398
|
for (const { segment, position } of ranges) {
|
|
@@ -385,7 +413,6 @@ export class PermutationVector extends Client {
|
|
|
385
413
|
}
|
|
386
414
|
break;
|
|
387
415
|
}
|
|
388
|
-
|
|
389
416
|
default: {
|
|
390
417
|
throw new Error("Unhandled MergeTreeDeltaType");
|
|
391
418
|
}
|
|
@@ -445,7 +472,7 @@ export function reinsertSegmentIntoVector(
|
|
|
445
472
|
|
|
446
473
|
// (Re)insert the removed number of rows at the original position.
|
|
447
474
|
const op = vector.insertSegmentLocal(pos, original);
|
|
448
|
-
const inserted = vector.getContainingSegment(pos)
|
|
475
|
+
const inserted = vector.getContainingSegment(pos)?.segment as PermutationSegment;
|
|
449
476
|
|
|
450
477
|
// we reuse the original handle here
|
|
451
478
|
// so if cells exist, they can be found, and re-inserted
|