@fluidframework/map 2.53.0 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/api-report/{map.legacy.alpha.api.md → map.legacy.beta.api.md} +15 -15
- package/dist/directory.d.ts +41 -91
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +448 -465
- package/dist/directory.js.map +1 -1
- package/dist/directoryFactory.d.ts +3 -6
- package/dist/directoryFactory.d.ts.map +1 -1
- package/dist/directoryFactory.js +2 -4
- package/dist/directoryFactory.js.map +1 -1
- package/dist/interfaces.d.ts +4 -8
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/internalInterfaces.d.ts +1 -2
- package/dist/internalInterfaces.d.ts.map +1 -1
- package/dist/internalInterfaces.js.map +1 -1
- package/dist/mapFactory.d.ts +3 -6
- package/dist/mapFactory.d.ts.map +1 -1
- package/dist/mapFactory.js +2 -4
- package/dist/mapFactory.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 +41 -91
- package/lib/directory.d.ts.map +1 -1
- package/lib/directory.js +441 -458
- package/lib/directory.js.map +1 -1
- package/lib/directoryFactory.d.ts +3 -6
- package/lib/directoryFactory.d.ts.map +1 -1
- package/lib/directoryFactory.js +2 -4
- package/lib/directoryFactory.js.map +1 -1
- package/lib/interfaces.d.ts +4 -8
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/interfaces.js.map +1 -1
- package/lib/internalInterfaces.d.ts +1 -2
- package/lib/internalInterfaces.d.ts.map +1 -1
- package/lib/internalInterfaces.js.map +1 -1
- package/lib/mapFactory.d.ts +3 -6
- package/lib/mapFactory.d.ts.map +1 -1
- package/lib/mapFactory.js +2 -4
- package/lib/mapFactory.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 +18 -19
- package/src/directory.ts +565 -574
- package/src/directoryFactory.ts +3 -6
- package/src/interfaces.ts +4 -8
- package/src/internalInterfaces.ts +1 -2
- package/src/mapFactory.ts +3 -6
- package/src/packageVersion.ts +1 -1
package/src/directory.ts
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
type ISequencedDocumentMessage,
|
|
16
16
|
} from "@fluidframework/driver-definitions/internal";
|
|
17
17
|
import { readAndParse } from "@fluidframework/driver-utils/internal";
|
|
18
|
-
import { RedBlackTree } from "@fluidframework/merge-tree/internal";
|
|
19
18
|
import type {
|
|
20
19
|
ISummaryTreeWithStats,
|
|
21
20
|
ITelemetryContext,
|
|
@@ -29,7 +28,9 @@ import {
|
|
|
29
28
|
parseHandles,
|
|
30
29
|
} from "@fluidframework/shared-object-base/internal";
|
|
31
30
|
import {
|
|
31
|
+
createChildMonitoringContext,
|
|
32
32
|
type ITelemetryLoggerExt,
|
|
33
|
+
type MonitoringContext,
|
|
33
34
|
UsageError,
|
|
34
35
|
} from "@fluidframework/telemetry-utils/internal";
|
|
35
36
|
import path from "path-browserify";
|
|
@@ -210,17 +211,20 @@ interface PendingKeySet {
|
|
|
210
211
|
type: "set";
|
|
211
212
|
path: string;
|
|
212
213
|
value: unknown;
|
|
214
|
+
subdir: SubDirectory;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
interface PendingKeyDelete {
|
|
216
218
|
type: "delete";
|
|
217
219
|
path: string;
|
|
218
220
|
key: string;
|
|
221
|
+
subdir: SubDirectory;
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
interface PendingClear {
|
|
222
225
|
type: "clear";
|
|
223
226
|
path: string;
|
|
227
|
+
subdir: SubDirectory;
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
/**
|
|
@@ -237,6 +241,7 @@ interface PendingKeyLifetime {
|
|
|
237
241
|
* must be removed from the pending data.
|
|
238
242
|
*/
|
|
239
243
|
keySets: PendingKeySet[];
|
|
244
|
+
subdir: SubDirectory;
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
/**
|
|
@@ -245,13 +250,26 @@ interface PendingKeyLifetime {
|
|
|
245
250
|
*/
|
|
246
251
|
type PendingStorageEntry = PendingKeyLifetime | PendingKeyDelete | PendingClear;
|
|
247
252
|
|
|
253
|
+
interface PendingSubDirectoryCreate {
|
|
254
|
+
type: "createSubDirectory";
|
|
255
|
+
subdirName: string;
|
|
256
|
+
subdir: SubDirectory;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface PendingSubDirectoryDelete {
|
|
260
|
+
type: "deleteSubDirectory";
|
|
261
|
+
subdirName: string;
|
|
262
|
+
subdir: SubDirectory;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type PendingSubDirectoryEntry = PendingSubDirectoryCreate | PendingSubDirectoryDelete;
|
|
266
|
+
|
|
248
267
|
/**
|
|
249
268
|
* Create info for the subdirectory.
|
|
250
269
|
*
|
|
251
270
|
* @deprecated - This interface will no longer be exported in the future(AB#8004).
|
|
252
271
|
*
|
|
253
|
-
* @legacy
|
|
254
|
-
* @alpha
|
|
272
|
+
* @legacy @beta
|
|
255
273
|
*/
|
|
256
274
|
export interface ICreateInfo {
|
|
257
275
|
/**
|
|
@@ -275,8 +293,7 @@ export interface ICreateInfo {
|
|
|
275
293
|
*
|
|
276
294
|
* @deprecated - This interface will no longer be exported in the future(AB#8004).
|
|
277
295
|
*
|
|
278
|
-
* @legacy
|
|
279
|
-
* @alpha
|
|
296
|
+
* @legacy @beta
|
|
280
297
|
*/
|
|
281
298
|
export interface IDirectoryDataObject {
|
|
282
299
|
/**
|
|
@@ -305,8 +322,7 @@ export interface IDirectoryDataObject {
|
|
|
305
322
|
*
|
|
306
323
|
* @deprecated - This interface will no longer be exported in the future(AB#8004).
|
|
307
324
|
*
|
|
308
|
-
* @legacy
|
|
309
|
-
* @alpha
|
|
325
|
+
* @legacy @beta
|
|
310
326
|
*/
|
|
311
327
|
export interface IDirectoryNewStorageFormat {
|
|
312
328
|
/**
|
|
@@ -370,69 +386,6 @@ interface SequenceData {
|
|
|
370
386
|
clientSeq?: number;
|
|
371
387
|
}
|
|
372
388
|
|
|
373
|
-
/**
|
|
374
|
-
* A utility class for tracking associations between keys and their creation indices.
|
|
375
|
-
* This is relevant to support map iteration in insertion order, see
|
|
376
|
-
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
|
|
377
|
-
*
|
|
378
|
-
* TODO: It can be combined with the creation tracker utilized in SharedMap
|
|
379
|
-
*/
|
|
380
|
-
class DirectoryCreationTracker {
|
|
381
|
-
public readonly indexToKey: RedBlackTree<SequenceData, string>;
|
|
382
|
-
|
|
383
|
-
public readonly keyToIndex: Map<string, SequenceData>;
|
|
384
|
-
|
|
385
|
-
public constructor() {
|
|
386
|
-
this.indexToKey = new RedBlackTree<SequenceData, string>(seqDataComparator);
|
|
387
|
-
this.keyToIndex = new Map<string, SequenceData>();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
public set(key: string, seqData: SequenceData): void {
|
|
391
|
-
this.indexToKey.put(seqData, key);
|
|
392
|
-
this.keyToIndex.set(key, seqData);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
public has(keyOrSeqData: string | SequenceData): boolean {
|
|
396
|
-
return typeof keyOrSeqData === "string"
|
|
397
|
-
? this.keyToIndex.has(keyOrSeqData)
|
|
398
|
-
: this.indexToKey.get(keyOrSeqData) !== undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
public delete(keyOrSeqData: string | SequenceData): void {
|
|
402
|
-
if (this.has(keyOrSeqData)) {
|
|
403
|
-
if (typeof keyOrSeqData === "string") {
|
|
404
|
-
const seqData = this.keyToIndex.get(keyOrSeqData) as SequenceData;
|
|
405
|
-
this.keyToIndex.delete(keyOrSeqData);
|
|
406
|
-
this.indexToKey.remove(seqData);
|
|
407
|
-
} else {
|
|
408
|
-
const key = this.indexToKey.get(keyOrSeqData)?.data as string;
|
|
409
|
-
this.indexToKey.remove(keyOrSeqData);
|
|
410
|
-
this.keyToIndex.delete(key);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Retrieves all subdirectories with creation order that satisfy an optional constraint function.
|
|
417
|
-
* @param constraint - An optional constraint function that filters keys.
|
|
418
|
-
* @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
|
|
419
|
-
*/
|
|
420
|
-
public keys(constraint?: (key: string) => boolean): string[] {
|
|
421
|
-
const keys: string[] = [];
|
|
422
|
-
this.indexToKey.mapRange((node) => {
|
|
423
|
-
if (!constraint || constraint(node.data)) {
|
|
424
|
-
keys.push(node.data);
|
|
425
|
-
}
|
|
426
|
-
return true;
|
|
427
|
-
}, keys);
|
|
428
|
-
return keys;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
public get size(): number {
|
|
432
|
-
return this.keyToIndex.size;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
389
|
/**
|
|
437
390
|
* {@inheritDoc ISharedDirectory}
|
|
438
391
|
*
|
|
@@ -674,6 +627,27 @@ export class SharedDirectory
|
|
|
674
627
|
return currentSubDir;
|
|
675
628
|
}
|
|
676
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Similar to `getWorkingDirectory`, but only returns directories that are sequenced.
|
|
632
|
+
* This can be useful for op processing since we only process ops on sequenced directories.
|
|
633
|
+
*/
|
|
634
|
+
private getSequencedWorkingDirectory(relativePath: string): IDirectory | undefined {
|
|
635
|
+
const absolutePath = this.makeAbsolute(relativePath);
|
|
636
|
+
if (absolutePath === posix.sep) {
|
|
637
|
+
return this.root;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
let currentSubDir: SubDirectory | undefined = this.root;
|
|
641
|
+
const subdirs = absolutePath.slice(1).split(posix.sep);
|
|
642
|
+
for (const subdir of subdirs) {
|
|
643
|
+
currentSubDir = currentSubDir.sequencedSubdirectories.get(subdir);
|
|
644
|
+
if (!currentSubDir) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return currentSubDir;
|
|
649
|
+
}
|
|
650
|
+
|
|
677
651
|
/**
|
|
678
652
|
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
|
|
679
653
|
*/
|
|
@@ -791,10 +765,6 @@ export class SharedDirectory
|
|
|
791
765
|
this.logger,
|
|
792
766
|
);
|
|
793
767
|
currentSubDir.populateSubDirectory(subdirName, newSubDir);
|
|
794
|
-
// Record the newly inserted subdirectory to the creation tracker
|
|
795
|
-
currentSubDir.ackedCreationSeqTracker.set(subdirName, {
|
|
796
|
-
...seqData,
|
|
797
|
-
});
|
|
798
768
|
}
|
|
799
769
|
stack.push([newSubDir, subdirObject]);
|
|
800
770
|
}
|
|
@@ -856,34 +826,16 @@ export class SharedDirectory
|
|
|
856
826
|
return posix.resolve(posix.sep, relativePath);
|
|
857
827
|
}
|
|
858
828
|
|
|
859
|
-
/**
|
|
860
|
-
* This checks if there is pending delete op for local delete for a any subdir in the relative path.
|
|
861
|
-
* @param relativePath - path of sub directory.
|
|
862
|
-
* @returns `true` if there is pending delete, `false` otherwise.
|
|
863
|
-
*/
|
|
864
|
-
private isSubDirectoryDeletePending(relativePath: string): boolean {
|
|
865
|
-
const absolutePath = this.makeAbsolute(relativePath);
|
|
866
|
-
if (absolutePath === posix.sep) {
|
|
867
|
-
return false;
|
|
868
|
-
}
|
|
869
|
-
let currentParent = this.root;
|
|
870
|
-
const pathParts = absolutePath.split(posix.sep).slice(1);
|
|
871
|
-
for (const dirName of pathParts) {
|
|
872
|
-
if (currentParent.isSubDirectoryDeletePending(dirName)) {
|
|
873
|
-
return true;
|
|
874
|
-
}
|
|
875
|
-
currentParent = currentParent.getSubDirectory(dirName) as SubDirectory;
|
|
876
|
-
if (currentParent === undefined) {
|
|
877
|
-
return true;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
829
|
/**
|
|
884
830
|
* Set the message handlers for the directory.
|
|
885
831
|
*/
|
|
886
832
|
private setMessageHandlers(): void {
|
|
833
|
+
// Notes on how we target the correct subdirectory:
|
|
834
|
+
// `process`: When processing ops, we only ever want to process ops on sequenced directories. This prevents
|
|
835
|
+
// scenarios where ops could be processed on a pending directory instead of a sequenced directory,
|
|
836
|
+
// leading to ops effectively being processed out of order.
|
|
837
|
+
// `resubmit`: When resubmitting ops, we use `localOpMetadata` to get a reference to the subdirectory that
|
|
838
|
+
// the op was originally targeting.
|
|
887
839
|
this.messageHandlers.set("clear", {
|
|
888
840
|
process: (
|
|
889
841
|
msg: ISequencedDocumentMessage,
|
|
@@ -891,17 +843,15 @@ export class SharedDirectory
|
|
|
891
843
|
local: boolean,
|
|
892
844
|
localOpMetadata: ClearLocalOpMetadata | undefined,
|
|
893
845
|
) => {
|
|
894
|
-
const subdir = this.
|
|
895
|
-
|
|
896
|
-
// as we are going to delete this subDirectory.
|
|
897
|
-
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
846
|
+
const subdir = this.getSequencedWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
847
|
+
if (subdir !== undefined && !subdir?.disposed) {
|
|
898
848
|
subdir.processClearMessage(msg, op, local, localOpMetadata);
|
|
899
849
|
}
|
|
900
850
|
},
|
|
901
851
|
resubmit: (op: IDirectoryClearOperation, localOpMetadata: ClearLocalOpMetadata) => {
|
|
902
|
-
const
|
|
903
|
-
if (
|
|
904
|
-
|
|
852
|
+
const targetSubdir = localOpMetadata.subdir;
|
|
853
|
+
if (!targetSubdir.disposed) {
|
|
854
|
+
targetSubdir.resubmitClearMessage(op, localOpMetadata);
|
|
905
855
|
}
|
|
906
856
|
},
|
|
907
857
|
});
|
|
@@ -912,17 +862,15 @@ export class SharedDirectory
|
|
|
912
862
|
local: boolean,
|
|
913
863
|
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
914
864
|
) => {
|
|
915
|
-
const subdir = this.
|
|
916
|
-
|
|
917
|
-
// as we are going to delete this subDirectory.
|
|
918
|
-
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
865
|
+
const subdir = this.getSequencedWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
866
|
+
if (subdir !== undefined && !subdir?.disposed) {
|
|
919
867
|
subdir.processDeleteMessage(msg, op, local, localOpMetadata);
|
|
920
868
|
}
|
|
921
869
|
},
|
|
922
870
|
resubmit: (op: IDirectoryDeleteOperation, localOpMetadata: EditLocalOpMetadata) => {
|
|
923
|
-
const
|
|
924
|
-
if (
|
|
925
|
-
|
|
871
|
+
const targetSubdir = localOpMetadata.subdir;
|
|
872
|
+
if (!targetSubdir.disposed) {
|
|
873
|
+
targetSubdir.resubmitKeyMessage(op, localOpMetadata);
|
|
926
874
|
}
|
|
927
875
|
},
|
|
928
876
|
});
|
|
@@ -933,19 +881,17 @@ export class SharedDirectory
|
|
|
933
881
|
local: boolean,
|
|
934
882
|
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
935
883
|
) => {
|
|
936
|
-
const subdir = this.
|
|
937
|
-
|
|
938
|
-
// as we are going to delete this subDirectory.
|
|
939
|
-
if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
|
|
884
|
+
const subdir = this.getSequencedWorkingDirectory(op.path) as SubDirectory | undefined;
|
|
885
|
+
if (subdir !== undefined && !subdir?.disposed) {
|
|
940
886
|
migrateIfSharedSerializable(op.value, this.serializer, this.handle);
|
|
941
887
|
const localValue: unknown = local ? undefined : op.value.value;
|
|
942
888
|
subdir.processSetMessage(msg, op, localValue, local, localOpMetadata);
|
|
943
889
|
}
|
|
944
890
|
},
|
|
945
891
|
resubmit: (op: IDirectorySetOperation, localOpMetadata: EditLocalOpMetadata) => {
|
|
946
|
-
const
|
|
947
|
-
if (
|
|
948
|
-
|
|
892
|
+
const targetSubdir = localOpMetadata.subdir;
|
|
893
|
+
if (!targetSubdir.disposed) {
|
|
894
|
+
targetSubdir.resubmitKeyMessage(op, localOpMetadata);
|
|
949
895
|
}
|
|
950
896
|
},
|
|
951
897
|
});
|
|
@@ -957,10 +903,10 @@ export class SharedDirectory
|
|
|
957
903
|
local: boolean,
|
|
958
904
|
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
959
905
|
) => {
|
|
960
|
-
const parentSubdir = this.
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if (parentSubdir && !
|
|
906
|
+
const parentSubdir = this.getSequencedWorkingDirectory(op.path) as
|
|
907
|
+
| SubDirectory
|
|
908
|
+
| undefined;
|
|
909
|
+
if (parentSubdir !== undefined && !parentSubdir?.disposed) {
|
|
964
910
|
parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
965
911
|
}
|
|
966
912
|
},
|
|
@@ -968,10 +914,10 @@ export class SharedDirectory
|
|
|
968
914
|
op: IDirectoryCreateSubDirectoryOperation,
|
|
969
915
|
localOpMetadata: SubDirLocalOpMetadata,
|
|
970
916
|
) => {
|
|
971
|
-
const
|
|
972
|
-
if (
|
|
917
|
+
const targetSubdir = localOpMetadata.parentSubdir;
|
|
918
|
+
if (!targetSubdir.disposed) {
|
|
973
919
|
// We don't reuse the metadata but send a new one on each submit.
|
|
974
|
-
|
|
920
|
+
targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
975
921
|
}
|
|
976
922
|
},
|
|
977
923
|
});
|
|
@@ -983,10 +929,10 @@ export class SharedDirectory
|
|
|
983
929
|
local: boolean,
|
|
984
930
|
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
985
931
|
) => {
|
|
986
|
-
const parentSubdir = this.
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (parentSubdir && !
|
|
932
|
+
const parentSubdir = this.getSequencedWorkingDirectory(op.path) as
|
|
933
|
+
| SubDirectory
|
|
934
|
+
| undefined;
|
|
935
|
+
if (parentSubdir !== undefined && !parentSubdir?.disposed) {
|
|
990
936
|
parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
|
|
991
937
|
}
|
|
992
938
|
},
|
|
@@ -994,10 +940,10 @@ export class SharedDirectory
|
|
|
994
940
|
op: IDirectoryDeleteSubDirectoryOperation,
|
|
995
941
|
localOpMetadata: SubDirLocalOpMetadata,
|
|
996
942
|
) => {
|
|
997
|
-
const
|
|
998
|
-
if (
|
|
943
|
+
const targetSubdir = localOpMetadata.parentSubdir;
|
|
944
|
+
if (!targetSubdir.disposed) {
|
|
999
945
|
// We don't reuse the metadata but send a new one on each submit.
|
|
1000
|
-
|
|
946
|
+
targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
|
|
1001
947
|
}
|
|
1002
948
|
},
|
|
1003
949
|
});
|
|
@@ -1107,11 +1053,13 @@ export class SharedDirectory
|
|
|
1107
1053
|
|
|
1108
1054
|
interface ICreateSubDirLocalOpMetadata {
|
|
1109
1055
|
type: "createSubDir";
|
|
1056
|
+
parentSubdir: SubDirectory;
|
|
1110
1057
|
}
|
|
1111
1058
|
|
|
1112
1059
|
interface IDeleteSubDirLocalOpMetadata {
|
|
1113
1060
|
type: "deleteSubDir";
|
|
1114
1061
|
subDirectory: SubDirectory | undefined;
|
|
1062
|
+
parentSubdir: SubDirectory;
|
|
1115
1063
|
}
|
|
1116
1064
|
|
|
1117
1065
|
type SubDirLocalOpMetadata = ICreateSubDirLocalOpMetadata | IDeleteSubDirLocalOpMetadata;
|
|
@@ -1132,8 +1080,6 @@ function assertNonNullClientId(clientId: string | null): asserts clientId is str
|
|
|
1132
1080
|
assert(clientId !== null, 0x6af /* client id should never be null */);
|
|
1133
1081
|
}
|
|
1134
1082
|
|
|
1135
|
-
let hasLoggedDirectoryInconsistency = false;
|
|
1136
|
-
|
|
1137
1083
|
/**
|
|
1138
1084
|
* Node of the directory tree.
|
|
1139
1085
|
* @sealed
|
|
@@ -1150,23 +1096,10 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1150
1096
|
public [Symbol.toStringTag]: string = "SubDirectory";
|
|
1151
1097
|
|
|
1152
1098
|
/**
|
|
1153
|
-
* The subdirectories the directory is holding
|
|
1099
|
+
* The sequenced subdirectories the directory is holding independent of any pending
|
|
1100
|
+
* create/delete subdirectory operations.
|
|
1154
1101
|
*/
|
|
1155
|
-
private readonly
|
|
1156
|
-
|
|
1157
|
-
/**
|
|
1158
|
-
* Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
|
|
1159
|
-
* of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
1160
|
-
* deleted sub directory.
|
|
1161
|
-
*/
|
|
1162
|
-
private readonly pendingDeleteSubDirectoriesTracker = new Map<string, number>();
|
|
1163
|
-
|
|
1164
|
-
/**
|
|
1165
|
-
* Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
|
|
1166
|
-
* of create op that are pending or yet to be acked from server. This is maintained just to track the locally
|
|
1167
|
-
* created sub directory.
|
|
1168
|
-
*/
|
|
1169
|
-
private readonly pendingCreateSubDirectoriesTracker = new Map<string, number>();
|
|
1102
|
+
private readonly _sequencedSubdirectories = new Map<string, SubDirectory>();
|
|
1170
1103
|
|
|
1171
1104
|
/**
|
|
1172
1105
|
* Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
|
|
@@ -1174,16 +1107,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1174
1107
|
*/
|
|
1175
1108
|
public localCreationSeq: number = 0;
|
|
1176
1109
|
|
|
1177
|
-
|
|
1178
|
-
* Maintains a bidirectional association between ack'd subdirectories and their seqData.
|
|
1179
|
-
* This helps to ensure iteration order which is consistent with the JS map spec.
|
|
1180
|
-
*/
|
|
1181
|
-
public readonly ackedCreationSeqTracker: DirectoryCreationTracker;
|
|
1182
|
-
|
|
1183
|
-
/**
|
|
1184
|
-
* Similar to {@link ackedCreationSeqTracker}, but for local (unacked) entries.
|
|
1185
|
-
*/
|
|
1186
|
-
public readonly localCreationSeqTracker: DirectoryCreationTracker;
|
|
1110
|
+
private readonly mc: MonitoringContext;
|
|
1187
1111
|
|
|
1188
1112
|
/**
|
|
1189
1113
|
* Constructor.
|
|
@@ -1201,11 +1125,10 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1201
1125
|
private readonly runtime: IFluidDataStoreRuntime,
|
|
1202
1126
|
private readonly serializer: IFluidSerializer,
|
|
1203
1127
|
public readonly absolutePath: string,
|
|
1204
|
-
|
|
1128
|
+
logger: ITelemetryLoggerExt,
|
|
1205
1129
|
) {
|
|
1206
1130
|
super();
|
|
1207
|
-
this.
|
|
1208
|
-
this.ackedCreationSeqTracker = new DirectoryCreationTracker();
|
|
1131
|
+
this.mc = createChildMonitoringContext({ logger, namespace: "Directory" });
|
|
1209
1132
|
}
|
|
1210
1133
|
|
|
1211
1134
|
public dispose(error?: Error): void {
|
|
@@ -1259,8 +1182,13 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1259
1182
|
}
|
|
1260
1183
|
const previousOptimisticLocalValue = this.getOptimisticValue(key);
|
|
1261
1184
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1185
|
+
const detachedBind =
|
|
1186
|
+
this.mc.config.getBoolean("Fluid.Directory.AllowDetachedResolve") ?? false;
|
|
1187
|
+
if (detachedBind) {
|
|
1188
|
+
// Create a local value and serialize it.
|
|
1189
|
+
// AB#47081: This will be removed once we can validate that it is no longer needed.
|
|
1190
|
+
bindHandles(value, this.serializer, this.directory.handle);
|
|
1191
|
+
}
|
|
1264
1192
|
|
|
1265
1193
|
// If we are not attached, don't submit the op.
|
|
1266
1194
|
if (!this.directory.isAttached()) {
|
|
@@ -1292,13 +1220,20 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1292
1220
|
latestPendingEntry.type === "delete" ||
|
|
1293
1221
|
latestPendingEntry.type === "clear"
|
|
1294
1222
|
) {
|
|
1295
|
-
latestPendingEntry = {
|
|
1223
|
+
latestPendingEntry = {
|
|
1224
|
+
type: "lifetime",
|
|
1225
|
+
path: this.absolutePath,
|
|
1226
|
+
key,
|
|
1227
|
+
keySets: [],
|
|
1228
|
+
subdir: this,
|
|
1229
|
+
};
|
|
1296
1230
|
this.pendingStorageData.push(latestPendingEntry);
|
|
1297
1231
|
}
|
|
1298
1232
|
const pendingKeySet: PendingKeySet = {
|
|
1299
1233
|
type: "set",
|
|
1300
1234
|
path: this.absolutePath,
|
|
1301
1235
|
value,
|
|
1236
|
+
subdir: this,
|
|
1302
1237
|
};
|
|
1303
1238
|
latestPendingEntry.keySets.push(pendingKeySet);
|
|
1304
1239
|
|
|
@@ -1328,7 +1263,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1328
1263
|
* {@inheritDoc IDirectory.countSubDirectory}
|
|
1329
1264
|
*/
|
|
1330
1265
|
public countSubDirectory(): number {
|
|
1331
|
-
return this.
|
|
1266
|
+
return [...this.subdirectories()].length;
|
|
1332
1267
|
}
|
|
1333
1268
|
|
|
1334
1269
|
/**
|
|
@@ -1345,31 +1280,57 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1345
1280
|
throw new Error(`SubDirectory name may not contain ${posix.sep}`);
|
|
1346
1281
|
}
|
|
1347
1282
|
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
this.getLocalSeq(),
|
|
1353
|
-
this.runtime.clientId ?? "detached",
|
|
1354
|
-
);
|
|
1355
|
-
const subDir = this._subdirectories.get(subdirName);
|
|
1356
|
-
assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
|
|
1283
|
+
let subDir = this.getOptimisticSubDirectory(subdirName, true);
|
|
1284
|
+
const seqData = this.getLocalSeq();
|
|
1285
|
+
const clientId = this.runtime.clientId ?? "detached";
|
|
1286
|
+
const isNewSubDirectory = subDir === undefined;
|
|
1357
1287
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1288
|
+
if (subDir === undefined) {
|
|
1289
|
+
// If we do not have optimistically have this subdirectory yet, we should create a new one
|
|
1290
|
+
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
1291
|
+
subDir = new SubDirectory(
|
|
1292
|
+
{ ...seqData },
|
|
1293
|
+
new Set([clientId]),
|
|
1294
|
+
this.directory,
|
|
1295
|
+
this.runtime,
|
|
1296
|
+
this.serializer,
|
|
1297
|
+
absolutePath,
|
|
1298
|
+
this.mc.logger,
|
|
1299
|
+
);
|
|
1300
|
+
} else {
|
|
1301
|
+
if (subDir.disposed) {
|
|
1302
|
+
// In the case that the subdir exists but is disposed, we should
|
|
1303
|
+
// still use the existing subdir to maintain any pending changes but
|
|
1304
|
+
// ensure it is no longer disposed.
|
|
1305
|
+
this.undisposeSubdirectoryTree(subDir);
|
|
1306
|
+
}
|
|
1307
|
+
subDir.clientIds.add(clientId);
|
|
1361
1308
|
}
|
|
1362
1309
|
|
|
1363
|
-
|
|
1364
|
-
if (isNew) {
|
|
1365
|
-
const op: IDirectoryCreateSubDirectoryOperation = {
|
|
1366
|
-
path: this.absolutePath,
|
|
1367
|
-
subdirName,
|
|
1368
|
-
type: "createSubDirectory",
|
|
1369
|
-
};
|
|
1370
|
-
this.submitCreateSubDirectoryMessage(op);
|
|
1371
|
-
}
|
|
1310
|
+
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
1372
1311
|
|
|
1312
|
+
// Only submit the op/emit event if we actually created a new subdir.
|
|
1313
|
+
if (isNewSubDirectory) {
|
|
1314
|
+
if (this.directory.isAttached()) {
|
|
1315
|
+
const pendingSubDirectoryCreate: PendingSubDirectoryCreate = {
|
|
1316
|
+
type: "createSubDirectory",
|
|
1317
|
+
subdirName,
|
|
1318
|
+
subdir: subDir,
|
|
1319
|
+
};
|
|
1320
|
+
this.pendingSubDirectoryData.push(pendingSubDirectoryCreate);
|
|
1321
|
+
const op: IDirectoryCreateSubDirectoryOperation = {
|
|
1322
|
+
subdirName,
|
|
1323
|
+
path: this.absolutePath,
|
|
1324
|
+
type: "createSubDirectory",
|
|
1325
|
+
};
|
|
1326
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
1327
|
+
} else {
|
|
1328
|
+
// If we are detached, don't submit the op and directly commit
|
|
1329
|
+
// the subdir to _sequencedSubdirectories.
|
|
1330
|
+
this._sequencedSubdirectories.set(subdirName, subDir);
|
|
1331
|
+
}
|
|
1332
|
+
this.emit("subDirectoryCreated", subdirName, true, this);
|
|
1333
|
+
}
|
|
1373
1334
|
return subDir;
|
|
1374
1335
|
}
|
|
1375
1336
|
|
|
@@ -1395,7 +1356,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1395
1356
|
*/
|
|
1396
1357
|
public getSubDirectory(subdirName: string): IDirectory | undefined {
|
|
1397
1358
|
this.throwIfDisposed();
|
|
1398
|
-
return this.
|
|
1359
|
+
return this.getOptimisticSubDirectory(subdirName);
|
|
1399
1360
|
}
|
|
1400
1361
|
|
|
1401
1362
|
/**
|
|
@@ -1403,7 +1364,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1403
1364
|
*/
|
|
1404
1365
|
public hasSubDirectory(subdirName: string): boolean {
|
|
1405
1366
|
this.throwIfDisposed();
|
|
1406
|
-
return this.
|
|
1367
|
+
return this.getOptimisticSubDirectory(subdirName) !== undefined;
|
|
1407
1368
|
}
|
|
1408
1369
|
|
|
1409
1370
|
/**
|
|
@@ -1411,25 +1372,41 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1411
1372
|
*/
|
|
1412
1373
|
public deleteSubDirectory(subdirName: string): boolean {
|
|
1413
1374
|
this.throwIfDisposed();
|
|
1414
|
-
// Delete the sub directory locally first.
|
|
1415
|
-
const subDir = this.deleteSubDirectoryCore(subdirName, true);
|
|
1416
1375
|
|
|
1417
|
-
// If we are not attached, don't submit the op.
|
|
1418
1376
|
if (!this.directory.isAttached()) {
|
|
1419
|
-
|
|
1377
|
+
const previousValue = this._sequencedSubdirectories.get(subdirName);
|
|
1378
|
+
const successfullyRemoved = this._sequencedSubdirectories.delete(subdirName);
|
|
1379
|
+
// Only emit if we actually deleted something.
|
|
1380
|
+
if (successfullyRemoved) {
|
|
1381
|
+
this.disposeSubDirectoryTree(previousValue);
|
|
1382
|
+
this.emit("subDirectoryDeleted", subdirName, true, this);
|
|
1383
|
+
}
|
|
1384
|
+
return successfullyRemoved;
|
|
1420
1385
|
}
|
|
1421
1386
|
|
|
1422
|
-
|
|
1423
|
-
if (
|
|
1424
|
-
|
|
1425
|
-
path: this.absolutePath,
|
|
1426
|
-
subdirName,
|
|
1427
|
-
type: "deleteSubDirectory",
|
|
1428
|
-
};
|
|
1429
|
-
|
|
1430
|
-
this.submitDeleteSubDirectoryMessage(op, subDir);
|
|
1387
|
+
const previousOptimisticSubDirectory = this.getOptimisticSubDirectory(subdirName);
|
|
1388
|
+
if (previousOptimisticSubDirectory === undefined) {
|
|
1389
|
+
return false;
|
|
1431
1390
|
}
|
|
1432
|
-
|
|
1391
|
+
const pendingSubdirDelete: PendingSubDirectoryDelete = {
|
|
1392
|
+
type: "deleteSubDirectory",
|
|
1393
|
+
subdirName,
|
|
1394
|
+
subdir: this,
|
|
1395
|
+
};
|
|
1396
|
+
this.pendingSubDirectoryData.push(pendingSubdirDelete);
|
|
1397
|
+
|
|
1398
|
+
const op: IDirectoryOperation = {
|
|
1399
|
+
subdirName,
|
|
1400
|
+
type: "deleteSubDirectory",
|
|
1401
|
+
path: this.absolutePath,
|
|
1402
|
+
};
|
|
1403
|
+
this.submitDeleteSubDirectoryMessage(op, previousOptimisticSubDirectory);
|
|
1404
|
+
this.emit("subDirectoryDeleted", subdirName, true, this);
|
|
1405
|
+
// We don't want to fully dispose the subdir tree since this is only a pending
|
|
1406
|
+
// local delete. Instead we will only emit the dispose event to reflect the
|
|
1407
|
+
// local state.
|
|
1408
|
+
this.emitDisposeForSubdirTree(previousOptimisticSubDirectory);
|
|
1409
|
+
return true;
|
|
1433
1410
|
}
|
|
1434
1411
|
|
|
1435
1412
|
/**
|
|
@@ -1437,52 +1414,40 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1437
1414
|
*/
|
|
1438
1415
|
public subdirectories(): IterableIterator<[string, IDirectory]> {
|
|
1439
1416
|
this.throwIfDisposed();
|
|
1440
|
-
const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
|
|
1441
|
-
const localSubdirsInOrder = this.localCreationSeqTracker.keys(
|
|
1442
|
-
(key) => !this.ackedCreationSeqTracker.has(key),
|
|
1443
|
-
);
|
|
1444
1417
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
if (
|
|
1453
|
-
|
|
1454
|
-
eventName: "inconsistentSubdirectoryOrdering",
|
|
1455
|
-
localKeyCount: this.localCreationSeqTracker.size,
|
|
1456
|
-
ackedKeyCount: this.ackedCreationSeqTracker.size,
|
|
1457
|
-
subdirNamesLength: subdirNames.length,
|
|
1458
|
-
subdirectoriesSize: this._subdirectories.size,
|
|
1459
|
-
});
|
|
1460
|
-
hasLoggedDirectoryInconsistency = true;
|
|
1418
|
+
// subdirectories() should reflect the optimistic state of subdirectories.
|
|
1419
|
+
// This means that we should return both sequenced and pending subdirectories
|
|
1420
|
+
// that do not also have a pending deletion.
|
|
1421
|
+
const sequencedSubdirs: [string, SubDirectory][] = [];
|
|
1422
|
+
const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
|
|
1423
|
+
for (const subdirName of sequencedSubdirNames) {
|
|
1424
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
|
|
1425
|
+
if (optimisticSubdir !== undefined) {
|
|
1426
|
+
sequencedSubdirs.push([subdirName, optimisticSubdir]);
|
|
1461
1427
|
}
|
|
1462
|
-
|
|
1463
|
-
return this._subdirectories.entries();
|
|
1464
1428
|
}
|
|
1429
|
+
const pendingSubdirNames = [
|
|
1430
|
+
...new Set(
|
|
1431
|
+
this.pendingSubDirectoryData
|
|
1432
|
+
.map((entry) => entry.subdirName)
|
|
1433
|
+
.filter((subdirName) => !sequencedSubdirNames.has(subdirName)),
|
|
1434
|
+
),
|
|
1435
|
+
];
|
|
1436
|
+
const pendingSubdirs: [string, SubDirectory][] = [];
|
|
1437
|
+
for (const subdirName of pendingSubdirNames) {
|
|
1438
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
|
|
1439
|
+
if (optimisticSubdir !== undefined) {
|
|
1440
|
+
pendingSubdirs.push([subdirName, optimisticSubdir]);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
|
|
1444
|
+
const orderedSubdirs = allSubdirs.sort((a, b) => {
|
|
1445
|
+
const aSeqData = a[1].seqData;
|
|
1446
|
+
const bSeqData = b[1].seqData;
|
|
1447
|
+
return seqDataComparator(aSeqData, bSeqData);
|
|
1448
|
+
});
|
|
1465
1449
|
|
|
1466
|
-
|
|
1467
|
-
index: 0,
|
|
1468
|
-
dirs: this._subdirectories,
|
|
1469
|
-
next(): IteratorResult<[string, IDirectory]> {
|
|
1470
|
-
if (this.index < subdirNames.length) {
|
|
1471
|
-
// Bounds check above guarantees non-null (at least at compile time, assuming all types are respected)
|
|
1472
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1473
|
-
const subdirName = subdirNames[this.index++]!;
|
|
1474
|
-
const subdir = this.dirs.get(subdirName);
|
|
1475
|
-
assert(subdir !== undefined, 0x8ac /* Could not find expected sub-directory. */);
|
|
1476
|
-
return { value: [subdirName, subdir], done: false };
|
|
1477
|
-
}
|
|
1478
|
-
return { value: undefined, done: true };
|
|
1479
|
-
},
|
|
1480
|
-
[Symbol.iterator](): IterableIterator<[string, IDirectory]> {
|
|
1481
|
-
return this;
|
|
1482
|
-
},
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
return entriesIterator;
|
|
1450
|
+
return orderedSubdirs[Symbol.iterator]();
|
|
1486
1451
|
}
|
|
1487
1452
|
|
|
1488
1453
|
/**
|
|
@@ -1499,10 +1464,10 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1499
1464
|
* @returns true if there is pending delete.
|
|
1500
1465
|
*/
|
|
1501
1466
|
public isSubDirectoryDeletePending(subDirName: string): boolean {
|
|
1502
|
-
|
|
1503
|
-
return
|
|
1504
|
-
}
|
|
1505
|
-
return
|
|
1467
|
+
const lastPendingEntry = findLast(this.pendingSubDirectoryData, (entry) => {
|
|
1468
|
+
return entry.subdirName === subDirName && entry.type === "deleteSubDirectory";
|
|
1469
|
+
});
|
|
1470
|
+
return lastPendingEntry !== undefined;
|
|
1506
1471
|
}
|
|
1507
1472
|
|
|
1508
1473
|
/**
|
|
@@ -1537,6 +1502,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1537
1502
|
type: "delete",
|
|
1538
1503
|
path: this.absolutePath,
|
|
1539
1504
|
key,
|
|
1505
|
+
subdir: this,
|
|
1540
1506
|
};
|
|
1541
1507
|
this.pendingStorageData.push(pendingKeyDelete);
|
|
1542
1508
|
|
|
@@ -1580,6 +1546,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1580
1546
|
const pendingClear: PendingClear = {
|
|
1581
1547
|
type: "clear",
|
|
1582
1548
|
path: this.absolutePath,
|
|
1549
|
+
subdir: this,
|
|
1583
1550
|
};
|
|
1584
1551
|
this.pendingStorageData.push(pendingClear);
|
|
1585
1552
|
|
|
@@ -1600,7 +1567,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1600
1567
|
): void {
|
|
1601
1568
|
this.throwIfDisposed();
|
|
1602
1569
|
for (const [key, localValue] of this.internalIterator()) {
|
|
1603
|
-
callback(
|
|
1570
|
+
callback(localValue, key, this);
|
|
1604
1571
|
}
|
|
1605
1572
|
}
|
|
1606
1573
|
|
|
@@ -1710,6 +1677,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1710
1677
|
*/
|
|
1711
1678
|
private readonly pendingStorageData: PendingStorageEntry[] = [];
|
|
1712
1679
|
|
|
1680
|
+
/**
|
|
1681
|
+
* A data structure containing all local pending subdirectory create/deletes, which is used in combination
|
|
1682
|
+
* with the _sequencedSubdirectories to compute optimistic values.
|
|
1683
|
+
*/
|
|
1684
|
+
private readonly pendingSubDirectoryData: PendingSubDirectoryEntry[] = [];
|
|
1685
|
+
|
|
1713
1686
|
/**
|
|
1714
1687
|
* An internal iterator that iterates over the entries in the directory.
|
|
1715
1688
|
*/
|
|
@@ -1821,6 +1794,44 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1821
1794
|
: latestPendingEntry.type === "lifetime";
|
|
1822
1795
|
};
|
|
1823
1796
|
|
|
1797
|
+
/**
|
|
1798
|
+
* Get the optimistic local subdirectory. This combines the sequenced data with
|
|
1799
|
+
* any pending changes that have not yet been sequenced. By default, we do not
|
|
1800
|
+
* consider disposed directories as optimistically existing, but if `getIfDisposed`
|
|
1801
|
+
* is true, we will include them since some scenarios require this.
|
|
1802
|
+
*/
|
|
1803
|
+
private readonly getOptimisticSubDirectory = (
|
|
1804
|
+
subdirName: string,
|
|
1805
|
+
getIfDisposed: boolean = false,
|
|
1806
|
+
): SubDirectory | undefined => {
|
|
1807
|
+
const latestPendingEntry = findLast(
|
|
1808
|
+
this.pendingSubDirectoryData,
|
|
1809
|
+
(entry) => entry.subdirName === subdirName,
|
|
1810
|
+
);
|
|
1811
|
+
let subdir: SubDirectory | undefined;
|
|
1812
|
+
if (latestPendingEntry === undefined) {
|
|
1813
|
+
subdir = this._sequencedSubdirectories.get(subdirName);
|
|
1814
|
+
} else if (latestPendingEntry.type === "createSubDirectory") {
|
|
1815
|
+
subdir = latestPendingEntry.subdir;
|
|
1816
|
+
assert(subdir !== undefined, 0xc2f /* Subdirectory should exist in pending data */);
|
|
1817
|
+
} else {
|
|
1818
|
+
// Pending delete
|
|
1819
|
+
return undefined;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// If the subdirectory is disposed, treat it as non-existent for optimistic reads (unless specified otherwise)
|
|
1823
|
+
if (subdir?.disposed && !getIfDisposed) {
|
|
1824
|
+
return undefined;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
return subdir;
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
public get sequencedSubdirectories(): ReadonlyMap<string, SubDirectory> {
|
|
1831
|
+
this.throwIfDisposed();
|
|
1832
|
+
return this._sequencedSubdirectories;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1824
1835
|
/**
|
|
1825
1836
|
* Process a clear operation.
|
|
1826
1837
|
* @param msg - The message from the server to apply.
|
|
@@ -1836,7 +1847,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1836
1847
|
localOpMetadata: ClearLocalOpMetadata | undefined,
|
|
1837
1848
|
): void {
|
|
1838
1849
|
this.throwIfDisposed();
|
|
1839
|
-
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1850
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
|
|
1840
1851
|
return;
|
|
1841
1852
|
}
|
|
1842
1853
|
|
|
@@ -1896,7 +1907,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1896
1907
|
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
1897
1908
|
): void {
|
|
1898
1909
|
this.throwIfDisposed();
|
|
1899
|
-
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1910
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
|
|
1900
1911
|
return;
|
|
1901
1912
|
}
|
|
1902
1913
|
if (local) {
|
|
@@ -1949,7 +1960,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1949
1960
|
localOpMetadata: EditLocalOpMetadata | undefined,
|
|
1950
1961
|
): void {
|
|
1951
1962
|
this.throwIfDisposed();
|
|
1952
|
-
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
|
|
1963
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
|
|
1953
1964
|
return;
|
|
1954
1965
|
}
|
|
1955
1966
|
|
|
@@ -1972,7 +1983,6 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
1972
1983
|
if (pendingEntry.keySets.length === 0) {
|
|
1973
1984
|
this.pendingStorageData.splice(pendingEntryIndex, 1);
|
|
1974
1985
|
}
|
|
1975
|
-
|
|
1976
1986
|
this.sequencedStorageData.set(key, pendingKeySet.value);
|
|
1977
1987
|
} else {
|
|
1978
1988
|
// Get the previous value before setting the new value
|
|
@@ -2006,21 +2016,78 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2006
2016
|
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
2007
2017
|
): void {
|
|
2008
2018
|
this.throwIfDisposed();
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
|
|
2012
|
-
this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)
|
|
2013
|
-
)
|
|
2014
|
-
) {
|
|
2019
|
+
|
|
2020
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
|
|
2015
2021
|
return;
|
|
2016
2022
|
}
|
|
2017
2023
|
assertNonNullClientId(msg.clientId);
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
+
|
|
2025
|
+
let subDir: SubDirectory | undefined;
|
|
2026
|
+
if (local) {
|
|
2027
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex(
|
|
2028
|
+
(entry) => entry.subdirName === op.subdirName,
|
|
2029
|
+
);
|
|
2030
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
2031
|
+
assert(
|
|
2032
|
+
pendingEntry !== undefined && pendingEntry.type === "createSubDirectory",
|
|
2033
|
+
0xc30 /* Got a local subdir create message we weren't expecting */,
|
|
2034
|
+
);
|
|
2035
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
2036
|
+
subDir = pendingEntry.subdir;
|
|
2037
|
+
|
|
2038
|
+
const existingSubdir = this._sequencedSubdirectories.get(op.subdirName);
|
|
2039
|
+
if (existingSubdir !== undefined) {
|
|
2040
|
+
// If the subdirectory already exists, we don't need to create it again.
|
|
2041
|
+
// This can happen if remote clients also create the same subdir and we processed
|
|
2042
|
+
// that message first.
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (subDir.disposed) {
|
|
2047
|
+
this.undisposeSubdirectoryTree(subDir);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
this._sequencedSubdirectories.set(op.subdirName, subDir);
|
|
2051
|
+
} else {
|
|
2052
|
+
subDir = this.getOptimisticSubDirectory(op.subdirName, true);
|
|
2053
|
+
if (subDir === undefined) {
|
|
2054
|
+
const absolutePath = posix.join(this.absolutePath, op.subdirName);
|
|
2055
|
+
subDir = new SubDirectory(
|
|
2056
|
+
{ seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber },
|
|
2057
|
+
new Set([msg.clientId]),
|
|
2058
|
+
this.directory,
|
|
2059
|
+
this.runtime,
|
|
2060
|
+
this.serializer,
|
|
2061
|
+
absolutePath,
|
|
2062
|
+
this.mc.logger,
|
|
2063
|
+
);
|
|
2064
|
+
} else {
|
|
2065
|
+
// If the subdirectory already optimistically exists, we don't need to create it again.
|
|
2066
|
+
// This can happen if remote clients also created the same subdir.
|
|
2067
|
+
if (subDir.disposed) {
|
|
2068
|
+
this.undisposeSubdirectoryTree(subDir);
|
|
2069
|
+
}
|
|
2070
|
+
subDir.clientIds.add(msg.clientId);
|
|
2071
|
+
}
|
|
2072
|
+
this.registerEventsOnSubDirectory(subDir, op.subdirName);
|
|
2073
|
+
this._sequencedSubdirectories.set(op.subdirName, subDir);
|
|
2074
|
+
|
|
2075
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
2076
|
+
if (!this.pendingSubDirectoryData.some((entry) => entry.subdirName === op.subdirName)) {
|
|
2077
|
+
this.emit("subDirectoryCreated", op.subdirName, local, this);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// Ensure correct seqData. This can be necessary if in scenarios where a subdir was created, deleted, and
|
|
2082
|
+
// then later recreated.
|
|
2083
|
+
if (
|
|
2084
|
+
this.seqData.seq !== -1 &&
|
|
2085
|
+
this.seqData.seq <= msg.sequenceNumber &&
|
|
2086
|
+
subDir.seqData.seq === -1
|
|
2087
|
+
) {
|
|
2088
|
+
subDir.seqData.seq = msg.sequenceNumber;
|
|
2089
|
+
subDir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
2090
|
+
}
|
|
2024
2091
|
}
|
|
2025
2092
|
|
|
2026
2093
|
/**
|
|
@@ -2038,15 +2105,57 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2038
2105
|
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
2039
2106
|
): void {
|
|
2040
2107
|
this.throwIfDisposed();
|
|
2041
|
-
if (
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
) {
|
|
2108
|
+
if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
const previousValue = this._sequencedSubdirectories.get(op.subdirName);
|
|
2113
|
+
if (previousValue === undefined) {
|
|
2114
|
+
// We are trying to delete a subdirectory that does not exist.
|
|
2115
|
+
// If this is a local delete, we should remove the pending delete entry.
|
|
2116
|
+
// This could happen if we already processed a remote delete op for
|
|
2117
|
+
// the same subdirectory.
|
|
2118
|
+
if (local) {
|
|
2119
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex(
|
|
2120
|
+
(entry) => entry.subdirName === op.subdirName,
|
|
2121
|
+
);
|
|
2122
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
2123
|
+
assert(
|
|
2124
|
+
pendingEntry !== undefined &&
|
|
2125
|
+
pendingEntry.type === "deleteSubDirectory" &&
|
|
2126
|
+
pendingEntry.subdirName === op.subdirName,
|
|
2127
|
+
0xc31 /* Got a local deleteSubDirectory message we weren't expecting */,
|
|
2128
|
+
);
|
|
2129
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
2130
|
+
}
|
|
2047
2131
|
return;
|
|
2048
2132
|
}
|
|
2049
|
-
|
|
2133
|
+
|
|
2134
|
+
this._sequencedSubdirectories.delete(op.subdirName);
|
|
2135
|
+
this.disposeSubDirectoryTree(previousValue);
|
|
2136
|
+
|
|
2137
|
+
if (local) {
|
|
2138
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex(
|
|
2139
|
+
(entry) => entry.subdirName === op.subdirName,
|
|
2140
|
+
);
|
|
2141
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
2142
|
+
assert(
|
|
2143
|
+
pendingEntry !== undefined &&
|
|
2144
|
+
pendingEntry.type === "deleteSubDirectory" &&
|
|
2145
|
+
pendingEntry.subdirName === op.subdirName,
|
|
2146
|
+
0xc32 /* Got a local deleteSubDirectory message we weren't expecting */,
|
|
2147
|
+
);
|
|
2148
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
2149
|
+
} else {
|
|
2150
|
+
// Suppress the event if local changes would cause the incoming change to be invisible optimistically.
|
|
2151
|
+
const pendingEntryIndex = this.pendingSubDirectoryData.findIndex(
|
|
2152
|
+
(entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory",
|
|
2153
|
+
);
|
|
2154
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
2155
|
+
if (pendingEntry === undefined) {
|
|
2156
|
+
this.emit("subDirectoryDeleted", op.subdirName, local, this);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2050
2159
|
}
|
|
2051
2160
|
|
|
2052
2161
|
/**
|
|
@@ -2105,50 +2214,28 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2105
2214
|
): void {
|
|
2106
2215
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
2107
2216
|
// is already deleted, in which case we don't need to submit the op.
|
|
2108
|
-
const pendingEntryIndex = this.pendingStorageData.findIndex(
|
|
2109
|
-
|
|
2110
|
-
|
|
2217
|
+
const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => {
|
|
2218
|
+
return op.type === "set"
|
|
2219
|
+
? entry.type === "lifetime" &&
|
|
2220
|
+
entry.key === op.key &&
|
|
2221
|
+
// We also check that the keySets include the localOpMetadata. It's possible we have new
|
|
2222
|
+
// pending key sets that are not the op we are looking for.
|
|
2223
|
+
entry.keySets.includes(localOpMetadata as PendingKeySet)
|
|
2224
|
+
: entry.type === "delete" && entry.key === op.key;
|
|
2225
|
+
});
|
|
2111
2226
|
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
2112
2227
|
if (pendingEntry !== undefined) {
|
|
2113
2228
|
this.submitKeyMessage(op, localOpMetadata as PendingKeySet | PendingKeyDelete);
|
|
2114
2229
|
}
|
|
2115
2230
|
}
|
|
2116
|
-
|
|
2117
|
-
private incrementPendingSubDirCount(map: Map<string, number>, subDirName: string): void {
|
|
2118
|
-
const count = map.get(subDirName) ?? 0;
|
|
2119
|
-
map.set(subDirName, count + 1);
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
private decrementPendingSubDirCount(map: Map<string, number>, subDirName: string): void {
|
|
2123
|
-
const count = map.get(subDirName) ?? 0;
|
|
2124
|
-
map.set(subDirName, count - 1);
|
|
2125
|
-
if (count <= 1) {
|
|
2126
|
-
map.delete(subDirName);
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
/**
|
|
2131
|
-
* Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
|
|
2132
|
-
* or while resubmitting the op.
|
|
2133
|
-
*/
|
|
2134
|
-
private updatePendingSubDirMessageCount(op: IDirectorySubDirectoryOperation): void {
|
|
2135
|
-
if (op.type === "deleteSubDirectory") {
|
|
2136
|
-
this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
2137
|
-
} else if (op.type === "createSubDirectory") {
|
|
2138
|
-
this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
2231
|
/**
|
|
2143
2232
|
* Submit a create subdirectory operation.
|
|
2144
2233
|
* @param op - The operation
|
|
2145
2234
|
*/
|
|
2146
2235
|
private submitCreateSubDirectoryMessage(op: IDirectorySubDirectoryOperation): void {
|
|
2147
|
-
this.throwIfDisposed();
|
|
2148
|
-
this.updatePendingSubDirMessageCount(op);
|
|
2149
|
-
|
|
2150
2236
|
const localOpMetadata: ICreateSubDirLocalOpMetadata = {
|
|
2151
2237
|
type: "createSubDir",
|
|
2238
|
+
parentSubdir: this,
|
|
2152
2239
|
};
|
|
2153
2240
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
2154
2241
|
}
|
|
@@ -2162,12 +2249,10 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2162
2249
|
op: IDirectorySubDirectoryOperation,
|
|
2163
2250
|
subDir: SubDirectory,
|
|
2164
2251
|
): void {
|
|
2165
|
-
this.throwIfDisposed();
|
|
2166
|
-
this.updatePendingSubDirMessageCount(op);
|
|
2167
|
-
|
|
2168
2252
|
const localOpMetadata: IDeleteSubDirLocalOpMetadata = {
|
|
2169
2253
|
type: "deleteSubDir",
|
|
2170
2254
|
subDirectory: subDir,
|
|
2255
|
+
parentSubdir: this,
|
|
2171
2256
|
};
|
|
2172
2257
|
this.directory.submitDirectoryMessage(op, localOpMetadata);
|
|
2173
2258
|
}
|
|
@@ -2183,28 +2268,36 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2183
2268
|
): void {
|
|
2184
2269
|
// Only submit the op, if we have record for it, otherwise it is possible that the older instance
|
|
2185
2270
|
// is already deleted, in which case we don't need to submit the op.
|
|
2186
|
-
if (
|
|
2187
|
-
localOpMetadata.type === "createSubDir" &&
|
|
2188
|
-
!this.pendingCreateSubDirectoriesTracker.has(op.subdirName)
|
|
2189
|
-
) {
|
|
2190
|
-
return;
|
|
2191
|
-
} else if (
|
|
2192
|
-
localOpMetadata.type === "deleteSubDir" &&
|
|
2193
|
-
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)
|
|
2194
|
-
) {
|
|
2195
|
-
return;
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
2271
|
if (localOpMetadata.type === "createSubDir") {
|
|
2199
|
-
|
|
2200
|
-
|
|
2272
|
+
// For create operations, look specifically for createSubDirectory entries
|
|
2273
|
+
const pendingEntry = findLast(
|
|
2274
|
+
this.pendingSubDirectoryData,
|
|
2275
|
+
(entry) => entry.subdirName === op.subdirName && entry.type === "createSubDirectory",
|
|
2276
|
+
);
|
|
2277
|
+
if (pendingEntry !== undefined) {
|
|
2278
|
+
assert(
|
|
2279
|
+
pendingEntry.type === "createSubDirectory",
|
|
2280
|
+
0xc33 /* pending entry should be createSubDirectory */,
|
|
2281
|
+
);
|
|
2282
|
+
// We should add the client id, since when reconnecting it can have a different client id.
|
|
2283
|
+
pendingEntry.subdir.clientIds.add(this.runtime.clientId ?? "detached");
|
|
2284
|
+
// We also need to undelete the subdirectory tree if it was previously deleted
|
|
2285
|
+
this.undisposeSubdirectoryTree(pendingEntry.subdir);
|
|
2286
|
+
this.submitCreateSubDirectoryMessage(op);
|
|
2287
|
+
}
|
|
2201
2288
|
} else if (localOpMetadata.type === "deleteSubDir") {
|
|
2202
|
-
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
|
|
2203
2289
|
assert(
|
|
2204
2290
|
localOpMetadata.subDirectory !== undefined,
|
|
2205
|
-
|
|
2291
|
+
0xc34 /* Subdirectory should exist */,
|
|
2292
|
+
);
|
|
2293
|
+
// For delete operations, look specifically for deleteSubDirectory entries
|
|
2294
|
+
const pendingEntry = findLast(
|
|
2295
|
+
this.pendingSubDirectoryData,
|
|
2296
|
+
(entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory",
|
|
2206
2297
|
);
|
|
2207
|
-
|
|
2298
|
+
if (pendingEntry !== undefined) {
|
|
2299
|
+
this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
|
|
2300
|
+
}
|
|
2208
2301
|
}
|
|
2209
2302
|
}
|
|
2210
2303
|
|
|
@@ -2251,7 +2344,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2251
2344
|
public populateSubDirectory(subdirName: string, newSubDir: SubDirectory): void {
|
|
2252
2345
|
this.throwIfDisposed();
|
|
2253
2346
|
this.registerEventsOnSubDirectory(newSubDir, subdirName);
|
|
2254
|
-
this.
|
|
2347
|
+
this._sequencedSubdirectories.set(subdirName, newSubDir);
|
|
2255
2348
|
}
|
|
2256
2349
|
|
|
2257
2350
|
/**
|
|
@@ -2266,11 +2359,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2266
2359
|
if (directoryOp.type === "clear") {
|
|
2267
2360
|
// A pending clear will be last in the list, since it terminates all prior lifetimes.
|
|
2268
2361
|
const pendingClear = this.pendingStorageData.pop();
|
|
2362
|
+
if (pendingClear === undefined) {
|
|
2363
|
+
// If we can't find a pending entry then it's possible that we deleted an ack'd subdir
|
|
2364
|
+
// from a remote delete subdir op. If that's the case then there is nothing to rollback
|
|
2365
|
+
// since the pending data was removed with the subdirectory deletion.
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2269
2368
|
assert(
|
|
2270
|
-
pendingClear
|
|
2271
|
-
|
|
2272
|
-
localOpMetadata.type === "clear",
|
|
2273
|
-
0xc09 /* Unexpected clear rollback */,
|
|
2369
|
+
pendingClear.type === "clear" && localOpMetadata.type === "clear",
|
|
2370
|
+
0xc35 /* Unexpected clear rollback */,
|
|
2274
2371
|
);
|
|
2275
2372
|
for (const [key] of this.internalIterator()) {
|
|
2276
2373
|
const event: IDirectoryValueChanged = {
|
|
@@ -2293,10 +2390,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2293
2390
|
(entry) => entry.type !== "clear" && entry.key === directoryOp.key,
|
|
2294
2391
|
);
|
|
2295
2392
|
const pendingEntry = this.pendingStorageData[pendingEntryIndex];
|
|
2393
|
+
if (pendingEntry === undefined) {
|
|
2394
|
+
// If we can't find a pending entry then it's possible that we deleted an ack'd subdir
|
|
2395
|
+
// from a remote delete subdir op. If that's the case then there is nothing to rollback
|
|
2396
|
+
// since the pending data was removed with the subdirectory deletion.
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2296
2399
|
assert(
|
|
2297
|
-
pendingEntry
|
|
2298
|
-
|
|
2299
|
-
0xc0a /* Unexpected pending data for set/delete op */,
|
|
2400
|
+
pendingEntry.type === "delete" || pendingEntry.type === "lifetime",
|
|
2401
|
+
0xc36 /* Unexpected pending data for set/delete op */,
|
|
2300
2402
|
);
|
|
2301
2403
|
if (pendingEntry.type === "delete") {
|
|
2302
2404
|
assert(pendingEntry === localOpMetadata, 0xc0b /* Unexpected delete rollback */);
|
|
@@ -2340,50 +2442,52 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2340
2442
|
directoryOp.type === "createSubDirectory" &&
|
|
2341
2443
|
localOpMetadata.type === "createSubDir"
|
|
2342
2444
|
) {
|
|
2343
|
-
const subdirName
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2445
|
+
const subdirName = directoryOp.subdirName;
|
|
2446
|
+
|
|
2447
|
+
const pendingEntryIndex = findLastIndex(
|
|
2448
|
+
this.pendingSubDirectoryData,
|
|
2449
|
+
(entry) => entry.type === "createSubDirectory" && entry.subdirName === subdirName,
|
|
2347
2450
|
);
|
|
2451
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
2348
2452
|
assert(
|
|
2349
|
-
|
|
2350
|
-
|
|
2453
|
+
pendingEntry !== undefined && pendingEntry.type === "createSubDirectory",
|
|
2454
|
+
0xc37 /* Unexpected pending data for createSubDirectory op */,
|
|
2351
2455
|
);
|
|
2352
2456
|
|
|
2353
|
-
|
|
2354
|
-
|
|
2457
|
+
// We still need to emit the disposed event for any locally created (and now
|
|
2458
|
+
// rolled back) subdirectory trees so listeners can observer the lifecycle
|
|
2459
|
+
// changes properly. We don't want to fully delete in case there is another
|
|
2460
|
+
// operation that references the same subdirectory.
|
|
2461
|
+
this.emitDisposeForSubdirTree(pendingEntry.subdir);
|
|
2462
|
+
|
|
2463
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
2464
|
+
this.emit("subDirectoryDeleted", subdirName, true, this);
|
|
2355
2465
|
} else if (
|
|
2356
2466
|
directoryOp.type === "deleteSubDirectory" &&
|
|
2357
2467
|
localOpMetadata.type === "deleteSubDir"
|
|
2358
2468
|
) {
|
|
2359
|
-
const subdirName
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2469
|
+
const subdirName = directoryOp.subdirName;
|
|
2470
|
+
|
|
2471
|
+
const pendingEntryIndex = findLastIndex(
|
|
2472
|
+
this.pendingSubDirectoryData,
|
|
2473
|
+
(entry) => entry.type === "deleteSubDirectory" && entry.subdirName === subdirName,
|
|
2363
2474
|
);
|
|
2475
|
+
const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
|
|
2364
2476
|
assert(
|
|
2365
|
-
|
|
2366
|
-
|
|
2477
|
+
pendingEntry !== undefined && pendingEntry.type === "deleteSubDirectory",
|
|
2478
|
+
0xc38 /* Unexpected pending data for deleteSubDirectory op */,
|
|
2367
2479
|
);
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
this.localCreationSeqTracker.set(subdirName, {
|
|
2380
|
-
...localOpMetadata.subDirectory.seqData,
|
|
2381
|
-
});
|
|
2382
|
-
}
|
|
2383
|
-
this.emit("subDirectoryCreated", subdirName, true, this);
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, subdirName);
|
|
2480
|
+
this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
|
|
2481
|
+
|
|
2482
|
+
// Restore the subdirectory
|
|
2483
|
+
const subDirectoryToRestore = localOpMetadata.subDirectory;
|
|
2484
|
+
assert(subDirectoryToRestore !== undefined, 0xc39 /* Subdirectory should exist */);
|
|
2485
|
+
// Recursively undispose all nested subdirectories before adding to the map
|
|
2486
|
+
// This ensures the subdirectory is properly restored before being exposed
|
|
2487
|
+
this.undisposeSubdirectoryTree(subDirectoryToRestore);
|
|
2488
|
+
// Re-register events
|
|
2489
|
+
this.registerEventsOnSubDirectory(subDirectoryToRestore, subdirName);
|
|
2490
|
+
this.emit("subDirectoryCreated", subdirName, true, this);
|
|
2387
2491
|
} else {
|
|
2388
2492
|
throw new Error("Unsupported op for rollback");
|
|
2389
2493
|
}
|
|
@@ -2402,179 +2506,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2402
2506
|
* This return true if the message is for the current instance of this sub directory. As the sub directory
|
|
2403
2507
|
* can be deleted and created again, then this finds if the message is for current instance of directory or not.
|
|
2404
2508
|
* @param msg - message for the directory
|
|
2509
|
+
* @param targetSubdir - subdirectory instance we are targeting from local op metadata (if a local op)
|
|
2405
2510
|
*/
|
|
2406
|
-
private isMessageForCurrentInstanceOfSubDirectory(
|
|
2407
|
-
// If the message is either from the creator of directory or this directory was created when
|
|
2408
|
-
// container was detached or in case this directory is already live(known to other clients)
|
|
2409
|
-
// and the op was created after the directory was created then apply this op.
|
|
2410
|
-
return (
|
|
2411
|
-
(msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
|
|
2412
|
-
this.clientIds.has("detached") ||
|
|
2413
|
-
(this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)
|
|
2414
|
-
);
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
/**
|
|
2418
|
-
* If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
|
|
2419
|
-
* not process the incoming operation.
|
|
2420
|
-
* @param op - Operation to check
|
|
2421
|
-
* @param local - Whether the message originated from the local client
|
|
2422
|
-
* @param message - The message
|
|
2423
|
-
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
2424
|
-
* For messages from a remote client, this will be undefined.
|
|
2425
|
-
* @returns True if the operation should be processed, false otherwise
|
|
2426
|
-
*/
|
|
2427
|
-
private needProcessSubDirectoryOperation(
|
|
2511
|
+
private isMessageForCurrentInstanceOfSubDirectory(
|
|
2428
2512
|
msg: ISequencedDocumentMessage,
|
|
2429
|
-
|
|
2430
|
-
local: boolean,
|
|
2431
|
-
localOpMetadata: SubDirLocalOpMetadata | undefined,
|
|
2513
|
+
targetSubdir?: SubDirectory | undefined,
|
|
2432
2514
|
): boolean {
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
(
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
assert(
|
|
2444
|
-
pendingDeleteCount !== undefined && pendingDeleteCount > 0,
|
|
2445
|
-
0x6c2 /* pendingDeleteCount should exist */,
|
|
2446
|
-
);
|
|
2447
|
-
this.decrementPendingSubDirCount(
|
|
2448
|
-
this.pendingDeleteSubDirectoriesTracker,
|
|
2449
|
-
op.subdirName,
|
|
2450
|
-
);
|
|
2451
|
-
} else if (localOpMetadata.type === "createSubDir") {
|
|
2452
|
-
assert(
|
|
2453
|
-
pendingCreateCount !== undefined && pendingCreateCount > 0,
|
|
2454
|
-
0x6c3 /* pendingCreateCount should exist */,
|
|
2455
|
-
);
|
|
2456
|
-
this.decrementPendingSubDirCount(
|
|
2457
|
-
this.pendingCreateSubDirectoriesTracker,
|
|
2458
|
-
op.subdirName,
|
|
2459
|
-
);
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
if (op.type === "deleteSubDirectory") {
|
|
2463
|
-
const resetSubDirectoryTree = (directory: SubDirectory | undefined): void => {
|
|
2464
|
-
if (!directory) {
|
|
2465
|
-
return;
|
|
2466
|
-
}
|
|
2467
|
-
// If this is delete op and we have keys in this subDirectory, then we need to delete these
|
|
2468
|
-
// keys except the pending ones as they will be sequenced after this delete.
|
|
2469
|
-
directory.sequencedStorageData.clear();
|
|
2470
|
-
directory.emit("clear", true, directory);
|
|
2471
|
-
|
|
2472
|
-
// In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
|
|
2473
|
-
// creators as the previous directory is getting deleted and we will initialize again when
|
|
2474
|
-
// we will receive op for the create again.
|
|
2475
|
-
directory.seqData.seq = -1;
|
|
2476
|
-
directory.seqData.clientSeq = -1;
|
|
2477
|
-
directory.clientIds.clear();
|
|
2478
|
-
// Do the same thing for the subtree of the directory. If create is not pending for a child, then just
|
|
2479
|
-
// delete it.
|
|
2480
|
-
const subDirectories = directory.subdirectories();
|
|
2481
|
-
for (const [subDirName, subDir] of subDirectories) {
|
|
2482
|
-
if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
|
|
2483
|
-
resetSubDirectoryTree(subDir as SubDirectory);
|
|
2484
|
-
continue;
|
|
2485
|
-
}
|
|
2486
|
-
directory.deleteSubDirectoryCore(subDirName, false);
|
|
2487
|
-
}
|
|
2488
|
-
};
|
|
2489
|
-
const subDirectory = this._subdirectories.get(op.subdirName);
|
|
2490
|
-
// Clear the creation tracker record
|
|
2491
|
-
this.ackedCreationSeqTracker.delete(op.subdirName);
|
|
2492
|
-
resetSubDirectoryTree(subDirectory);
|
|
2493
|
-
}
|
|
2494
|
-
if (op.type === "createSubDirectory") {
|
|
2495
|
-
const dir = this._subdirectories.get(op.subdirName);
|
|
2496
|
-
// Child sub directory create seq number can't be lower than the parent subdirectory.
|
|
2497
|
-
// The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
|
|
2498
|
-
if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
|
|
2499
|
-
if (dir?.seqData.seq === -1) {
|
|
2500
|
-
// Only set the sequence data based on the first message
|
|
2501
|
-
dir.seqData.seq = msg.sequenceNumber;
|
|
2502
|
-
dir.seqData.clientSeq = msg.clientSequenceNumber;
|
|
2503
|
-
|
|
2504
|
-
// set the creation seq in tracker
|
|
2505
|
-
if (
|
|
2506
|
-
!this.ackedCreationSeqTracker.has(op.subdirName) &&
|
|
2507
|
-
!this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)
|
|
2508
|
-
) {
|
|
2509
|
-
this.ackedCreationSeqTracker.set(op.subdirName, {
|
|
2510
|
-
seq: msg.sequenceNumber,
|
|
2511
|
-
clientSeq: msg.clientSequenceNumber,
|
|
2512
|
-
});
|
|
2513
|
-
if (local) {
|
|
2514
|
-
this.localCreationSeqTracker.delete(op.subdirName);
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
// The client created the dir at or after the dirs seq, so list its client id as a creator.
|
|
2519
|
-
if (
|
|
2520
|
-
dir !== undefined &&
|
|
2521
|
-
!dir.clientIds.has(msg.clientId) &&
|
|
2522
|
-
dir.seqData.seq <= msg.sequenceNumber
|
|
2523
|
-
) {
|
|
2524
|
-
dir.clientIds.add(msg.clientId);
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return false;
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
return !local;
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
/**
|
|
2535
|
-
* Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
|
|
2536
|
-
* @param subdirName - The name of the subdirectory being created
|
|
2537
|
-
* @param local - Whether the message originated from the local client
|
|
2538
|
-
* @param seqData - Sequence number and client sequence number at which this directory is created
|
|
2539
|
-
* @param clientId - Id of client which created this directory.
|
|
2540
|
-
* @returns True if is newly created, false if it already existed.
|
|
2541
|
-
*/
|
|
2542
|
-
private createSubDirectoryCore(
|
|
2543
|
-
subdirName: string,
|
|
2544
|
-
local: boolean,
|
|
2545
|
-
seqData: SequenceData,
|
|
2546
|
-
clientId: string,
|
|
2547
|
-
): boolean {
|
|
2548
|
-
const subdir = this._subdirectories.get(subdirName);
|
|
2549
|
-
if (subdir === undefined) {
|
|
2550
|
-
const absolutePath = posix.join(this.absolutePath, subdirName);
|
|
2551
|
-
const subDir = new SubDirectory(
|
|
2552
|
-
{ ...seqData },
|
|
2553
|
-
new Set([clientId]),
|
|
2554
|
-
this.directory,
|
|
2555
|
-
this.runtime,
|
|
2556
|
-
this.serializer,
|
|
2557
|
-
absolutePath,
|
|
2558
|
-
this.logger,
|
|
2559
|
-
);
|
|
2560
|
-
/**
|
|
2561
|
-
* Store the sequence numbers of newly created subdirectory to the proper creation tracker, based
|
|
2562
|
-
* on whether the creation behavior has been ack'd or not
|
|
2563
|
-
*/
|
|
2564
|
-
if (isAcknowledgedOrDetached(seqData)) {
|
|
2565
|
-
this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
|
|
2566
|
-
} else {
|
|
2567
|
-
this.localCreationSeqTracker.set(subdirName, { ...seqData });
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
this.registerEventsOnSubDirectory(subDir, subdirName);
|
|
2571
|
-
this._subdirectories.set(subdirName, subDir);
|
|
2572
|
-
this.emit("subDirectoryCreated", subdirName, local, this);
|
|
2573
|
-
return true;
|
|
2574
|
-
} else {
|
|
2575
|
-
subdir.clientIds.add(clientId);
|
|
2576
|
-
}
|
|
2577
|
-
return false;
|
|
2515
|
+
// The message must be from this instance of the directory (if a local op) AND one of the following must be true:
|
|
2516
|
+
// 1. The message was from the creator of this directory
|
|
2517
|
+
// 2. This directory was created while detached
|
|
2518
|
+
// 3. This directory was already live (known to other clients) and the op was created after the directory was created.
|
|
2519
|
+
return (
|
|
2520
|
+
(targetSubdir === undefined || targetSubdir === this) &&
|
|
2521
|
+
((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
|
|
2522
|
+
this.clientIds.has("detached") ||
|
|
2523
|
+
(this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber))
|
|
2524
|
+
);
|
|
2578
2525
|
}
|
|
2579
2526
|
|
|
2580
2527
|
private registerEventsOnSubDirectory(subDirectory: SubDirectory, subDirName: string): void {
|
|
@@ -2586,56 +2533,100 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
|
|
|
2586
2533
|
});
|
|
2587
2534
|
}
|
|
2588
2535
|
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
* @param local - Whether the message originated from the local client
|
|
2593
|
-
*/
|
|
2594
|
-
private deleteSubDirectoryCore(
|
|
2595
|
-
subdirName: string,
|
|
2596
|
-
local: boolean,
|
|
2597
|
-
): SubDirectory | undefined {
|
|
2598
|
-
const previousValue = this._subdirectories.get(subdirName);
|
|
2599
|
-
// This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
|
|
2600
|
-
// Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
|
|
2601
|
-
if (previousValue !== undefined) {
|
|
2602
|
-
this._subdirectories.delete(subdirName);
|
|
2603
|
-
/**
|
|
2604
|
-
* Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
|
|
2605
|
-
* ack'd already or still not committed yet (could be both).
|
|
2606
|
-
*/
|
|
2607
|
-
if (this.ackedCreationSeqTracker.has(subdirName)) {
|
|
2608
|
-
this.ackedCreationSeqTracker.delete(subdirName);
|
|
2609
|
-
}
|
|
2610
|
-
if (this.localCreationSeqTracker.has(subdirName)) {
|
|
2611
|
-
this.localCreationSeqTracker.delete(subdirName);
|
|
2612
|
-
}
|
|
2613
|
-
this.disposeSubDirectoryTree(previousValue);
|
|
2614
|
-
this.emit("subDirectoryDeleted", subdirName, local, this);
|
|
2536
|
+
private disposeSubDirectoryTree(directory: SubDirectory | undefined): void {
|
|
2537
|
+
if (directory === undefined) {
|
|
2538
|
+
return;
|
|
2615
2539
|
}
|
|
2616
|
-
|
|
2540
|
+
// Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
|
|
2541
|
+
const subDirectories = directory.subdirectories();
|
|
2542
|
+
for (const [_, subDirectory] of subDirectories) {
|
|
2543
|
+
this.disposeSubDirectoryTree(subDirectory as SubDirectory);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// We need to reset the sequenced data as the previous directory is getting deleted and we will
|
|
2547
|
+
// initialize again when we will receive op for the create again.
|
|
2548
|
+
directory.clearSubDirectorySequencedData();
|
|
2549
|
+
|
|
2550
|
+
directory.dispose();
|
|
2617
2551
|
}
|
|
2618
2552
|
|
|
2619
|
-
private
|
|
2620
|
-
if (
|
|
2553
|
+
private emitDisposeForSubdirTree(directory: SubDirectory): void {
|
|
2554
|
+
if (directory === undefined || directory.disposed) {
|
|
2621
2555
|
return;
|
|
2622
2556
|
}
|
|
2623
2557
|
// Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
|
|
2624
2558
|
const subDirectories = directory.subdirectories();
|
|
2625
2559
|
for (const [_, subDirectory] of subDirectories) {
|
|
2626
|
-
this.
|
|
2560
|
+
this.emitDisposeForSubdirTree(subDirectory as SubDirectory);
|
|
2627
2561
|
}
|
|
2562
|
+
|
|
2628
2563
|
if (typeof directory.dispose === "function") {
|
|
2629
|
-
directory.
|
|
2564
|
+
directory.emit("disposed", directory);
|
|
2630
2565
|
}
|
|
2631
2566
|
}
|
|
2632
2567
|
|
|
2633
|
-
private
|
|
2634
|
-
//
|
|
2635
|
-
|
|
2568
|
+
private undisposeSubdirectoryTree(directory: SubDirectory): void {
|
|
2569
|
+
// This will unmark "deleted" from the subdirectories from bottom to top.
|
|
2570
|
+
for (const [_, subDirectory] of directory.getSubdirectoriesEvenIfDisposed()) {
|
|
2571
|
+
this.undisposeSubdirectoryTree(subDirectory as SubDirectory);
|
|
2572
|
+
}
|
|
2636
2573
|
directory.undispose();
|
|
2637
|
-
|
|
2638
|
-
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
/**
|
|
2577
|
+
* Similar to {@link subdirectories}, but also includes subdirectories that are disposed.
|
|
2578
|
+
*/
|
|
2579
|
+
private getSubdirectoriesEvenIfDisposed(): IterableIterator<[string, IDirectory]> {
|
|
2580
|
+
const sequencedSubdirs: [string, SubDirectory][] = [];
|
|
2581
|
+
const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
|
|
2582
|
+
for (const subdirName of sequencedSubdirNames) {
|
|
2583
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
|
|
2584
|
+
if (optimisticSubdir !== undefined) {
|
|
2585
|
+
sequencedSubdirs.push([subdirName, optimisticSubdir]);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
const pendingSubdirNames = [
|
|
2590
|
+
...new Set(
|
|
2591
|
+
this.pendingSubDirectoryData
|
|
2592
|
+
.map((entry) => entry.subdirName)
|
|
2593
|
+
.filter((subdirName) => !sequencedSubdirNames.has(subdirName)),
|
|
2594
|
+
),
|
|
2595
|
+
];
|
|
2596
|
+
const pendingSubdirs: [string, SubDirectory][] = [];
|
|
2597
|
+
for (const subdirName of pendingSubdirNames) {
|
|
2598
|
+
const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
|
|
2599
|
+
if (optimisticSubdir !== undefined) {
|
|
2600
|
+
pendingSubdirs.push([subdirName, optimisticSubdir]);
|
|
2601
|
+
}
|
|
2639
2602
|
}
|
|
2603
|
+
|
|
2604
|
+
const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
|
|
2605
|
+
|
|
2606
|
+
const orderedSubdirs = allSubdirs.sort((a, b) => {
|
|
2607
|
+
const aSeqData = a[1].seqData;
|
|
2608
|
+
const bSeqData = b[1].seqData;
|
|
2609
|
+
assert(
|
|
2610
|
+
aSeqData !== undefined && bSeqData !== undefined,
|
|
2611
|
+
0xc3a /* seqData should be defined */,
|
|
2612
|
+
);
|
|
2613
|
+
return seqDataComparator(aSeqData, bSeqData);
|
|
2614
|
+
});
|
|
2615
|
+
|
|
2616
|
+
return orderedSubdirs[Symbol.iterator]();
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Clears the sequenced data of a subdirectory but notably retains the pending
|
|
2621
|
+
* storage data. This is done when disposing of a directory so if we need to
|
|
2622
|
+
* re-create it, then we still have the pending ops.
|
|
2623
|
+
*/
|
|
2624
|
+
public clearSubDirectorySequencedData(): void {
|
|
2625
|
+
this.seqData.seq = -1;
|
|
2626
|
+
this.seqData.clientSeq = -1;
|
|
2627
|
+
this.sequencedStorageData.clear();
|
|
2628
|
+
this._sequencedSubdirectories.clear();
|
|
2629
|
+
this.clientIds.clear();
|
|
2630
|
+
this.clientIds.add(this.runtime.clientId ?? "detached");
|
|
2640
2631
|
}
|
|
2641
2632
|
}
|