@fluidframework/map 2.0.0-internal.3.3.2 → 2.0.0-internal.3.4.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/dist/directory.d.ts +32 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +219 -60
- package/dist/directory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/lib/directory.d.ts +32 -0
- package/lib/directory.d.ts.map +1 -1
- package/lib/directory.js +219 -60
- package/lib/directory.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/package.json +21 -22
- package/src/directory.ts +308 -58
- package/src/index.ts +1 -0
- package/src/packageVersion.ts +1 -1
package/src/directory.ts
CHANGED
|
@@ -43,12 +43,18 @@ const snapshotFileName = "header";
|
|
|
43
43
|
interface IDirectoryMessageHandler {
|
|
44
44
|
/**
|
|
45
45
|
* Apply the given operation.
|
|
46
|
+
* @param msg - The message from the server to apply.
|
|
46
47
|
* @param op - The directory operation to apply
|
|
47
48
|
* @param local - Whether the message originated from the local client
|
|
48
49
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
49
50
|
* For messages from a remote client, this will be undefined.
|
|
50
51
|
*/
|
|
51
|
-
process(
|
|
52
|
+
process(
|
|
53
|
+
msg: ISequencedDocumentMessage,
|
|
54
|
+
op: IDirectoryOperation,
|
|
55
|
+
local: boolean,
|
|
56
|
+
localOpMetadata: unknown,
|
|
57
|
+
): void;
|
|
52
58
|
|
|
53
59
|
/**
|
|
54
60
|
* Communicate the operation to remote clients.
|
|
@@ -182,6 +188,21 @@ export type IDirectorySubDirectoryOperation =
|
|
|
182
188
|
*/
|
|
183
189
|
export type IDirectoryOperation = IDirectoryStorageOperation | IDirectorySubDirectoryOperation;
|
|
184
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Create info for the subdirectory.
|
|
193
|
+
*/
|
|
194
|
+
export interface ICreateInfo {
|
|
195
|
+
/**
|
|
196
|
+
* Sequence number at which this subdirectory was created.
|
|
197
|
+
*/
|
|
198
|
+
csn: number;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* clientids of the clients which created this sub directory.
|
|
202
|
+
*/
|
|
203
|
+
ccIds: string[];
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
/**
|
|
186
207
|
* Defines the in-memory object structure to be used for the conversion to/from serialized.
|
|
187
208
|
*
|
|
@@ -200,6 +221,15 @@ export interface IDirectoryDataObject {
|
|
|
200
221
|
* Recursive sub-directories {@link IDirectoryDataObject | objects}.
|
|
201
222
|
*/
|
|
202
223
|
subdirectories?: { [subdirName: string]: IDirectoryDataObject };
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create info for the sub directory. Since directories with same name can get deleted/created by multiple clients
|
|
227
|
+
* asynchronously, this info helps us to determine whether the ops where for the current instance of sub directory
|
|
228
|
+
* or not and whether to process them or not based on that. Summaries which were not produced which this change
|
|
229
|
+
* will not have this info and in that case we can still run in eventual consistency issues but that is no worse
|
|
230
|
+
* than the state before this change.
|
|
231
|
+
*/
|
|
232
|
+
ci?: ICreateInfo;
|
|
203
233
|
}
|
|
204
234
|
|
|
205
235
|
/**
|
|
@@ -336,6 +366,8 @@ export class SharedDirectory
|
|
|
336
366
|
* Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
|
|
337
367
|
*/
|
|
338
368
|
private readonly root: SubDirectory = new SubDirectory(
|
|
369
|
+
0,
|
|
370
|
+
new Set(),
|
|
339
371
|
this,
|
|
340
372
|
this.runtime,
|
|
341
373
|
this.serializer,
|
|
@@ -620,7 +652,12 @@ export class SharedDirectory
|
|
|
620
652
|
)) {
|
|
621
653
|
let newSubDir = currentSubDir.getSubDirectory(subdirName) as SubDirectory;
|
|
622
654
|
if (!newSubDir) {
|
|
655
|
+
const createInfo = subdirObject.ci;
|
|
623
656
|
newSubDir = new SubDirectory(
|
|
657
|
+
createInfo !== undefined ? createInfo.csn : 0,
|
|
658
|
+
createInfo !== undefined
|
|
659
|
+
? new Set<string>(createInfo.ccIds)
|
|
660
|
+
: new Set(),
|
|
624
661
|
this,
|
|
625
662
|
this.runtime,
|
|
626
663
|
this.serializer,
|
|
@@ -658,7 +695,7 @@ export class SharedDirectory
|
|
|
658
695
|
const op: IDirectoryOperation = message.contents as IDirectoryOperation;
|
|
659
696
|
const handler = this.messageHandlers.get(op.type);
|
|
660
697
|
assert(handler !== undefined, 0x00e /* Missing message handler for message type */);
|
|
661
|
-
handler.process(op, local, localOpMetadata);
|
|
698
|
+
handler.process(message, op, local, localOpMetadata);
|
|
662
699
|
}
|
|
663
700
|
}
|
|
664
701
|
|
|
@@ -705,15 +742,48 @@ export class SharedDirectory
|
|
|
705
742
|
return this.localValueMaker.fromSerializable(serializable);
|
|
706
743
|
}
|
|
707
744
|
|
|
745
|
+
/**
|
|
746
|
+
* This checks if there is pending delete op for local delete for a subdirectory.
|
|
747
|
+
* @param relativePath - path of sub directory.
|
|
748
|
+
* @returns - true if there is pending delete.
|
|
749
|
+
*/
|
|
750
|
+
private isSubDirectoryDeletePending(relativePath: string): boolean {
|
|
751
|
+
const parentSubDir = this.getParentDirectory(relativePath);
|
|
752
|
+
const index = relativePath.lastIndexOf(posix.sep);
|
|
753
|
+
const dirName = relativePath.substring(index + 1);
|
|
754
|
+
return !!parentSubDir?.isSubDirectoryDeletePending(dirName);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Gets the parent directory of a sub directory.
|
|
759
|
+
* @param relativePath - path of sub directory of which parent needs to be find out.
|
|
760
|
+
*/
|
|
761
|
+
private getParentDirectory(relativePath: string): SubDirectory | undefined {
|
|
762
|
+
const absolutePath = this.makeAbsolute(relativePath);
|
|
763
|
+
if (absolutePath === posix.sep) {
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
const index = absolutePath.lastIndexOf(posix.sep);
|
|
767
|
+
const parentAbsPath = absolutePath.substring(0, index);
|
|
768
|
+
return this.getWorkingDirectory(parentAbsPath) as SubDirectory;
|
|
769
|
+
}
|
|
770
|
+
|
|
708
771
|
/**
|
|
709
772
|
* Set the message handlers for the directory.
|
|
710
773
|
*/
|
|
711
774
|
private setMessageHandlers(): void {
|
|
712
775
|
this.messageHandlers.set("clear", {
|
|
713
|
-
process: (
|
|
776
|
+
process: (
|
|
777
|
+
msg: ISequencedDocumentMessage,
|
|
778
|
+
op: IDirectoryClearOperation,
|
|
779
|
+
local,
|
|
780
|
+
localOpMetadata,
|
|
781
|
+
) => {
|
|
714
782
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
715
|
-
|
|
716
|
-
|
|
783
|
+
// If there is pending delete op for this subDirectory, then don't apply the this op as we are going
|
|
784
|
+
// to delete this subDirectory.
|
|
785
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
786
|
+
subdir.processClearMessage(msg, op, local, localOpMetadata);
|
|
717
787
|
}
|
|
718
788
|
},
|
|
719
789
|
submit: (op: IDirectoryClearOperation, localOpMetadata: unknown) => {
|
|
@@ -730,10 +800,17 @@ export class SharedDirectory
|
|
|
730
800
|
},
|
|
731
801
|
});
|
|
732
802
|
this.messageHandlers.set("delete", {
|
|
733
|
-
process: (
|
|
803
|
+
process: (
|
|
804
|
+
msg: ISequencedDocumentMessage,
|
|
805
|
+
op: IDirectoryDeleteOperation,
|
|
806
|
+
local,
|
|
807
|
+
localOpMetadata,
|
|
808
|
+
) => {
|
|
734
809
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
735
|
-
|
|
736
|
-
|
|
810
|
+
// If there is pending delete op for this subDirectory, then don't apply the this op as we are going
|
|
811
|
+
// to delete this subDirectory.
|
|
812
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
813
|
+
subdir.processDeleteMessage(msg, op, local, localOpMetadata);
|
|
737
814
|
}
|
|
738
815
|
},
|
|
739
816
|
submit: (op: IDirectoryDeleteOperation, localOpMetadata: unknown) => {
|
|
@@ -752,11 +829,18 @@ export class SharedDirectory
|
|
|
752
829
|
},
|
|
753
830
|
});
|
|
754
831
|
this.messageHandlers.set("set", {
|
|
755
|
-
process: (
|
|
832
|
+
process: (
|
|
833
|
+
msg: ISequencedDocumentMessage,
|
|
834
|
+
op: IDirectorySetOperation,
|
|
835
|
+
local,
|
|
836
|
+
localOpMetadata,
|
|
837
|
+
) => {
|
|
756
838
|
const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
757
|
-
|
|
839
|
+
// If there is pending delete op for this subDirectory, then don't apply the this op as we are going
|
|
840
|
+
// to delete this subDirectory.
|
|
841
|
+
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
758
842
|
const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
|
|
759
|
-
subdir.processSetMessage(op, context, local, localOpMetadata);
|
|
843
|
+
subdir.processSetMessage(msg, op, context, local, localOpMetadata);
|
|
760
844
|
}
|
|
761
845
|
},
|
|
762
846
|
submit: (op: IDirectorySetOperation, localOpMetadata: unknown) => {
|
|
@@ -775,10 +859,15 @@ export class SharedDirectory
|
|
|
775
859
|
});
|
|
776
860
|
|
|
777
861
|
this.messageHandlers.set("createSubDirectory", {
|
|
778
|
-
process: (
|
|
862
|
+
process: (
|
|
863
|
+
msg: ISequencedDocumentMessage,
|
|
864
|
+
op: IDirectoryCreateSubDirectoryOperation,
|
|
865
|
+
local,
|
|
866
|
+
localOpMetadata,
|
|
867
|
+
) => {
|
|
779
868
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
780
869
|
if (parentSubdir) {
|
|
781
|
-
parentSubdir.processCreateSubDirectoryMessage(op, local, localOpMetadata);
|
|
870
|
+
parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
782
871
|
}
|
|
783
872
|
},
|
|
784
873
|
submit: (op: IDirectoryCreateSubDirectoryOperation, localOpMetadata: unknown) => {
|
|
@@ -799,10 +888,15 @@ export class SharedDirectory
|
|
|
799
888
|
});
|
|
800
889
|
|
|
801
890
|
this.messageHandlers.set("deleteSubDirectory", {
|
|
802
|
-
process: (
|
|
891
|
+
process: (
|
|
892
|
+
msg: ISequencedDocumentMessage,
|
|
893
|
+
op: IDirectoryDeleteSubDirectoryOperation,
|
|
894
|
+
local,
|
|
895
|
+
localOpMetadata,
|
|
896
|
+
) => {
|
|
803
897
|
const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
804
898
|
if (parentSubdir) {
|
|
805
|
-
parentSubdir.processDeleteSubDirectoryMessage(op, local, localOpMetadata);
|
|
899
|
+
parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
806
900
|
}
|
|
807
901
|
},
|
|
808
902
|
submit: (op: IDirectoryDeleteSubDirectoryOperation, localOpMetadata: unknown) => {
|
|
@@ -853,6 +947,7 @@ export class SharedDirectory
|
|
|
853
947
|
while (stack.length > 0) {
|
|
854
948
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
855
949
|
const [currentSubDir, currentSubDirObject] = stack.pop()!;
|
|
950
|
+
currentSubDirObject.ci = currentSubDir.getSerializableCreateInfo();
|
|
856
951
|
for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
|
|
857
952
|
if (!currentSubDirObject.storage) {
|
|
858
953
|
currentSubDirObject.storage = {};
|
|
@@ -916,7 +1011,6 @@ interface IClearLocalOpMetadata {
|
|
|
916
1011
|
interface ICreateSubDirLocalOpMetadata {
|
|
917
1012
|
type: "createSubDir";
|
|
918
1013
|
pendingMessageId: number;
|
|
919
|
-
previouslyExisted: boolean;
|
|
920
1014
|
}
|
|
921
1015
|
|
|
922
1016
|
interface IDeleteSubDirLocalOpMetadata {
|
|
@@ -954,8 +1048,7 @@ function isSubDirLocalOpMetadata(metadata: any): metadata is SubDirLocalOpMetada
|
|
|
954
1048
|
return (
|
|
955
1049
|
metadata !== undefined &&
|
|
956
1050
|
typeof metadata.pendingMessageId === "number" &&
|
|
957
|
-
(
|
|
958
|
-
metadata.type === "deleteSubDir")
|
|
1051
|
+
(metadata.type === "createSubDir" || metadata.type === "deleteSubDir")
|
|
959
1052
|
);
|
|
960
1053
|
}
|
|
961
1054
|
|
|
@@ -966,7 +1059,7 @@ function isDirectoryLocalOpMetadata(metadata: any): metadata is DirectoryLocalOp
|
|
|
966
1059
|
(metadata.type === "edit" ||
|
|
967
1060
|
metadata.type === "deleteSubDir" ||
|
|
968
1061
|
(metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
|
|
969
|
-
|
|
1062
|
+
metadata.type === "createSubDir")
|
|
970
1063
|
);
|
|
971
1064
|
}
|
|
972
1065
|
|
|
@@ -1003,10 +1096,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1003
1096
|
private readonly pendingKeys: Map<string, number[]> = new Map();
|
|
1004
1097
|
|
|
1005
1098
|
/**
|
|
1006
|
-
* Subdirectories that have been
|
|
1099
|
+
* Subdirectories that have been created/deleted locally but not yet ack'd from the server.
|
|
1007
1100
|
*/
|
|
1008
1101
|
private readonly pendingSubDirectories: Map<string, number[]> = new Map();
|
|
1009
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the count
|
|
1105
|
+
* of delete op that are pending or yet to be acked from server.
|
|
1106
|
+
*/
|
|
1107
|
+
private readonly pendingDeleteSubDirectoriesCount: Map<string, number> = new Map();
|
|
1108
|
+
|
|
1010
1109
|
/**
|
|
1011
1110
|
* This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
|
|
1012
1111
|
*/
|
|
@@ -1019,12 +1118,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1019
1118
|
|
|
1020
1119
|
/**
|
|
1021
1120
|
* Constructor.
|
|
1121
|
+
* @param sequenceNumber - Message seq number at which this was created.
|
|
1122
|
+
* @param clientIds - Ids of client which created this directory.
|
|
1022
1123
|
* @param directory - Reference back to the SharedDirectory to perform operations
|
|
1023
1124
|
* @param runtime - The data store runtime this directory is associated with
|
|
1024
1125
|
* @param serializer - The serializer to serialize / parse handles
|
|
1025
1126
|
* @param absolutePath - The absolute path of this IDirectory
|
|
1026
1127
|
*/
|
|
1027
1128
|
public constructor(
|
|
1129
|
+
private sequenceNumber: number,
|
|
1130
|
+
private readonly clientIds: Set<string>,
|
|
1028
1131
|
private readonly directory: SharedDirectory,
|
|
1029
1132
|
private readonly runtime: IFluidDataStoreRuntime,
|
|
1030
1133
|
private readonly serializer: IFluidSerializer,
|
|
@@ -1132,22 +1235,29 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1132
1235
|
}
|
|
1133
1236
|
|
|
1134
1237
|
// Create the sub directory locally first.
|
|
1135
|
-
const isNew = this.createSubDirectoryCore(
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1238
|
+
const isNew = this.createSubDirectoryCore(
|
|
1239
|
+
subdirName,
|
|
1240
|
+
true,
|
|
1241
|
+
-1,
|
|
1242
|
+
this.runtime.clientId ?? "detached",
|
|
1243
|
+
);
|
|
1244
|
+
const subDir = this._subdirectories.get(subdirName);
|
|
1245
|
+
assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
|
|
1139
1246
|
|
|
1140
1247
|
// If we are not attached, don't submit the op.
|
|
1141
1248
|
if (!this.directory.isAttached()) {
|
|
1142
1249
|
return subDir;
|
|
1143
1250
|
}
|
|
1144
1251
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1252
|
+
// Only submit the op, if it is newly created.
|
|
1253
|
+
if (isNew) {
|
|
1254
|
+
const op: IDirectoryCreateSubDirectoryOperation = {
|
|
1255
|
+
path: this.absolutePath,
|
|
1256
|
+
subdirName,
|
|
1257
|
+
type: "createSubDirectory",
|
|
1258
|
+
};
|
|
1259
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
1260
|
+
}
|
|
1151
1261
|
|
|
1152
1262
|
return subDir;
|
|
1153
1263
|
}
|
|
@@ -1181,13 +1291,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1181
1291
|
return subDir !== undefined;
|
|
1182
1292
|
}
|
|
1183
1293
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1294
|
+
// Only submit the op, if the directory existed and we deleted it.
|
|
1295
|
+
if (subDir !== undefined) {
|
|
1296
|
+
const op: IDirectoryDeleteSubDirectoryOperation = {
|
|
1297
|
+
path: this.absolutePath,
|
|
1298
|
+
subdirName,
|
|
1299
|
+
type: "deleteSubDirectory",
|
|
1300
|
+
};
|
|
1189
1301
|
|
|
1190
|
-
|
|
1302
|
+
this.submitDeleteSubDirectoryMessage(op, subDir);
|
|
1303
|
+
}
|
|
1191
1304
|
return subDir !== undefined;
|
|
1192
1305
|
}
|
|
1193
1306
|
|
|
@@ -1207,6 +1320,19 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1207
1320
|
return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
|
|
1208
1321
|
}
|
|
1209
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* This checks if there is pending delete op for local delete for a given child subdirectory.
|
|
1325
|
+
* @param subDirName - directory name.
|
|
1326
|
+
* @returns - true if there is pending delete.
|
|
1327
|
+
*/
|
|
1328
|
+
public isSubDirectoryDeletePending(subDirName: string): boolean {
|
|
1329
|
+
const pendingDeleteSubDirectory = this.pendingDeleteSubDirectoriesCount.get(subDirName);
|
|
1330
|
+
if (pendingDeleteSubDirectory !== undefined && pendingDeleteSubDirectory > 0) {
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1210
1336
|
/**
|
|
1211
1337
|
* Deletes the given key from within this IDirectory.
|
|
1212
1338
|
* @param key - The key to delete
|
|
@@ -1337,6 +1463,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1337
1463
|
|
|
1338
1464
|
/**
|
|
1339
1465
|
* Process a clear operation.
|
|
1466
|
+
* @param msg - The message from the server to apply.
|
|
1340
1467
|
* @param op - The op to process
|
|
1341
1468
|
* @param local - Whether the message originated from the local client
|
|
1342
1469
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
@@ -1344,11 +1471,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1344
1471
|
* @internal
|
|
1345
1472
|
*/
|
|
1346
1473
|
public processClearMessage(
|
|
1474
|
+
msg: ISequencedDocumentMessage,
|
|
1347
1475
|
op: IDirectoryClearOperation,
|
|
1348
1476
|
local: boolean,
|
|
1349
1477
|
localOpMetadata: unknown,
|
|
1350
1478
|
): void {
|
|
1351
1479
|
this.throwIfDisposed();
|
|
1480
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1352
1483
|
if (local) {
|
|
1353
1484
|
assert(
|
|
1354
1485
|
isClearLocalOpMetadata(localOpMetadata),
|
|
@@ -1385,6 +1516,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1385
1516
|
|
|
1386
1517
|
/**
|
|
1387
1518
|
* Process a delete operation.
|
|
1519
|
+
* @param msg - The message from the server to apply.
|
|
1388
1520
|
* @param op - The op to process
|
|
1389
1521
|
* @param local - Whether the message originated from the local client
|
|
1390
1522
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
@@ -1392,12 +1524,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1392
1524
|
* @internal
|
|
1393
1525
|
*/
|
|
1394
1526
|
public processDeleteMessage(
|
|
1527
|
+
msg: ISequencedDocumentMessage,
|
|
1395
1528
|
op: IDirectoryDeleteOperation,
|
|
1396
1529
|
local: boolean,
|
|
1397
1530
|
localOpMetadata: unknown,
|
|
1398
1531
|
): void {
|
|
1399
1532
|
this.throwIfDisposed();
|
|
1400
|
-
if (
|
|
1533
|
+
if (
|
|
1534
|
+
!(
|
|
1535
|
+
this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1536
|
+
this.needProcessStorageOperation(op, local, localOpMetadata)
|
|
1537
|
+
)
|
|
1538
|
+
) {
|
|
1401
1539
|
return;
|
|
1402
1540
|
}
|
|
1403
1541
|
this.deleteCore(op.key, local);
|
|
@@ -1422,6 +1560,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1422
1560
|
|
|
1423
1561
|
/**
|
|
1424
1562
|
* Process a set operation.
|
|
1563
|
+
* @param msg - The message from the server to apply.
|
|
1425
1564
|
* @param op - The op to process
|
|
1426
1565
|
* @param local - Whether the message originated from the local client
|
|
1427
1566
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
@@ -1429,19 +1568,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1429
1568
|
* @internal
|
|
1430
1569
|
*/
|
|
1431
1570
|
public processSetMessage(
|
|
1571
|
+
msg: ISequencedDocumentMessage,
|
|
1432
1572
|
op: IDirectorySetOperation,
|
|
1433
1573
|
context: ILocalValue | undefined,
|
|
1434
1574
|
local: boolean,
|
|
1435
1575
|
localOpMetadata: unknown,
|
|
1436
1576
|
): void {
|
|
1437
1577
|
this.throwIfDisposed();
|
|
1438
|
-
if (
|
|
1578
|
+
if (
|
|
1579
|
+
!(
|
|
1580
|
+
this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1581
|
+
this.needProcessStorageOperation(op, local, localOpMetadata)
|
|
1582
|
+
)
|
|
1583
|
+
) {
|
|
1439
1584
|
return;
|
|
1440
1585
|
}
|
|
1441
1586
|
|
|
1442
1587
|
// needProcessStorageOperation should have returned false if local is true
|
|
1443
1588
|
// so we can assume context is not undefined
|
|
1444
|
-
|
|
1445
1589
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1446
1590
|
this.setCore(op.key, context!, local);
|
|
1447
1591
|
}
|
|
@@ -1470,6 +1614,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1470
1614
|
}
|
|
1471
1615
|
/**
|
|
1472
1616
|
* Process a create subdirectory operation.
|
|
1617
|
+
* @param msg - The message from the server to apply.
|
|
1473
1618
|
* @param op - The op to process
|
|
1474
1619
|
* @param local - Whether the message originated from the local client
|
|
1475
1620
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
@@ -1477,15 +1622,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1477
1622
|
* @internal
|
|
1478
1623
|
*/
|
|
1479
1624
|
public processCreateSubDirectoryMessage(
|
|
1625
|
+
msg: ISequencedDocumentMessage,
|
|
1480
1626
|
op: IDirectoryCreateSubDirectoryOperation,
|
|
1481
1627
|
local: boolean,
|
|
1482
1628
|
localOpMetadata: unknown,
|
|
1483
1629
|
): void {
|
|
1484
1630
|
this.throwIfDisposed();
|
|
1485
|
-
if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
|
|
1631
|
+
if (!this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)) {
|
|
1486
1632
|
return;
|
|
1487
1633
|
}
|
|
1488
|
-
this.createSubDirectoryCore(op.subdirName, local);
|
|
1634
|
+
this.createSubDirectoryCore(op.subdirName, local, msg.sequenceNumber, msg.clientId);
|
|
1489
1635
|
}
|
|
1490
1636
|
|
|
1491
1637
|
/**
|
|
@@ -1498,19 +1644,19 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1498
1644
|
): ICreateSubDirLocalOpMetadata {
|
|
1499
1645
|
this.throwIfDisposed();
|
|
1500
1646
|
// Create the sub directory locally first.
|
|
1501
|
-
|
|
1647
|
+
this.createSubDirectoryCore(op.subdirName, true, -1, this.runtime.clientId ?? "detached");
|
|
1502
1648
|
const newMessageId = this.getSubDirMessageId(op);
|
|
1503
1649
|
|
|
1504
1650
|
const localOpMetadata: ICreateSubDirLocalOpMetadata = {
|
|
1505
1651
|
type: "createSubDir",
|
|
1506
1652
|
pendingMessageId: newMessageId,
|
|
1507
|
-
previouslyExisted: !isNew,
|
|
1508
1653
|
};
|
|
1509
1654
|
return localOpMetadata;
|
|
1510
1655
|
}
|
|
1511
1656
|
|
|
1512
1657
|
/**
|
|
1513
1658
|
* Process a delete subdirectory operation.
|
|
1659
|
+
* @param msg - The message from the server to apply.
|
|
1514
1660
|
* @param op - The op to process
|
|
1515
1661
|
* @param local - Whether the message originated from the local client
|
|
1516
1662
|
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
@@ -1518,12 +1664,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1518
1664
|
* @internal
|
|
1519
1665
|
*/
|
|
1520
1666
|
public processDeleteSubDirectoryMessage(
|
|
1667
|
+
msg: ISequencedDocumentMessage,
|
|
1521
1668
|
op: IDirectoryDeleteSubDirectoryOperation,
|
|
1522
1669
|
local: boolean,
|
|
1523
1670
|
localOpMetadata: unknown,
|
|
1524
1671
|
): void {
|
|
1525
1672
|
this.throwIfDisposed();
|
|
1526
|
-
if (
|
|
1673
|
+
if (
|
|
1674
|
+
!(
|
|
1675
|
+
this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
1676
|
+
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)
|
|
1677
|
+
)
|
|
1678
|
+
) {
|
|
1527
1679
|
return;
|
|
1528
1680
|
}
|
|
1529
1681
|
this.deleteSubDirectoryCore(op.subdirName, local);
|
|
@@ -1652,25 +1804,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1652
1804
|
} else {
|
|
1653
1805
|
this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
|
|
1654
1806
|
}
|
|
1807
|
+
if (op.type === "deleteSubDirectory") {
|
|
1808
|
+
const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName) ?? 0;
|
|
1809
|
+
this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count + 1);
|
|
1810
|
+
}
|
|
1655
1811
|
return newMessageId;
|
|
1656
1812
|
}
|
|
1657
1813
|
|
|
1658
1814
|
/**
|
|
1659
1815
|
* Submit a create subdirectory operation.
|
|
1660
1816
|
* @param op - The operation
|
|
1661
|
-
* @param prevExisted - Whether the subdirectory existed before the op
|
|
1662
1817
|
*/
|
|
1663
|
-
private submitCreateSubDirectoryMessage(
|
|
1664
|
-
op: IDirectorySubDirectoryOperation,
|
|
1665
|
-
prevExisted: boolean,
|
|
1666
|
-
): void {
|
|
1818
|
+
private submitCreateSubDirectoryMessage(op: IDirectorySubDirectoryOperation): void {
|
|
1667
1819
|
this.throwIfDisposed();
|
|
1668
1820
|
const newMessageId = this.getSubDirMessageId(op);
|
|
1669
1821
|
|
|
1670
1822
|
const localOpMetadata: ICreateSubDirLocalOpMetadata = {
|
|
1671
1823
|
type: "createSubDir",
|
|
1672
1824
|
pendingMessageId: newMessageId,
|
|
1673
|
-
previouslyExisted: prevExisted,
|
|
1674
1825
|
};
|
|
1675
1826
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
1676
1827
|
}
|
|
@@ -1723,7 +1874,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1723
1874
|
}
|
|
1724
1875
|
|
|
1725
1876
|
if (localOpMetadata.type === "createSubDir") {
|
|
1726
|
-
this.submitCreateSubDirectoryMessage(op
|
|
1877
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
1727
1878
|
} else {
|
|
1728
1879
|
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
1729
1880
|
}
|
|
@@ -1746,6 +1897,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1746
1897
|
}
|
|
1747
1898
|
}
|
|
1748
1899
|
|
|
1900
|
+
public getSerializableCreateInfo() {
|
|
1901
|
+
this.throwIfDisposed();
|
|
1902
|
+
const createInfo: ICreateInfo = {
|
|
1903
|
+
csn: this.sequenceNumber,
|
|
1904
|
+
ccIds: Array.from(this.clientIds),
|
|
1905
|
+
};
|
|
1906
|
+
return createInfo;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1749
1909
|
/**
|
|
1750
1910
|
* Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
|
|
1751
1911
|
* @param key - The key to populate
|
|
@@ -1838,9 +1998,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1838
1998
|
localOpMetadata.pendingMessageId,
|
|
1839
1999
|
);
|
|
1840
2000
|
} else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
|
|
1841
|
-
|
|
1842
|
-
this.deleteSubDirectoryCore(op.subdirName as string, true);
|
|
1843
|
-
}
|
|
2001
|
+
this.deleteSubDirectoryCore(op.subdirName as string, true);
|
|
1844
2002
|
|
|
1845
2003
|
this.rollbackPendingMessageId(
|
|
1846
2004
|
this.pendingSubDirectories,
|
|
@@ -1860,6 +2018,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1860
2018
|
op.subdirName as string,
|
|
1861
2019
|
localOpMetadata.pendingMessageId,
|
|
1862
2020
|
);
|
|
2021
|
+
const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
|
|
2022
|
+
assert(count !== undefined && count > 0, 0x5ab /* should have record for delete op */);
|
|
2023
|
+
this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
|
|
2024
|
+
if (count === 1) {
|
|
2025
|
+
this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
|
|
2026
|
+
}
|
|
1863
2027
|
} else {
|
|
1864
2028
|
throw new Error("Unsupported op for rollback");
|
|
1865
2029
|
}
|
|
@@ -1898,7 +2062,23 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1898
2062
|
localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
|
|
1899
2063
|
0x010 /* "Received out of order storage op when there is an unackd clear message" */,
|
|
1900
2064
|
);
|
|
2065
|
+
// Remove all pendingMessageIds lower than first pendingClearMessageId.
|
|
2066
|
+
const lowestPendingClearMessageId = this.pendingClearMessageIds[0];
|
|
2067
|
+
const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
|
|
2068
|
+
if (pendingKeyMessageIdArray !== undefined) {
|
|
2069
|
+
let index = 0;
|
|
2070
|
+
while (pendingKeyMessageIdArray[index] < lowestPendingClearMessageId) {
|
|
2071
|
+
index += 1;
|
|
2072
|
+
}
|
|
2073
|
+
const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
|
|
2074
|
+
if (newPendingKeyMessageId.length === 0) {
|
|
2075
|
+
this.pendingKeys.delete(op.key);
|
|
2076
|
+
} else {
|
|
2077
|
+
this.pendingKeys.set(op.key, newPendingKeyMessageId);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
1901
2080
|
}
|
|
2081
|
+
|
|
1902
2082
|
// If I have a NACK clear, we can ignore all ops.
|
|
1903
2083
|
return false;
|
|
1904
2084
|
}
|
|
@@ -1930,6 +2110,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1930
2110
|
return !local;
|
|
1931
2111
|
}
|
|
1932
2112
|
|
|
2113
|
+
/**
|
|
2114
|
+
* This return true if the message is for the current instance of this sub directory. As the sub directory
|
|
2115
|
+
* can be deleted and created again, then this finds if the message is for current instance of directory or not.
|
|
2116
|
+
* @param msg - message for the directory
|
|
2117
|
+
*/
|
|
2118
|
+
private isMessageForCurrentInstanceOfSubDirectory(msg: ISequencedDocumentMessage) {
|
|
2119
|
+
// If the message is either from the creator of directory or this directory was created when
|
|
2120
|
+
// container was detached or in case this directory is already live(known to other clients)
|
|
2121
|
+
// and the op was created after the directory was created then apply this op.
|
|
2122
|
+
return (
|
|
2123
|
+
this.clientIds.has(msg.clientId) ||
|
|
2124
|
+
this.clientIds.has("detached") ||
|
|
2125
|
+
(this.sequenceNumber !== -1 && this.sequenceNumber <= msg.referenceSequenceNumber)
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
1933
2129
|
/**
|
|
1934
2130
|
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
1935
2131
|
* not process the incoming operation.
|
|
@@ -1941,6 +2137,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1941
2137
|
* @returns True if the operation should be processed, false otherwise
|
|
1942
2138
|
*/
|
|
1943
2139
|
private needProcessSubDirectoryOperation(
|
|
2140
|
+
msg: ISequencedDocumentMessage,
|
|
1944
2141
|
op: IDirectorySubDirectoryOperation,
|
|
1945
2142
|
local: boolean,
|
|
1946
2143
|
localOpMetadata: unknown,
|
|
@@ -1962,6 +2159,44 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1962
2159
|
if (pendingMessageIds.length === 0) {
|
|
1963
2160
|
this.pendingSubDirectories.delete(op.subdirName);
|
|
1964
2161
|
}
|
|
2162
|
+
if (op.type === "deleteSubDirectory") {
|
|
2163
|
+
const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
|
|
2164
|
+
assert(
|
|
2165
|
+
count !== undefined && count > 0,
|
|
2166
|
+
0x5ac /* should have record for delete op */,
|
|
2167
|
+
);
|
|
2168
|
+
this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
|
|
2169
|
+
if (count === 1) {
|
|
2170
|
+
this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
} else if (op.type === "deleteSubDirectory") {
|
|
2174
|
+
// If this is remote delete op and we have keys in this subDirectory, then we need to delete these
|
|
2175
|
+
// keys except the pending ones as they will be sequenced after this delete.
|
|
2176
|
+
const subDirectory = this._subdirectories.get(op.subdirName);
|
|
2177
|
+
if (subDirectory) {
|
|
2178
|
+
subDirectory.clearExceptPendingKeys(local);
|
|
2179
|
+
// In case of remote delete op, we need to reset the creation seq number and client ids of
|
|
2180
|
+
// creators as the previous directory is getting deleted and we will initialize again when
|
|
2181
|
+
// we will receive op for the create again.
|
|
2182
|
+
subDirectory.sequenceNumber = -1;
|
|
2183
|
+
subDirectory.clientIds.clear();
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if (op.type === "createSubDirectory") {
|
|
2187
|
+
const dir = this._subdirectories.get(op.subdirName);
|
|
2188
|
+
if (dir?.sequenceNumber === -1) {
|
|
2189
|
+
// Only set the seq on the first message, could be more
|
|
2190
|
+
dir.sequenceNumber = msg.sequenceNumber;
|
|
2191
|
+
}
|
|
2192
|
+
// The client created the dir at or after the dirs seq, so list its client id as a creator.
|
|
2193
|
+
if (
|
|
2194
|
+
dir !== undefined &&
|
|
2195
|
+
!dir.clientIds.has(msg.clientId) &&
|
|
2196
|
+
dir.sequenceNumber <= msg.sequenceNumber
|
|
2197
|
+
) {
|
|
2198
|
+
dir.clientIds.add(msg.clientId);
|
|
2199
|
+
}
|
|
1965
2200
|
}
|
|
1966
2201
|
return false;
|
|
1967
2202
|
}
|
|
@@ -1978,8 +2213,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1978
2213
|
const temp = new Map<string, ILocalValue>();
|
|
1979
2214
|
|
|
1980
2215
|
for (const [key] of this.pendingKeys) {
|
|
1981
|
-
|
|
1982
|
-
|
|
2216
|
+
const value = this._storage.get(key);
|
|
2217
|
+
// If this key is already deleted, then we don't need to add it again.
|
|
2218
|
+
if (value !== undefined) {
|
|
2219
|
+
temp.set(key, value);
|
|
2220
|
+
}
|
|
1983
2221
|
}
|
|
1984
2222
|
|
|
1985
2223
|
this.clearCore(local);
|
|
@@ -2039,12 +2277,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2039
2277
|
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
2040
2278
|
* @param subdirName - The name of the subdirectory being created
|
|
2041
2279
|
* @param local - Whether the message originated from the local client
|
|
2280
|
+
* @param seq - Sequence number at which this directory is created
|
|
2281
|
+
* @param clientId - Id of client which created this directory.
|
|
2042
2282
|
* @returns - True if is newly created, false if it already existed.
|
|
2043
2283
|
*/
|
|
2044
|
-
private createSubDirectoryCore(
|
|
2045
|
-
|
|
2284
|
+
private createSubDirectoryCore(
|
|
2285
|
+
subdirName: string,
|
|
2286
|
+
local: boolean,
|
|
2287
|
+
seq: number,
|
|
2288
|
+
clientId: string,
|
|
2289
|
+
): boolean {
|
|
2290
|
+
const subdir = this._subdirectories.get(subdirName);
|
|
2291
|
+
if (subdir === undefined) {
|
|
2046
2292
|
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
2047
2293
|
const subDir = new SubDirectory(
|
|
2294
|
+
seq,
|
|
2295
|
+
new Set([clientId]),
|
|
2048
2296
|
this.directory,
|
|
2049
2297
|
this.runtime,
|
|
2050
2298
|
this.serializer,
|
|
@@ -2054,6 +2302,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2054
2302
|
this._subdirectories.set(subdirName, subDir);
|
|
2055
2303
|
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
2056
2304
|
return true;
|
|
2305
|
+
} else {
|
|
2306
|
+
subdir.clientIds.add(clientId);
|
|
2057
2307
|
}
|
|
2058
2308
|
return false;
|
|
2059
2309
|
}
|