@fluidframework/map 1.1.0 → 1.2.0-77818
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/dist/directory.d.ts +5 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +288 -79
- package/dist/directory.js.map +1 -1
- package/dist/interfaces.d.ts +29 -14
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.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/lib/directory.d.ts +5 -0
- package/lib/directory.d.ts.map +1 -1
- package/lib/directory.js +288 -79
- package/lib/directory.js.map +1 -1
- package/lib/interfaces.d.ts +29 -14
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/interfaces.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/package.json +13 -13
- package/src/directory.ts +348 -85
- package/src/interfaces.ts +29 -14
- package/src/packageVersion.ts +1 -1
package/src/directory.ts
CHANGED
|
@@ -629,6 +629,18 @@ export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implem
|
|
|
629
629
|
}
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
+
/**
|
|
633
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
|
|
634
|
+
* @internal
|
|
635
|
+
*/
|
|
636
|
+
protected rollback(content: any, localOpMetadata: unknown) {
|
|
637
|
+
const op: IDirectoryOperation = content as IDirectoryOperation;
|
|
638
|
+
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
639
|
+
if (subdir) {
|
|
640
|
+
subdir.rollback(op, localOpMetadata);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
632
644
|
/**
|
|
633
645
|
* Converts the given relative path to absolute against the root.
|
|
634
646
|
* @param relativePath - The path to convert
|
|
@@ -675,8 +687,7 @@ export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implem
|
|
|
675
687
|
submit: (op: IDirectoryClearOperation, localOpMetadata: unknown) => {
|
|
676
688
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
677
689
|
if (subdir) {
|
|
678
|
-
|
|
679
|
-
subdir.submitClearMessage(op);
|
|
690
|
+
subdir.resubmitClearMessage(op, localOpMetadata);
|
|
680
691
|
}
|
|
681
692
|
},
|
|
682
693
|
},
|
|
@@ -693,8 +704,7 @@ export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implem
|
|
|
693
704
|
submit: (op: IDirectoryDeleteOperation, localOpMetadata: unknown) => {
|
|
694
705
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
695
706
|
if (subdir) {
|
|
696
|
-
|
|
697
|
-
subdir.submitKeyMessage(op);
|
|
707
|
+
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
698
708
|
}
|
|
699
709
|
},
|
|
700
710
|
},
|
|
@@ -712,8 +722,7 @@ export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implem
|
|
|
712
722
|
submit: (op: IDirectorySetOperation, localOpMetadata: unknown) => {
|
|
713
723
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
714
724
|
if (subdir) {
|
|
715
|
-
|
|
716
|
-
subdir.submitKeyMessage(op);
|
|
725
|
+
subdir.resubmitKeyMessage(op, localOpMetadata);
|
|
717
726
|
}
|
|
718
727
|
},
|
|
719
728
|
},
|
|
@@ -732,7 +741,7 @@ export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implem
|
|
|
732
741
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
733
742
|
if (parentSubdir) {
|
|
734
743
|
// We don't reuse the metadata but send a new one on each submit.
|
|
735
|
-
parentSubdir.
|
|
744
|
+
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
736
745
|
}
|
|
737
746
|
},
|
|
738
747
|
},
|
|
@@ -751,7 +760,7 @@ export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implem
|
|
|
751
760
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
752
761
|
if (parentSubdir) {
|
|
753
762
|
// We don't reuse the metadata but send a new one on each submit.
|
|
754
|
-
parentSubdir.
|
|
763
|
+
parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
755
764
|
}
|
|
756
765
|
},
|
|
757
766
|
},
|
|
@@ -832,15 +841,64 @@ export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implem
|
|
|
832
841
|
}
|
|
833
842
|
}
|
|
834
843
|
|
|
844
|
+
interface IKeyEditLocalOpMetadata {
|
|
845
|
+
type: "edit";
|
|
846
|
+
pendingMessageId: number;
|
|
847
|
+
previousValue: ILocalValue | undefined;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
interface IClearLocalOpMetadata {
|
|
851
|
+
type: "clear";
|
|
852
|
+
pendingMessageId: number;
|
|
853
|
+
previousStorage: Map<string, ILocalValue>;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
interface ICreateSubDirLocalOpMetadata {
|
|
857
|
+
type: "createSubDir";
|
|
858
|
+
pendingMessageId: number;
|
|
859
|
+
previouslyExisted: boolean;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
interface IDeleteSubDirLocalOpMetadata {
|
|
863
|
+
type: "deleteSubDir";
|
|
864
|
+
pendingMessageId: number;
|
|
865
|
+
subDirectory: SubDirectory | undefined;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
type SubDirLocalOpMetadata = ICreateSubDirLocalOpMetadata | IDeleteSubDirLocalOpMetadata;
|
|
869
|
+
type DirectoryLocalOpMetadata = IClearLocalOpMetadata | IKeyEditLocalOpMetadata | SubDirLocalOpMetadata;
|
|
870
|
+
|
|
871
|
+
function isKeyEditLocalOpMetadata(metadata: any): metadata is IKeyEditLocalOpMetadata {
|
|
872
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" && metadata.type === "edit";
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function isClearLocalOpMetadata(metadata: any): metadata is IClearLocalOpMetadata {
|
|
876
|
+
return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number" &&
|
|
877
|
+
typeof metadata.previousStorage === "object";
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function isSubDirLocalOpMetadata(metadata: any): metadata is SubDirLocalOpMetadata {
|
|
881
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
|
|
882
|
+
((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
|
|
883
|
+
metadata.type === "deleteSubDir");
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function isDirectoryLocalOpMetadata(metadata: any): metadata is DirectoryLocalOpMetadata {
|
|
887
|
+
return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
|
|
888
|
+
(metadata.type === "edit" || metadata.type === "deleteSubDir" ||
|
|
889
|
+
(metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
|
|
890
|
+
(metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"));
|
|
891
|
+
}
|
|
892
|
+
|
|
835
893
|
/**
|
|
836
894
|
* Node of the directory tree.
|
|
837
895
|
* @sealed
|
|
838
896
|
*/
|
|
839
897
|
class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirectory {
|
|
840
898
|
/**
|
|
841
|
-
* Tells if the sub directory is
|
|
899
|
+
* Tells if the sub directory is deleted or not.
|
|
842
900
|
*/
|
|
843
|
-
private
|
|
901
|
+
private _deleted = false;
|
|
844
902
|
|
|
845
903
|
/**
|
|
846
904
|
* String representation for the class.
|
|
@@ -860,12 +918,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
860
918
|
/**
|
|
861
919
|
* Keys that have been modified locally but not yet ack'd from the server.
|
|
862
920
|
*/
|
|
863
|
-
private readonly pendingKeys: Map<string, number> = new Map();
|
|
921
|
+
private readonly pendingKeys: Map<string, number[]> = new Map();
|
|
864
922
|
|
|
865
923
|
/**
|
|
866
924
|
* Subdirectories that have been modified locally but not yet ack'd from the server.
|
|
867
925
|
*/
|
|
868
|
-
private readonly pendingSubDirectories: Map<string, number> = new Map();
|
|
926
|
+
private readonly pendingSubDirectories: Map<string, number[]> = new Map();
|
|
869
927
|
|
|
870
928
|
/**
|
|
871
929
|
* This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
|
|
@@ -873,10 +931,9 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
873
931
|
private pendingMessageId: number = -1;
|
|
874
932
|
|
|
875
933
|
/**
|
|
876
|
-
*
|
|
877
|
-
* of that clear operation. Otherwise, is -1.
|
|
934
|
+
* The pending ids of any clears that have been performed locally but not yet ack'd from the server
|
|
878
935
|
*/
|
|
879
|
-
private
|
|
936
|
+
private readonly pendingClearMessageIds: number[] = [];
|
|
880
937
|
|
|
881
938
|
/**
|
|
882
939
|
* Constructor.
|
|
@@ -895,16 +952,23 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
895
952
|
}
|
|
896
953
|
|
|
897
954
|
public dispose(error?: Error): void {
|
|
898
|
-
this.
|
|
955
|
+
this._deleted = true;
|
|
899
956
|
this.emit("disposed", this);
|
|
900
957
|
}
|
|
901
958
|
|
|
959
|
+
/**
|
|
960
|
+
* Unmark the deleted property when rolling back delete.
|
|
961
|
+
*/
|
|
962
|
+
private undispose(): void {
|
|
963
|
+
this._deleted = false;
|
|
964
|
+
}
|
|
965
|
+
|
|
902
966
|
public get disposed(): boolean {
|
|
903
|
-
return this.
|
|
967
|
+
return this._deleted;
|
|
904
968
|
}
|
|
905
969
|
|
|
906
970
|
private throwIfDisposed() {
|
|
907
|
-
if (this.
|
|
971
|
+
if (this._deleted) {
|
|
908
972
|
throw new UsageError("Cannot access Disposed subDirectory");
|
|
909
973
|
}
|
|
910
974
|
}
|
|
@@ -945,7 +1009,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
945
1009
|
this.directory.handle);
|
|
946
1010
|
|
|
947
1011
|
// Set the value locally.
|
|
948
|
-
this.setCore(
|
|
1012
|
+
const previousValue = this.setCore(
|
|
949
1013
|
key,
|
|
950
1014
|
localValue,
|
|
951
1015
|
true,
|
|
@@ -962,7 +1026,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
962
1026
|
type: "set",
|
|
963
1027
|
value: serializableValue,
|
|
964
1028
|
};
|
|
965
|
-
this.submitKeyMessage(op);
|
|
1029
|
+
this.submitKeyMessage(op, previousValue);
|
|
966
1030
|
return this;
|
|
967
1031
|
}
|
|
968
1032
|
|
|
@@ -988,7 +1052,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
988
1052
|
}
|
|
989
1053
|
|
|
990
1054
|
// Create the sub directory locally first.
|
|
991
|
-
this.createSubDirectoryCore(subdirName, true);
|
|
1055
|
+
const isNew = this.createSubDirectoryCore(subdirName, true);
|
|
992
1056
|
|
|
993
1057
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
994
1058
|
const subDir: IDirectory = this._subdirectories.get(subdirName)!;
|
|
@@ -1003,7 +1067,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1003
1067
|
subdirName,
|
|
1004
1068
|
type: "createSubDirectory",
|
|
1005
1069
|
};
|
|
1006
|
-
this.
|
|
1070
|
+
this.submitCreateSubDirectoryMessage(op, !isNew);
|
|
1007
1071
|
|
|
1008
1072
|
return subDir;
|
|
1009
1073
|
}
|
|
@@ -1030,11 +1094,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1030
1094
|
public deleteSubDirectory(subdirName: string): boolean {
|
|
1031
1095
|
this.throwIfDisposed();
|
|
1032
1096
|
// Delete the sub directory locally first.
|
|
1033
|
-
const
|
|
1097
|
+
const subDir = this.deleteSubDirectoryCore(subdirName, true);
|
|
1034
1098
|
|
|
1035
1099
|
// If we are not attached, don't submit the op.
|
|
1036
1100
|
if (!this.directory.isAttached()) {
|
|
1037
|
-
return
|
|
1101
|
+
return subDir !== undefined;
|
|
1038
1102
|
}
|
|
1039
1103
|
|
|
1040
1104
|
const op: IDirectoryDeleteSubDirectoryOperation = {
|
|
@@ -1043,8 +1107,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1043
1107
|
type: "deleteSubDirectory",
|
|
1044
1108
|
};
|
|
1045
1109
|
|
|
1046
|
-
this.
|
|
1047
|
-
return
|
|
1110
|
+
this.submitDeleteSubDirectoryMessage(op, subDir);
|
|
1111
|
+
return subDir !== undefined;
|
|
1048
1112
|
}
|
|
1049
1113
|
|
|
1050
1114
|
/**
|
|
@@ -1071,11 +1135,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1071
1135
|
public delete(key: string): boolean {
|
|
1072
1136
|
this.throwIfDisposed();
|
|
1073
1137
|
// Delete the key locally first.
|
|
1074
|
-
const
|
|
1138
|
+
const previousValue = this.deleteCore(key, true);
|
|
1075
1139
|
|
|
1076
1140
|
// If we are not attached, don't submit the op.
|
|
1077
1141
|
if (!this.directory.isAttached()) {
|
|
1078
|
-
return
|
|
1142
|
+
return previousValue !== undefined;
|
|
1079
1143
|
}
|
|
1080
1144
|
|
|
1081
1145
|
const op: IDirectoryDeleteOperation = {
|
|
@@ -1084,8 +1148,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1084
1148
|
type: "delete",
|
|
1085
1149
|
};
|
|
1086
1150
|
|
|
1087
|
-
this.submitKeyMessage(op);
|
|
1088
|
-
return
|
|
1151
|
+
this.submitKeyMessage(op, previousValue);
|
|
1152
|
+
return previousValue !== undefined;
|
|
1089
1153
|
}
|
|
1090
1154
|
|
|
1091
1155
|
/**
|
|
@@ -1093,19 +1157,20 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1093
1157
|
*/
|
|
1094
1158
|
public clear(): void {
|
|
1095
1159
|
this.throwIfDisposed();
|
|
1096
|
-
// Clear the data locally first.
|
|
1097
|
-
this.clearCore(true);
|
|
1098
1160
|
|
|
1099
1161
|
// If we are not attached, don't submit the op.
|
|
1100
1162
|
if (!this.directory.isAttached()) {
|
|
1163
|
+
this.clearCore(true);
|
|
1101
1164
|
return;
|
|
1102
1165
|
}
|
|
1103
1166
|
|
|
1167
|
+
const copy = new Map<string, ILocalValue>(this._storage);
|
|
1168
|
+
this.clearCore(true);
|
|
1104
1169
|
const op: IDirectoryClearOperation = {
|
|
1105
1170
|
path: this.absolutePath,
|
|
1106
1171
|
type: "clear",
|
|
1107
1172
|
};
|
|
1108
|
-
this.submitClearMessage(op);
|
|
1173
|
+
this.submitClearMessage(op, copy);
|
|
1109
1174
|
}
|
|
1110
1175
|
|
|
1111
1176
|
/**
|
|
@@ -1209,16 +1274,14 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1209
1274
|
): void {
|
|
1210
1275
|
this.throwIfDisposed();
|
|
1211
1276
|
if (local) {
|
|
1212
|
-
assert(localOpMetadata
|
|
1213
|
-
0x00f /* pendingMessageId is missing from the local client's operation */);
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1277
|
+
assert(isClearLocalOpMetadata(localOpMetadata),
|
|
1278
|
+
0x00f /* `pendingMessageId is missing from the local client's ${op.type} operation` */);
|
|
1279
|
+
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
1280
|
+
assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
|
|
1281
|
+
"pendingMessageId does not match");
|
|
1218
1282
|
return;
|
|
1219
1283
|
}
|
|
1220
1284
|
this.clearExceptPendingKeys();
|
|
1221
|
-
this.directory.emit("clear", local, this.directory);
|
|
1222
1285
|
}
|
|
1223
1286
|
|
|
1224
1287
|
/**
|
|
@@ -1284,7 +1347,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1284
1347
|
localOpMetadata: unknown,
|
|
1285
1348
|
): void {
|
|
1286
1349
|
this.throwIfDisposed();
|
|
1287
|
-
if (!this.
|
|
1350
|
+
if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
|
|
1288
1351
|
return;
|
|
1289
1352
|
}
|
|
1290
1353
|
this.createSubDirectoryCore(op.subdirName, local);
|
|
@@ -1305,7 +1368,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1305
1368
|
localOpMetadata: unknown,
|
|
1306
1369
|
): void {
|
|
1307
1370
|
this.throwIfDisposed();
|
|
1308
|
-
if (!this.
|
|
1371
|
+
if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
|
|
1309
1372
|
return;
|
|
1310
1373
|
}
|
|
1311
1374
|
this.deleteSubDirectoryCore(op.subdirName, local);
|
|
@@ -1314,37 +1377,156 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1314
1377
|
/**
|
|
1315
1378
|
* Submit a clear operation.
|
|
1316
1379
|
* @param op - The operation
|
|
1317
|
-
* @internal
|
|
1318
1380
|
*/
|
|
1319
|
-
|
|
1381
|
+
private submitClearMessage(op: IDirectoryClearOperation,
|
|
1382
|
+
previousValue: Map<string, ILocalValue>): void {
|
|
1320
1383
|
this.throwIfDisposed();
|
|
1384
|
+
const pendingMsgId = ++this.pendingMessageId;
|
|
1385
|
+
this.pendingClearMessageIds.push(pendingMsgId);
|
|
1386
|
+
const metadata: IClearLocalOpMetadata = {
|
|
1387
|
+
type: "clear",
|
|
1388
|
+
pendingMessageId: pendingMsgId,
|
|
1389
|
+
previousStorage: previousValue,
|
|
1390
|
+
};
|
|
1391
|
+
this.directory.submitDirectoryMessage(op, metadata);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Resubmit a clear operation.
|
|
1396
|
+
* @param op - The operation
|
|
1397
|
+
* @internal
|
|
1398
|
+
*/
|
|
1399
|
+
public resubmitClearMessage(op: IDirectoryClearOperation, localOpMetadata: unknown): void {
|
|
1400
|
+
assert(isClearLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata for clear");
|
|
1401
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1402
|
+
const pendingClearMessageId = this.pendingClearMessageIds.shift();
|
|
1403
|
+
assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
|
|
1404
|
+
"pendingMessageId does not match");
|
|
1405
|
+
this.submitClearMessage(op, localOpMetadata.previousStorage);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Get a new pending message id for the op and cache it to track the pending op
|
|
1410
|
+
*/
|
|
1411
|
+
private getKeyMessageId(op: IDirectoryKeyOperation): number {
|
|
1412
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1321
1413
|
const pendingMessageId = ++this.pendingMessageId;
|
|
1322
|
-
this.
|
|
1323
|
-
|
|
1414
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1415
|
+
if (pendingMessageIds !== undefined) {
|
|
1416
|
+
pendingMessageIds.push(pendingMessageId);
|
|
1417
|
+
} else {
|
|
1418
|
+
this.pendingKeys.set(op.key, [pendingMessageId]);
|
|
1419
|
+
}
|
|
1420
|
+
return pendingMessageId;
|
|
1324
1421
|
}
|
|
1325
1422
|
|
|
1326
1423
|
/**
|
|
1327
1424
|
* Submit a key operation.
|
|
1328
1425
|
* @param op - The operation
|
|
1426
|
+
* @param previousValue - The value of the key before this op
|
|
1427
|
+
*/
|
|
1428
|
+
private submitKeyMessage(op: IDirectoryKeyOperation, previousValue?: ILocalValue): void {
|
|
1429
|
+
this.throwIfDisposed();
|
|
1430
|
+
const pendingMessageId = this.getKeyMessageId(op);
|
|
1431
|
+
const localMetadata = { type: "edit", pendingMessageId, previousValue };
|
|
1432
|
+
this.directory.submitDirectoryMessage(op, localMetadata);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Submit a key message to remote clients based on a previous submit.
|
|
1437
|
+
* @param op - The map key message
|
|
1438
|
+
* @param localOpMetadata - Metadata from the previous submit
|
|
1329
1439
|
* @internal
|
|
1330
1440
|
*/
|
|
1331
|
-
public
|
|
1441
|
+
public resubmitKeyMessage(op: IDirectoryKeyOperation, localOpMetadata: unknown): void {
|
|
1442
|
+
assert(isKeyEditLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata in submit");
|
|
1443
|
+
|
|
1444
|
+
// clear the old pending message id
|
|
1445
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1446
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
|
|
1447
|
+
"Unexpected pending message received");
|
|
1448
|
+
pendingMessageIds.shift();
|
|
1449
|
+
if (pendingMessageIds.length === 0) {
|
|
1450
|
+
this.pendingKeys.delete(op.key);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
this.submitKeyMessage(op, localOpMetadata.previousValue);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Get a new pending message id for the op and cache it to track the pending op
|
|
1458
|
+
*/
|
|
1459
|
+
private getSubDirMessageId(op: IDirectorySubDirectoryOperation): number {
|
|
1460
|
+
// We don't reuse the metadata pendingMessageId but send a new one on each submit.
|
|
1461
|
+
const newMessageId = ++this.pendingMessageId;
|
|
1462
|
+
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1463
|
+
if (pendingMessageIds !== undefined) {
|
|
1464
|
+
pendingMessageIds.push(newMessageId);
|
|
1465
|
+
} else {
|
|
1466
|
+
this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
|
|
1467
|
+
}
|
|
1468
|
+
return newMessageId;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Submit a create subdirectory operation.
|
|
1473
|
+
* @param op - The operation
|
|
1474
|
+
* @param prevExisted - Whether the subdirectory existed before the op
|
|
1475
|
+
*/
|
|
1476
|
+
private submitCreateSubDirectoryMessage(op: IDirectorySubDirectoryOperation,
|
|
1477
|
+
prevExisted: boolean): void {
|
|
1332
1478
|
this.throwIfDisposed();
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1479
|
+
const newMessageId = this.getSubDirMessageId(op);
|
|
1480
|
+
|
|
1481
|
+
const localOpMetadata: ICreateSubDirLocalOpMetadata = {
|
|
1482
|
+
type: "createSubDir",
|
|
1483
|
+
pendingMessageId: newMessageId,
|
|
1484
|
+
previouslyExisted: prevExisted,
|
|
1485
|
+
};
|
|
1486
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1336
1487
|
}
|
|
1337
1488
|
|
|
1338
1489
|
/**
|
|
1339
|
-
* Submit a subdirectory operation.
|
|
1490
|
+
* Submit a delete subdirectory operation.
|
|
1340
1491
|
* @param op - The operation
|
|
1341
|
-
* @
|
|
1492
|
+
* @param subDir - Any subdirectory deleted by the op
|
|
1342
1493
|
*/
|
|
1343
|
-
|
|
1494
|
+
private submitDeleteSubDirectoryMessage(op: IDirectorySubDirectoryOperation,
|
|
1495
|
+
subDir: SubDirectory | undefined): void {
|
|
1344
1496
|
this.throwIfDisposed();
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1497
|
+
const newMessageId = this.getSubDirMessageId(op);
|
|
1498
|
+
|
|
1499
|
+
const localOpMetadata: IDeleteSubDirLocalOpMetadata = {
|
|
1500
|
+
type: "deleteSubDir",
|
|
1501
|
+
pendingMessageId: newMessageId,
|
|
1502
|
+
subDirectory: subDir,
|
|
1503
|
+
};
|
|
1504
|
+
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Submit a subdirectory operation again
|
|
1509
|
+
* @param op - The operation
|
|
1510
|
+
* @param localOpMetadata - metadata submitted with the op originally
|
|
1511
|
+
* @internal
|
|
1512
|
+
*/
|
|
1513
|
+
public resubmitSubDirectoryMessage(op: IDirectorySubDirectoryOperation, localOpMetadata: unknown): void {
|
|
1514
|
+
assert(isSubDirLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata for sub directory op");
|
|
1515
|
+
|
|
1516
|
+
// clear the old pending message id
|
|
1517
|
+
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1518
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
|
|
1519
|
+
"Unexpected pending message received");
|
|
1520
|
+
pendingMessageIds.shift();
|
|
1521
|
+
if (pendingMessageIds.length === 0) {
|
|
1522
|
+
this.pendingSubDirectories.delete(op.subdirName);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (localOpMetadata.type === "createSubDir") {
|
|
1526
|
+
this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
|
|
1527
|
+
} else {
|
|
1528
|
+
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1529
|
+
}
|
|
1348
1530
|
}
|
|
1349
1531
|
|
|
1350
1532
|
/**
|
|
@@ -1396,6 +1578,69 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1396
1578
|
return this._storage.get(key) as T;
|
|
1397
1579
|
}
|
|
1398
1580
|
|
|
1581
|
+
/**
|
|
1582
|
+
* Remove the pendingMessageId from the map tracking it on rollback
|
|
1583
|
+
* @param map - map tracking the pending messages
|
|
1584
|
+
* @param key - key of the edit in the op
|
|
1585
|
+
*/
|
|
1586
|
+
private rollbackPendingMessageId(map: Map<string, number[]>, key: string, pendingMessageId) {
|
|
1587
|
+
const pendingMessageIds = map.get(key);
|
|
1588
|
+
const lastPendingMessageId = pendingMessageIds?.pop();
|
|
1589
|
+
if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
|
|
1590
|
+
throw new Error("Rollback op does not match last pending");
|
|
1591
|
+
}
|
|
1592
|
+
if (pendingMessageIds.length === 0) {
|
|
1593
|
+
map.delete(key);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Rollback a local op
|
|
1599
|
+
* @param op - The operation to rollback
|
|
1600
|
+
* @param localOpMetadata - The local metadata associated with the op.
|
|
1601
|
+
*/
|
|
1602
|
+
public rollback(op: any, localOpMetadata: unknown) {
|
|
1603
|
+
if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
|
|
1604
|
+
throw new Error("Invalid localOpMetadata");
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (op.type === "clear" && localOpMetadata.type === "clear") {
|
|
1608
|
+
localOpMetadata.previousStorage.forEach((localValue, key) => {
|
|
1609
|
+
this.setCore(key, localValue, true);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
const lastPendingClearId = this.pendingClearMessageIds.pop();
|
|
1613
|
+
if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
|
|
1614
|
+
throw new Error("Rollback op does match last clear");
|
|
1615
|
+
}
|
|
1616
|
+
} else if ((op.type === "delete" || op.type === "set") && localOpMetadata.type === "edit") {
|
|
1617
|
+
if (localOpMetadata.previousValue === undefined) {
|
|
1618
|
+
this.deleteCore(op.key, true);
|
|
1619
|
+
} else {
|
|
1620
|
+
this.setCore(op.key, localOpMetadata.previousValue, true);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
|
|
1624
|
+
} else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
|
|
1625
|
+
if (!localOpMetadata.previouslyExisted) {
|
|
1626
|
+
this.deleteSubDirectoryCore(op.subdirName, true);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
|
|
1630
|
+
} else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
|
|
1631
|
+
if (localOpMetadata.subDirectory !== undefined) {
|
|
1632
|
+
this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
|
|
1633
|
+
// don't need to register events because deleting never unregistered
|
|
1634
|
+
this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
|
|
1635
|
+
this.emit("subDirectoryCreated", op.subdirName, true, this);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
|
|
1639
|
+
} else {
|
|
1640
|
+
throw new Error("Unsupported op for rollback");
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1399
1644
|
/**
|
|
1400
1645
|
* Converts the given relative path into an absolute path.
|
|
1401
1646
|
* @param path - Relative path to convert
|
|
@@ -1409,10 +1654,9 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1409
1654
|
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
1410
1655
|
* not process the incoming operation.
|
|
1411
1656
|
* @param op - Operation to check
|
|
1412
|
-
* @param local - Whether the
|
|
1413
|
-
* @param
|
|
1414
|
-
*
|
|
1415
|
-
* For messages from a remote client, this will be undefined.
|
|
1657
|
+
* @param local - Whether the operation originated from the local client
|
|
1658
|
+
* @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
|
|
1659
|
+
* For ops from a remote client, this will be undefined.
|
|
1416
1660
|
* @returns True if the operation should be processed, false otherwise
|
|
1417
1661
|
*/
|
|
1418
1662
|
private needProcessStorageOperation(
|
|
@@ -1420,9 +1664,10 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1420
1664
|
local: boolean,
|
|
1421
1665
|
localOpMetadata: unknown,
|
|
1422
1666
|
): boolean {
|
|
1423
|
-
if (this.
|
|
1667
|
+
if (this.pendingClearMessageIds.length > 0) {
|
|
1424
1668
|
if (local) {
|
|
1425
|
-
assert(localOpMetadata !== undefined && localOpMetadata
|
|
1669
|
+
assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata) &&
|
|
1670
|
+
localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
|
|
1426
1671
|
0x010 /* "Received out of order storage op when there is an unackd clear message" */);
|
|
1427
1672
|
}
|
|
1428
1673
|
// If I have a NACK clear, we can ignore all ops.
|
|
@@ -1434,10 +1679,13 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1434
1679
|
// Found an NACK op, clear it from the directory if the latest sequence number in the directory
|
|
1435
1680
|
// match the message's and don't process the op.
|
|
1436
1681
|
if (local) {
|
|
1437
|
-
assert(localOpMetadata !== undefined,
|
|
1682
|
+
assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata),
|
|
1438
1683
|
0x011 /* pendingMessageId is missing from the local client's operation */);
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1684
|
+
const pendingMessageIds = this.pendingKeys.get(op.key);
|
|
1685
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
|
|
1686
|
+
"Unexpected pending message received");
|
|
1687
|
+
pendingMessageIds.shift();
|
|
1688
|
+
if (pendingMessageIds.length === 0) {
|
|
1441
1689
|
this.pendingKeys.delete(op.key);
|
|
1442
1690
|
}
|
|
1443
1691
|
}
|
|
@@ -1458,7 +1706,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1458
1706
|
* For messages from a remote client, this will be undefined.
|
|
1459
1707
|
* @returns True if the operation should be processed, false otherwise
|
|
1460
1708
|
*/
|
|
1461
|
-
private
|
|
1709
|
+
private needProcessSubDirectoryOperation(
|
|
1462
1710
|
op: IDirectorySubDirectoryOperation,
|
|
1463
1711
|
local: boolean,
|
|
1464
1712
|
localOpMetadata: unknown,
|
|
@@ -1466,10 +1714,13 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1466
1714
|
const pendingSubDirectoryMessageId = this.pendingSubDirectories.get(op.subdirName);
|
|
1467
1715
|
if (pendingSubDirectoryMessageId !== undefined) {
|
|
1468
1716
|
if (local) {
|
|
1469
|
-
assert(localOpMetadata
|
|
1717
|
+
assert(isSubDirLocalOpMetadata(localOpMetadata),
|
|
1470
1718
|
0x012 /* pendingMessageId is missing from the local client's operation */);
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1719
|
+
const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
|
|
1720
|
+
assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
|
|
1721
|
+
"Unexpected pending message received");
|
|
1722
|
+
pendingMessageIds.shift();
|
|
1723
|
+
if (pendingMessageIds.length === 0) {
|
|
1473
1724
|
this.pendingSubDirectories.delete(op.subdirName);
|
|
1474
1725
|
}
|
|
1475
1726
|
}
|
|
@@ -1490,9 +1741,9 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1490
1741
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1491
1742
|
temp.set(key, this._storage.get(key)!);
|
|
1492
1743
|
});
|
|
1493
|
-
this.
|
|
1744
|
+
this.clearCore(false);
|
|
1494
1745
|
temp.forEach((value, key, map) => {
|
|
1495
|
-
this.
|
|
1746
|
+
this.setCore(key, value, true);
|
|
1496
1747
|
});
|
|
1497
1748
|
}
|
|
1498
1749
|
|
|
@@ -1510,11 +1761,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1510
1761
|
* Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
|
|
1511
1762
|
* @param key - The key being deleted
|
|
1512
1763
|
* @param local - Whether the message originated from the local client
|
|
1513
|
-
* @
|
|
1514
|
-
* @returns True if the key existed and was deleted, false if it did not exist
|
|
1764
|
+
* @returns Previous local value of the key if it existed, undefined if it did not exist
|
|
1515
1765
|
*/
|
|
1516
|
-
private deleteCore(key: string, local: boolean) {
|
|
1517
|
-
const
|
|
1766
|
+
private deleteCore(key: string, local: boolean): ILocalValue | undefined {
|
|
1767
|
+
const previousLocalValue = this._storage.get(key);
|
|
1768
|
+
const previousValue = previousLocalValue?.value;
|
|
1518
1769
|
const successfullyRemoved = this._storage.delete(key);
|
|
1519
1770
|
if (successfullyRemoved) {
|
|
1520
1771
|
const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
|
|
@@ -1522,7 +1773,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1522
1773
|
const containedEvent: IValueChanged = { key, previousValue };
|
|
1523
1774
|
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1524
1775
|
}
|
|
1525
|
-
return
|
|
1776
|
+
return previousLocalValue;
|
|
1526
1777
|
}
|
|
1527
1778
|
|
|
1528
1779
|
/**
|
|
@@ -1530,30 +1781,35 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1530
1781
|
* @param key - The key being set
|
|
1531
1782
|
* @param value - The value being set
|
|
1532
1783
|
* @param local - Whether the message originated from the local client
|
|
1533
|
-
* @
|
|
1784
|
+
* @returns Previous local value of the key, if any
|
|
1534
1785
|
*/
|
|
1535
|
-
private setCore(key: string, value: ILocalValue, local: boolean) {
|
|
1536
|
-
const
|
|
1786
|
+
private setCore(key: string, value: ILocalValue, local: boolean): ILocalValue | undefined {
|
|
1787
|
+
const previousLocalValue = this._storage.get(key);
|
|
1788
|
+
const previousValue = previousLocalValue?.value;
|
|
1537
1789
|
this._storage.set(key, value);
|
|
1538
1790
|
const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
|
|
1539
1791
|
this.directory.emit("valueChanged", event, local, this.directory);
|
|
1540
1792
|
const containedEvent: IValueChanged = { key, previousValue };
|
|
1541
1793
|
this.emit("containedValueChanged", containedEvent, local, this);
|
|
1794
|
+
return previousLocalValue;
|
|
1542
1795
|
}
|
|
1543
1796
|
|
|
1544
1797
|
/**
|
|
1545
1798
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1546
1799
|
* @param subdirName - The name of the subdirectory being created
|
|
1547
1800
|
* @param local - Whether the message originated from the local client
|
|
1801
|
+
* @returns - True if is newly created, false if it already existed.
|
|
1548
1802
|
*/
|
|
1549
|
-
private createSubDirectoryCore(subdirName: string, local: boolean) {
|
|
1803
|
+
private createSubDirectoryCore(subdirName: string, local: boolean): boolean {
|
|
1550
1804
|
if (!this._subdirectories.has(subdirName)) {
|
|
1551
1805
|
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
1552
1806
|
const subDir = new SubDirectory(this.directory, this.runtime, this.serializer, absolutePath);
|
|
1553
1807
|
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
1554
1808
|
this._subdirectories.set(subdirName, subDir);
|
|
1555
1809
|
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
1810
|
+
return true;
|
|
1556
1811
|
}
|
|
1812
|
+
return false;
|
|
1557
1813
|
}
|
|
1558
1814
|
|
|
1559
1815
|
private registerEventsOnSubDirectory(subDirectory: SubDirectory, subDirName: string) {
|
|
@@ -1569,18 +1825,17 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1569
1825
|
* Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
1570
1826
|
* @param subdirName - The name of the subdirectory being deleted
|
|
1571
1827
|
* @param local - Whether the message originated from the local client
|
|
1572
|
-
* @param op - The message if from a remote delete, or null if from a local delete
|
|
1573
1828
|
*/
|
|
1574
1829
|
private deleteSubDirectoryCore(subdirName: string, local: boolean) {
|
|
1575
|
-
const previousValue = this.
|
|
1830
|
+
const previousValue = this._subdirectories.get(subdirName);
|
|
1576
1831
|
// This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
|
|
1577
|
-
// Might want to consider cleaning out the structure more exhaustively though?
|
|
1578
|
-
const successfullyRemoved = this._subdirectories.delete(subdirName);
|
|
1832
|
+
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
1579
1833
|
if (previousValue !== undefined) {
|
|
1834
|
+
this._subdirectories.delete(subdirName);
|
|
1580
1835
|
this.disposeSubDirectoryTree(previousValue);
|
|
1581
1836
|
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
1582
1837
|
}
|
|
1583
|
-
return
|
|
1838
|
+
return previousValue;
|
|
1584
1839
|
}
|
|
1585
1840
|
|
|
1586
1841
|
private disposeSubDirectoryTree(directory: IDirectory | undefined) {
|
|
@@ -1596,4 +1851,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1596
1851
|
directory.dispose();
|
|
1597
1852
|
}
|
|
1598
1853
|
}
|
|
1854
|
+
|
|
1855
|
+
private undeleteSubDirectoryTree(directory: SubDirectory) {
|
|
1856
|
+
// Restore deleted subdirectory tree. This will unmark "deleted" from the subdirectories from bottom to top.
|
|
1857
|
+
for (const [_, subDirectory] of this._subdirectories.entries()) {
|
|
1858
|
+
this.undeleteSubDirectoryTree(subDirectory);
|
|
1859
|
+
}
|
|
1860
|
+
directory.undispose();
|
|
1861
|
+
}
|
|
1599
1862
|
}
|