@fluidframework/map 2.53.1 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/{map.legacy.alpha.api.md → map.legacy.beta.api.md} +15 -15
  3. package/dist/directory.d.ts +41 -91
  4. package/dist/directory.d.ts.map +1 -1
  5. package/dist/directory.js +447 -464
  6. package/dist/directory.js.map +1 -1
  7. package/dist/directoryFactory.d.ts +3 -6
  8. package/dist/directoryFactory.d.ts.map +1 -1
  9. package/dist/directoryFactory.js +2 -4
  10. package/dist/directoryFactory.js.map +1 -1
  11. package/dist/interfaces.d.ts +4 -8
  12. package/dist/interfaces.d.ts.map +1 -1
  13. package/dist/interfaces.js.map +1 -1
  14. package/dist/internalInterfaces.d.ts +1 -2
  15. package/dist/internalInterfaces.d.ts.map +1 -1
  16. package/dist/internalInterfaces.js.map +1 -1
  17. package/dist/mapFactory.d.ts +3 -6
  18. package/dist/mapFactory.d.ts.map +1 -1
  19. package/dist/mapFactory.js +2 -4
  20. package/dist/mapFactory.js.map +1 -1
  21. package/dist/packageVersion.d.ts +1 -1
  22. package/dist/packageVersion.js +1 -1
  23. package/dist/packageVersion.js.map +1 -1
  24. package/lib/directory.d.ts +41 -91
  25. package/lib/directory.d.ts.map +1 -1
  26. package/lib/directory.js +440 -457
  27. package/lib/directory.js.map +1 -1
  28. package/lib/directoryFactory.d.ts +3 -6
  29. package/lib/directoryFactory.d.ts.map +1 -1
  30. package/lib/directoryFactory.js +2 -4
  31. package/lib/directoryFactory.js.map +1 -1
  32. package/lib/interfaces.d.ts +4 -8
  33. package/lib/interfaces.d.ts.map +1 -1
  34. package/lib/interfaces.js.map +1 -1
  35. package/lib/internalInterfaces.d.ts +1 -2
  36. package/lib/internalInterfaces.d.ts.map +1 -1
  37. package/lib/internalInterfaces.js.map +1 -1
  38. package/lib/mapFactory.d.ts +3 -6
  39. package/lib/mapFactory.d.ts.map +1 -1
  40. package/lib/mapFactory.js +2 -4
  41. package/lib/mapFactory.js.map +1 -1
  42. package/lib/packageVersion.d.ts +1 -1
  43. package/lib/packageVersion.js +1 -1
  44. package/lib/packageVersion.js.map +1 -1
  45. package/package.json +17 -18
  46. package/src/directory.ts +564 -573
  47. package/src/directoryFactory.ts +3 -6
  48. package/src/interfaces.ts +4 -8
  49. package/src/internalInterfaces.ts +1 -2
  50. package/src/mapFactory.ts +3 -6
  51. 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.getWorkingDirectory(op.path) as SubDirectory | undefined;
