@@ -205,6 +205,15 @@ export interface ISharedMatrix<T = any>
205
205
switchSetCellPolicy ( ) : void ;
206
206
}
207
207
208
+ type FirstWriterWinsPolicy =
209
+ | { state : "off" }
210
+ | { state : "local" }
211
+ | {
212
+ state : "on" ;
213
+ switchOpSeqNumber : number ;
214
+ cellLastWriteTracker : SparseArray2D < CellLastWriteTrackerItem > ;
215
+ } ;
216
+
208
217
/**
209
218
* A SharedMatrix holds a rectangular 2D array of values. Supported operations
210
219
* include setting values and inserting/removing rows and columns.
@@ -245,10 +254,10 @@ export class SharedMatrix<T = any>
245
254
246
255
private cells = new SparseArray2D < MatrixItem < T > > ( ) ; // Stores cell values.
247
256
private readonly pending = new SparseArray2D < number [ ] > ( ) ; // Tracks pending writes.
248
- private cellLastWriteTracker = new SparseArray2D < CellLastWriteTrackerItem > ( ) ; // Tracks last writes sequence number and clientId in a cell.
249
- // Tracks the seq number of Op at which policy switch happens from Last Write Win to First Write Win.
250
- private setCellLwwToFwwPolicySwitchOpSeqNumber : number ;
251
- private userSwitchedSetCellPolicy = false ; // Set to true when the user calls switchPolicy.
257
+
258
+ private fwwPolicy : FirstWriterWinsPolicy = {
259
+ state : "off" ,
260
+ } ;
252
261
253
262
// Used to track if there is any reentrancy in setCell code.
254
263
private reentrantCount : number = 0 ;
@@ -268,7 +277,6 @@ export class SharedMatrix<T = any>
268
277
) {
269
278
super ( id , runtime , attributes , "fluid_matrix_" ) ;
270
279
271
- this . setCellLwwToFwwPolicySwitchOpSeqNumber = - 1 ;
272
280
this . rows = new PermutationVector (
273
281
SnapshotPath . rows ,
274
282
this . logger ,
@@ -334,7 +342,7 @@ export class SharedMatrix<T = any>
334
342
}
335
343
336
344
public isSetCellConflictResolutionPolicyFWW ( ) : boolean {
337
- return this . setCellLwwToFwwPolicySwitchOpSeqNumber > - 1 || this . userSwitchedSetCellPolicy ;
345
+ return this . fwwPolicy . state !== "off" ;
338
346
}
339
347
340
348
public getCell ( row : number , col : number ) : MatrixItem < T > {
@@ -472,8 +480,7 @@ export class SharedMatrix<T = any>
472
480
row,
473
481
col,
474
482
value,
475
- fwwMode :
476
- this . userSwitchedSetCellPolicy || this . setCellLwwToFwwPolicySwitchOpSeqNumber > - 1 ,
483
+ fwwMode : this . fwwPolicy . state !== "off" ,
477
484
} ;
478
485
479
486
const rowsRef = this . createOpMetadataLocalRef ( this . rows , row , localSeq ) ;
@@ -673,15 +680,29 @@ export class SharedMatrix<T = any>
673
680
SnapshotPath . cols ,
674
681
this . cols . summarize ( this . runtime , this . handle , serializer ) ,
675
682
) ;
676
- const artifactsToSummarize = [
677
- this . cells . snapshot ( ) ,
678
- this . pending . snapshot ( ) ,
679
- this . setCellLwwToFwwPolicySwitchOpSeqNumber ,
680
- ] ;
683
+ const artifactsToSummarize : (
684
+ | undefined
685
+ | number
686
+ | ReturnType < SparseArray2D < MatrixItem < T > | number > [ "snapshot" ] >
687
+ ) [ ] = [ this . cells . snapshot ( ) , this . pending . snapshot ( ) ] ;
681
688
682
689
// Only need to store it in the snapshot if we have switched the policy already.
683
- if ( this . setCellLwwToFwwPolicySwitchOpSeqNumber > - 1 ) {
684
- artifactsToSummarize . push ( this . cellLastWriteTracker . snapshot ( ) ) ;
690
+ if ( this . fwwPolicy . state === "on" ) {
691
+ artifactsToSummarize . push (
692
+ this . fwwPolicy . switchOpSeqNumber ,
693
+ this . fwwPolicy . cellLastWriteTracker . snapshot ( ) ,
694
+ ) ;
695
+ } else {
696
+ // back-compat: used -1 for disabled
697
+ artifactsToSummarize . push (
698
+ - 1 ,
699
+ /*
700
+ * we should set undefined in place of cellLastWriteTracker to ensure the number of array entries is consistent.
701
+ * Doing that currently breaks snapshot tests. Its is probably fine, but if new elements are ever added, we need
702
+ * ensure undefined is also set.
703
+ */
704
+ // undefined
705
+ ) ;
685
706
}
686
707
builder . addBlob (
687
708
SnapshotPath . cells ,
@@ -798,10 +819,6 @@ export class SharedMatrix<T = any>
798
819
this . rows . removeLocalReferencePosition ( rowsRef ) ;
799
820
this . cols . removeLocalReferencePosition ( colsRef ) ;
800
821
if ( row !== undefined && col !== undefined && row >= 0 && col >= 0 ) {
801
- const lastCellModificationDetails = this . cellLastWriteTracker . getCell (
802
- rowHandle ,
803
- colHandle ,
804
- ) ;
805
822
const pending = this . pending . getCell ( rowHandle , colHandle ) ;
806
823
assert ( pending !== undefined , "local operation must have a pending array" ) ;
807
824
const localSeqIndex = pending . indexOf ( localSeq ) ;
@@ -813,9 +830,9 @@ export class SharedMatrix<T = any>
813
830
// otherwise raise conflict. We want to check the current mode here and not that
814
831
// whether op was made in FWW or not.
815
832
if (
816
- this . setCellLwwToFwwPolicySwitchOpSeqNumber === - 1 ||
817
- lastCellModificationDetails === undefined ||
818
- referenceSeqNumber >= lastCellModificationDetails . seqNum
833
+ this . fwwPolicy . state !== "on" ||
834
+ referenceSeqNumber >=
835
+ ( this . fwwPolicy . cellLastWriteTracker . getCell ( rowHandle , colHandle ) ? .seqNum ?? 0 )
819
836
) {
820
837
this . sendSetCellOp (
821
838
row ,
@@ -876,11 +893,21 @@ export class SharedMatrix<T = any>
876
893
] ;
877
894
878
895
this . cells = SparseArray2D . load ( cellData ) ;
879
- this . setCellLwwToFwwPolicySwitchOpSeqNumber =
880
- setCellLwwToFwwPolicySwitchOpSeqNumber ?? - 1 ;
881
- if ( cellLastWriteTracker !== undefined ) {
882
- this . cellLastWriteTracker = SparseArray2D . load ( cellLastWriteTracker ) ;
883
- }
896
+ // back-compat: used -1 for disabled, also may not exist
897
+ const switchOpSeqNumber =
898
+ setCellLwwToFwwPolicySwitchOpSeqNumber === - 1
899
+ ? undefined
900
+ : ( setCellLwwToFwwPolicySwitchOpSeqNumber ?? undefined ) ;
901
+ this . fwwPolicy =
902
+ switchOpSeqNumber === undefined
903
+ ? {
904
+ state : "off" ,
905
+ }
906
+ : {
907
+ state : "on" ,
908
+ switchOpSeqNumber,
909
+ cellLastWriteTracker : SparseArray2D . load ( cellLastWriteTracker ) ,
910
+ } ;
884
911
} catch ( error ) {
885
912
this . logger . sendErrorEvent ( { eventName : "MatrixLoadFailed" } , error ) ;
886
913
}
@@ -896,11 +923,11 @@ export class SharedMatrix<T = any>
896
923
message : ISequencedDocumentMessage ,
897
924
) : boolean {
898
925
assert (
899
- this . setCellLwwToFwwPolicySwitchOpSeqNumber > - 1 ,
926
+ this . fwwPolicy . state === "on" ,
900
927
0x85f /* should be in Fww mode when calling this method */ ,
901
928
) ;
902
929
assert ( message . clientId !== null , 0x860 /* clientId should not be null */ ) ;
903
- const lastCellModificationDetails = this . cellLastWriteTracker . getCell (
930
+ const lastCellModificationDetails = this . fwwPolicy . cellLastWriteTracker . getCell (
904
931
rowHandle ,
905
932
colHandle ,
906
933
) ;
@@ -949,11 +976,13 @@ export class SharedMatrix<T = any>
949
976
) ;
950
977
951
978
const { row, col, value, fwwMode } = contents ;
952
- const isPreviousSetCellPolicyModeFWW =
953
- this . setCellLwwToFwwPolicySwitchOpSeqNumber > - 1 ;
954
979
// If this is the first op notifying us of the policy change, then set the policy change seq number.
955
- if ( this . setCellLwwToFwwPolicySwitchOpSeqNumber === - 1 && fwwMode === true ) {
956
- this . setCellLwwToFwwPolicySwitchOpSeqNumber = msg . sequenceNumber ;
980
+ if ( fwwMode === true && this . fwwPolicy . state !== "on" ) {
981
+ this . fwwPolicy = {
982
+ state : "on" ,
983
+ switchOpSeqNumber : msg . sequenceNumber ,
984
+ cellLastWriteTracker : new SparseArray2D ( ) ,
985
+ } ;
957
986
}
958
987
959
988
assert ( msg . clientId !== null , 0x861 /* clientId should not be null!! */ ) ;
@@ -971,17 +1000,15 @@ export class SharedMatrix<T = any>
971
1000
972
1001
// if there are no more pending entries, the current must be the latest
973
1002
//
974
- const isLatestPendingOp = pending . length === 0 ;
975
1003
this . rows . removeLocalReferencePosition ( rowsRef ) ;
976
1004
this . cols . removeLocalReferencePosition ( colsRef ) ;
977
1005
// If policy is switched and cell should be modified too based on policy, then update the tracker.
978
1006
// If policy is not switched, then also update the tracker in case it is the latest.
979
1007
if (
980
- ( this . setCellLwwToFwwPolicySwitchOpSeqNumber > - 1 &&
981
- this . shouldSetCellBasedOnFWW ( rowHandle , colHandle , msg ) ) ||
982
- ( this . setCellLwwToFwwPolicySwitchOpSeqNumber === - 1 && isLatestPendingOp )
1008
+ this . fwwPolicy . state === "on" &&
1009
+ this . shouldSetCellBasedOnFWW ( rowHandle , colHandle , msg )
983
1010
) {
984
- this . cellLastWriteTracker . setCell ( rowHandle , colHandle , {
1011
+ this . fwwPolicy . cellLastWriteTracker . setCell ( rowHandle , colHandle , {
985
1012
seqNum : msg . sequenceNumber ,
986
1013
clientId : msg . clientId ,
987
1014
} ) ;
@@ -999,17 +1026,14 @@ export class SharedMatrix<T = any>
999
1026
isHandleValid ( rowHandle ) && isHandleValid ( colHandle ) ,
1000
1027
0x022 /* "SharedMatrix row and/or col handles are invalid!" */ ,
1001
1028
) ;
1002
- if ( this . setCellLwwToFwwPolicySwitchOpSeqNumber > - 1 ) {
1029
+ if ( this . fwwPolicy . state === "on" ) {
1003
1030
// If someone tried to Overwrite the cell value or first write on this cell or
1004
1031
// same client tried to modify the cell or if the previous mode was LWW, then we need to still
1005
1032
// overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
1006
- if (
1007
- ! isPreviousSetCellPolicyModeFWW ||
1008
- this . shouldSetCellBasedOnFWW ( rowHandle , colHandle , msg )
1009
- ) {
1033
+ if ( this . shouldSetCellBasedOnFWW ( rowHandle , colHandle , msg ) ) {
1010
1034
const previousValue = this . cells . getCell ( rowHandle , colHandle ) ;
1011
1035
this . cells . setCell ( rowHandle , colHandle , value ) ;
1012
- this . cellLastWriteTracker . setCell ( rowHandle , colHandle , {
1036
+ this . fwwPolicy . cellLastWriteTracker . setCell ( rowHandle , colHandle , {
1013
1037
seqNum : msg . sequenceNumber ,
1014
1038
clientId : msg . clientId ,
1015
1039
} ) ;
@@ -1034,10 +1058,6 @@ export class SharedMatrix<T = any>
1034
1058
// If there is a pending (unACKed) local write to the same cell, skip the current op
1035
1059
// since it "happened before" the pending write.
1036
1060
this . cells . setCell ( rowHandle , colHandle , value ) ;
1037
- this . cellLastWriteTracker . setCell ( rowHandle , colHandle , {
1038
- seqNum : msg . sequenceNumber ,
1039
- clientId : msg . clientId ,
1040
- } ) ;
1041
1061
for ( const consumer of this . consumers . values ( ) ) {
1042
1062
consumer . cellsChanged ( adjustedRow , adjustedCol , 1 , 1 , this ) ;
1043
1063
}
@@ -1115,25 +1135,37 @@ export class SharedMatrix<T = any>
1115
1135
for ( const rowHandle of rowHandles ) {
1116
1136
this . cells . clearRows ( /* rowStart: */ rowHandle , /* rowCount: */ 1 ) ;
1117
1137
this . pending . clearRows ( /* rowStart: */ rowHandle , /* rowCount: */ 1 ) ;
1118
- this . cellLastWriteTracker . clearRows ( /* rowStart: */ rowHandle , /* rowCount: */ 1 ) ;
1138
+ if ( this . fwwPolicy . state === "on" ) {
1139
+ this . fwwPolicy . cellLastWriteTracker ?. clearRows (
1140
+ /* rowStart: */ rowHandle ,
1141
+ /* rowCount: */ 1 ,
1142
+ ) ;
1143
+ }
1119
1144
}
1120
1145
} ;
1121
1146
1122
1147
private readonly onColHandlesRecycled = ( colHandles : Handle [ ] ) : void => {
1123
1148
for ( const colHandle of colHandles ) {
1124
1149
this . cells . clearCols ( /* colStart: */ colHandle , /* colCount: */ 1 ) ;
1125
1150
this . pending . clearCols ( /* colStart: */ colHandle , /* colCount: */ 1 ) ;
1126
- this . cellLastWriteTracker . clearCols ( /* colStart: */ colHandle , /* colCount: */ 1 ) ;
1151
+ if ( this . fwwPolicy . state === "on" ) {
1152
+ this . fwwPolicy . cellLastWriteTracker ?. clearCols (
1153
+ /* colStart: */ colHandle ,
1154
+ /* colCount: */ 1 ,
1155
+ ) ;
1156
+ }
1127
1157
}
1128
1158
} ;
1129
1159
1130
1160
public switchSetCellPolicy ( ) : void {
1131
- if ( this . setCellLwwToFwwPolicySwitchOpSeqNumber === - 1 ) {
1132
- if ( this . isAttached ( ) ) {
1133
- this . userSwitchedSetCellPolicy = true ;
1134
- } else {
1135
- this . setCellLwwToFwwPolicySwitchOpSeqNumber = 0 ;
1136
- }
1161
+ if ( this . fwwPolicy . state === "off" ) {
1162
+ this . fwwPolicy = this . isAttached ( )
1163
+ ? { state : "local" }
1164
+ : {
1165
+ state : "on" ,
1166
+ switchOpSeqNumber : 0 ,
1167
+ cellLastWriteTracker : new SparseArray2D ( ) ,
1168
+ } ;
1137
1169
}
1138
1170
}
1139
1171
0 commit comments