895
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
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 subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
903
- if (subdir) {
904
- subdir.resubmitClearMessage(op, localOpMetadata);
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.getWorkingDirectory(op.path) as SubDirectory | undefined;
916
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
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 subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
924
- if (subdir) {
925
- subdir.resubmitKeyMessage(op, localOpMetadata);
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.getWorkingDirectory(op.path) as SubDirectory | undefined;
937
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
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 subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
947
- if (subdir) {
948
- subdir.resubmitKeyMessage(op, localOpMetadata);
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.getWorkingDirectory(op.path) as SubDirectory | undefined;
961
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
962
- // as we are going to delete this subDirectory.
963
- if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
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 parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
972
- if (parentSubdir) {
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
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
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.getWorkingDirectory(op.path) as SubDirectory | undefined;
987
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
988
- // as we are going to delete this subDirectory.
989
- if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
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 parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
998
- if (parentSubdir) {
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
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
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 _subdirectories = new Map<string, SubDirectory>();
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
- private readonly logger: ITelemetryLoggerExt,
1128
+ logger: ITelemetryLoggerExt,
1205
1129
  ) {
1206
1130
  super();
1207
- this.localCreationSeqTracker = new DirectoryCreationTracker();
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
- // Create a local value and serialize it.
1263
- bindHandles(value, this.serializer, this.directory.handle);
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 = { type: "lifetime", path: this.absolutePath, key, keySets: [] };
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._subdirectories.size;
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
- // Create the sub directory locally first.
1349
- const isNew = this.createSubDirectoryCore(
1350
- subdirName,
1351
- true,
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
- // If we are not attached, don't submit the op.
1359
- if (!this.directory.isAttached()) {
1360
- return subDir;
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
- // Only submit the op, if it is newly created.
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._subdirectories.get(subdirName);
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._subdirectories.has(subdirName);
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
- return subDir !== undefined;
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
- // Only submit the op, if the directory existed and we deleted it.
1423
- if (subDir !== undefined) {
1424
- const op: IDirectoryDeleteSubDirectoryOperation = {
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
- return subDir !== undefined;
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
- const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1446
-
1447
- if (subdirNames.length !== this._subdirectories.size) {
1448
- // TODO: AB#7022: Hitting this block indicates that the eventual consistency scheme for ordering subdirectories
1449
- // has failed. Fall back to previous directory behavior, which didn't guarantee ordering.
1450
- // It's not currently clear how to reach this state, so log some diagnostics to help understand the issue.
1451
- // This whole block should eventually be replaced by an assert that the two sizes align.
1452
- if (!hasLoggedDirectoryInconsistency) {
1453
- this.logger.sendTelemetryEvent({
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
- const entriesIterator = {
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
- if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
1503
- return true;
1504
- }
1505
- return false;
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
 
@@ -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
- if (
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
- this.createSubDirectoryCore(
2019
- op.subdirName,
2020
- local,
2021
- { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber },
2022
- msg.clientId,
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
- this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
2044
- this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)
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
- this.deleteSubDirectoryCore(op.subdirName, local);
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
- (entry) => entry.type !== "clear" && entry.key === op.key,
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
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
2200
- this.submitCreateSubDirectoryMessage(op);
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
- 0xc08 /* localOpMetadata.subDirectory should be defined */,
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
- this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
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._subdirectories.set(subdirName, newSubDir);
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 !== undefined &&
2271
- pendingClear.type === "clear" &&
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 !== undefined &&
2298
- (pendingEntry.type === "delete" || pendingEntry.type === "lifetime"),
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: unknown = directoryOp.subdirName;
2344
- assert(
2345
- subdirName !== undefined,
2346
- 0x8af /* "subdirName" property is missing from "createSubDirectory" operation. */,
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
- typeof subdirName === "string",
2350
- 0x8b0 /* "subdirName" property in "createSubDirectory" operation is misconfigured. Expected a string. */,
2453
+ pendingEntry !== undefined && pendingEntry.type === "createSubDirectory",
2454
+ 0xc37 /* Unexpected pending data for createSubDirectory op */,
2351
2455
  );
2352
2456
 
2353
- this.deleteSubDirectoryCore(subdirName, true);
2354
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, subdirName);
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: unknown = directoryOp.subdirName;
2360
- assert(
2361
- subdirName !== undefined,
2362
- 0x8b1 /* "subdirName" property is missing from "deleteSubDirectory" operation. */,
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
- typeof subdirName === "string",
2366
- 0x8b2 /* "subdirName" property in "deleteSubDirectory" operation is misconfigured. Expected a string. */,
2477
+ pendingEntry !== undefined && pendingEntry.type === "deleteSubDirectory",
2478
+ 0xc38 /* Unexpected pending data for deleteSubDirectory op */,
2367
2479
  );
2368
-
2369
- if (localOpMetadata.subDirectory !== undefined) {
2370
- this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
2371
- // don't need to register events because deleting never unregistered
2372
- this._subdirectories.set(subdirName, localOpMetadata.subDirectory);
2373
- // Restore the record in creation tracker
2374
- if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
2375
- this.ackedCreationSeqTracker.set(subdirName, {
2376
- ...localOpMetadata.subDirectory.seqData,
2377
- });
2378
- } else {
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(msg: ISequencedDocumentMessage): boolean {
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
- op: IDirectorySubDirectoryOperation,
2430
- local: boolean,
2431
- localOpMetadata: SubDirLocalOpMetadata | undefined,
2513
+ targetSubdir?: SubDirectory | undefined,
2432
2514
  ): boolean {
2433
- assertNonNullClientId(msg.clientId);
2434
- const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
2435
- const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
2436
- if (
2437
- (pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
2438
- (pendingCreateCount !== undefined && pendingCreateCount > 0)
2439
- ) {
2440
- if (local) {
2441
- assert(localOpMetadata !== undefined, 0xc0d /* localOpMetadata should be defined */);
2442
- if (localOpMetadata.type === "deleteSubDir") {
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
- * Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
2591
- * @param subdirName - The name of the subdirectory being deleted
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
- return previousValue;
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 disposeSubDirectoryTree(directory: IDirectory | undefined): void {
2620
- if (!directory) {
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.disposeSubDirectoryTree(subDirectory);
2560
+ this.emitDisposeForSubdirTree(subDirectory as SubDirectory);
2627
2561
  }
2562
+
2628
2563
  if (typeof directory.dispose === "function") {
2629
- directory.dispose();
2564
+ directory.emit("disposed", directory);
2630
2565
  }
2631
2566
  }
2632
2567
 
2633
- private undeleteSubDirectoryTree(directory: SubDirectory): void {
2634
- // Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
2635
- // This will unmark "deleted" from the subdirectories from top to bottom.
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
- for (const [_, subDirectory] of directory.subdirectories()) {
2638
- this.undeleteSubDirectoryTree(subDirectory as SubDirectory);
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
  }