@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/dist/directory.js CHANGED
@@ -13,10 +13,9 @@ const client_utils_1 = require("@fluid-internal/client-utils");
13
13
  const internal_1 = require("@fluidframework/core-utils/internal");
14
14
  const internal_2 = require("@fluidframework/driver-definitions/internal");
15
15
  const internal_3 = require("@fluidframework/driver-utils/internal");
16
- const internal_4 = require("@fluidframework/merge-tree/internal");
17
- const internal_5 = require("@fluidframework/runtime-utils/internal");
18
- const internal_6 = require("@fluidframework/shared-object-base/internal");
19
- const internal_7 = require("@fluidframework/telemetry-utils/internal");
16
+ const internal_4 = require("@fluidframework/runtime-utils/internal");
17
+ const internal_5 = require("@fluidframework/shared-object-base/internal");
18
+ const internal_6 = require("@fluidframework/telemetry-utils/internal");
20
19
  const path_browserify_1 = __importDefault(require("path-browserify"));
21
20
  const localValues_js_1 = require("./localValues.js");
22
21
  const utils_js_1 = require("./utils.js");
@@ -67,60 +66,6 @@ const seqDataComparator = (a, b) => {
67
66
  function isAcknowledgedOrDetached(seqData) {
68
67
  return seqData.seq >= 0;
69
68
  }
70
- /**
71
- * A utility class for tracking associations between keys and their creation indices.
72
- * This is relevant to support map iteration in insertion order, see
73
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
74
- *
75
- * TODO: It can be combined with the creation tracker utilized in SharedMap
76
- */
77
- class DirectoryCreationTracker {
78
- constructor() {
79
- this.indexToKey = new internal_4.RedBlackTree(seqDataComparator);
80
- this.keyToIndex = new Map();
81
- }
82
- set(key, seqData) {
83
- this.indexToKey.put(seqData, key);
84
- this.keyToIndex.set(key, seqData);
85
- }
86
- has(keyOrSeqData) {
87
- return typeof keyOrSeqData === "string"
88
- ? this.keyToIndex.has(keyOrSeqData)
89
- : this.indexToKey.get(keyOrSeqData) !== undefined;
90
- }
91
- delete(keyOrSeqData) {
92
- if (this.has(keyOrSeqData)) {
93
- if (typeof keyOrSeqData === "string") {
94
- const seqData = this.keyToIndex.get(keyOrSeqData);
95
- this.keyToIndex.delete(keyOrSeqData);
96
- this.indexToKey.remove(seqData);
97
- }
98
- else {
99
- const key = this.indexToKey.get(keyOrSeqData)?.data;
100
- this.indexToKey.remove(keyOrSeqData);
101
- this.keyToIndex.delete(key);
102
- }
103
- }
104
- }
105
- /**
106
- * Retrieves all subdirectories with creation order that satisfy an optional constraint function.
107
- * @param constraint - An optional constraint function that filters keys.
108
- * @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
109
- */
110
- keys(constraint) {
111
- const keys = [];
112
- this.indexToKey.mapRange((node) => {
113
- if (!constraint || constraint(node.data)) {
114
- keys.push(node.data);
115
- }
116
- return true;
117
- }, keys);
118
- return keys;
119
- }
120
- get size() {
121
- return this.keyToIndex.size;
122
- }
123
- }
124
69
  /**
125
70
  * {@inheritDoc ISharedDirectory}
126
71
  *
@@ -134,7 +79,7 @@ class DirectoryCreationTracker {
134
79
  *
135
80
  * @sealed
136
81
  */
137
- class SharedDirectory extends internal_6.SharedObject {
82
+ class SharedDirectory extends internal_5.SharedObject {
138
83
  /**
139
84
  * {@inheritDoc IDirectory.absolutePath}
140
85
  */
@@ -321,6 +266,25 @@ class SharedDirectory extends internal_6.SharedObject {
321
266
  }
322
267
  return currentSubDir;
323
268
  }
269
+ /**
270
+ * Similar to `getWorkingDirectory`, but only returns directories that are sequenced.
271
+ * This can be useful for op processing since we only process ops on sequenced directories.
272
+ */
273
+ getSequencedWorkingDirectory(relativePath) {
274
+ const absolutePath = this.makeAbsolute(relativePath);
275
+ if (absolutePath === posix.sep) {
276
+ return this.root;
277
+ }
278
+ let currentSubDir = this.root;
279
+ const subdirs = absolutePath.slice(1).split(posix.sep);
280
+ for (const subdir of subdirs) {
281
+ currentSubDir = currentSubDir.sequencedSubdirectories.get(subdir);
282
+ if (!currentSubDir) {
283
+ return undefined;
284
+ }
285
+ }
286
+ return currentSubDir;
287
+ }
324
288
  /**
325
289
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
326
290
  */
@@ -413,17 +377,13 @@ class SharedDirectory extends internal_6.SharedObject {
413
377
  }
414
378
  newSubDir = new SubDirectory(seqData, createInfo === undefined ? new Set() : new Set(createInfo.ccIds), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName), this.logger);
415
379
  currentSubDir.populateSubDirectory(subdirName, newSubDir);
416
- // Record the newly inserted subdirectory to the creation tracker
417
- currentSubDir.ackedCreationSeqTracker.set(subdirName, {
418
- ...seqData,
419
- });
420
380
  }
421
381
  stack.push([newSubDir, subdirObject]);
422
382
  }
423
383
  }
424
384
  if (currentSubDirObject.storage) {
425
385
  for (const [key, serializable] of Object.entries(currentSubDirObject.storage)) {
426
- const parsedSerializable = (0, internal_6.parseHandles)(serializable, this.serializer);
386
+ const parsedSerializable = (0, internal_5.parseHandles)(serializable, this.serializer);
427
387
  (0, localValues_js_1.migrateIfSharedSerializable)(parsedSerializable, this.serializer, this.handle);
428
388
  currentSubDir.populateStorage(key, parsedSerializable.value);
429
389
  }
@@ -459,114 +419,87 @@ class SharedDirectory extends internal_6.SharedObject {
459
419
  makeAbsolute(relativePath) {
460
420
  return posix.resolve(posix.sep, relativePath);
461
421
  }
462
- /**
463
- * This checks if there is pending delete op for local delete for a any subdir in the relative path.
464
- * @param relativePath - path of sub directory.
465
- * @returns `true` if there is pending delete, `false` otherwise.
466
- */
467
- isSubDirectoryDeletePending(relativePath) {
468
- const absolutePath = this.makeAbsolute(relativePath);
469
- if (absolutePath === posix.sep) {
470
- return false;
471
- }
472
- let currentParent = this.root;
473
- const pathParts = absolutePath.split(posix.sep).slice(1);
474
- for (const dirName of pathParts) {
475
- if (currentParent.isSubDirectoryDeletePending(dirName)) {
476
- return true;
477
- }
478
- currentParent = currentParent.getSubDirectory(dirName);
479
- if (currentParent === undefined) {
480
- return true;
481
- }
482
- }
483
- return false;
484
- }
485
422
  /**
486
423
  * Set the message handlers for the directory.
487
424
  */
488
425
  setMessageHandlers() {
426
+ // Notes on how we target the correct subdirectory:
427
+ // `process`: When processing ops, we only ever want to process ops on sequenced directories. This prevents
428
+ // scenarios where ops could be processed on a pending directory instead of a sequenced directory,
429
+ // leading to ops effectively being processed out of order.
430
+ // `resubmit`: When resubmitting ops, we use `localOpMetadata` to get a reference to the subdirectory that
431
+ // the op was originally targeting.
489
432
  this.messageHandlers.set("clear", {
490
433
  process: (msg, op, local, localOpMetadata) => {
491
- const subdir = this.getWorkingDirectory(op.path);
492
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
493
- // as we are going to delete this subDirectory.
494
- if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
434
+ const subdir = this.getSequencedWorkingDirectory(op.path);
435
+ if (subdir !== undefined && !subdir?.disposed) {
495
436
  subdir.processClearMessage(msg, op, local, localOpMetadata);
496
437
  }
497
438
  },
498
439
  resubmit: (op, localOpMetadata) => {
499
- const subdir = this.getWorkingDirectory(op.path);
500
- if (subdir) {
501
- subdir.resubmitClearMessage(op, localOpMetadata);
440
+ const targetSubdir = localOpMetadata.subdir;
441
+ if (!targetSubdir.disposed) {
442
+ targetSubdir.resubmitClearMessage(op, localOpMetadata);
502
443
  }
503
444
  },
504
445
  });
505
446
  this.messageHandlers.set("delete", {
506
447
  process: (msg, op, local, localOpMetadata) => {
507
- const subdir = this.getWorkingDirectory(op.path);
508
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
509
- // as we are going to delete this subDirectory.
510
- if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
448
+ const subdir = this.getSequencedWorkingDirectory(op.path);
449
+ if (subdir !== undefined && !subdir?.disposed) {
511
450
  subdir.processDeleteMessage(msg, op, local, localOpMetadata);
512
451
  }
513
452
  },
514
453
  resubmit: (op, localOpMetadata) => {
515
- const subdir = this.getWorkingDirectory(op.path);
516
- if (subdir) {
517
- subdir.resubmitKeyMessage(op, localOpMetadata);
454
+ const targetSubdir = localOpMetadata.subdir;
455
+ if (!targetSubdir.disposed) {
456
+ targetSubdir.resubmitKeyMessage(op, localOpMetadata);
518
457
  }
519
458
  },
520
459
  });
521
460
  this.messageHandlers.set("set", {
522
461
  process: (msg, op, local, localOpMetadata) => {
523
- const subdir = this.getWorkingDirectory(op.path);
524
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
525
- // as we are going to delete this subDirectory.
526
- if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
462
+ const subdir = this.getSequencedWorkingDirectory(op.path);
463
+ if (subdir !== undefined && !subdir?.disposed) {
527
464
  (0, localValues_js_1.migrateIfSharedSerializable)(op.value, this.serializer, this.handle);
528
465
  const localValue = local ? undefined : op.value.value;
529
466
  subdir.processSetMessage(msg, op, localValue, local, localOpMetadata);
530
467
  }
531
468
  },
532
469
  resubmit: (op, localOpMetadata) => {
533
- const subdir = this.getWorkingDirectory(op.path);
534
- if (subdir) {
535
- subdir.resubmitKeyMessage(op, localOpMetadata);
470
+ const targetSubdir = localOpMetadata.subdir;
471
+ if (!targetSubdir.disposed) {
472
+ targetSubdir.resubmitKeyMessage(op, localOpMetadata);
536
473
  }
537
474
  },
538
475
  });
539
476
  this.messageHandlers.set("createSubDirectory", {
540
477
  process: (msg, op, local, localOpMetadata) => {
541
- const parentSubdir = this.getWorkingDirectory(op.path);
542
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
543
- // as we are going to delete this subDirectory.
544
- if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
478
+ const parentSubdir = this.getSequencedWorkingDirectory(op.path);
479
+ if (parentSubdir !== undefined && !parentSubdir?.disposed) {
545
480
  parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
546
481
  }
547
482
  },
548
483
  resubmit: (op, localOpMetadata) => {
549
- const parentSubdir = this.getWorkingDirectory(op.path);
550
- if (parentSubdir) {
484
+ const targetSubdir = localOpMetadata.parentSubdir;
485
+ if (!targetSubdir.disposed) {
551
486
  // We don't reuse the metadata but send a new one on each submit.
552
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
487
+ targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
553
488
  }
554
489
  },
555
490
  });
556
491
  this.messageHandlers.set("deleteSubDirectory", {
557
492
  process: (msg, op, local, localOpMetadata) => {
558
- const parentSubdir = this.getWorkingDirectory(op.path);
559
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
560
- // as we are going to delete this subDirectory.
561
- if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
493
+ const parentSubdir = this.getSequencedWorkingDirectory(op.path);
494
+ if (parentSubdir !== undefined && !parentSubdir?.disposed) {
562
495
  parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
563
496
  }
564
497
  },
565
498
  resubmit: (op, localOpMetadata) => {
566
- const parentSubdir = this.getWorkingDirectory(op.path);
567
- if (parentSubdir) {
499
+ const targetSubdir = localOpMetadata.parentSubdir;
500
+ if (!targetSubdir.disposed) {
568
501
  // We don't reuse the metadata but send a new one on each submit.
569
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
502
+ targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
570
503
  }
571
504
  },
572
505
  });
@@ -606,7 +539,7 @@ class SharedDirectory extends internal_6.SharedObject {
606
539
  }
607
540
  serializeDirectory(root, serializer, telemetryContext) {
608
541
  const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
609
- const builder = new internal_5.SummaryTreeBuilder();
542
+ const builder = new internal_4.SummaryTreeBuilder();
610
543
  let counter = 0;
611
544
  const blobs = [];
612
545
  const stack = [];
@@ -667,7 +600,6 @@ exports.SharedDirectory = SharedDirectory;
667
600
  function assertNonNullClientId(clientId) {
668
601
  (0, internal_1.assert)(clientId !== null, 0x6af /* client id should never be null */);
669
602
  }
670
- let hasLoggedDirectoryInconsistency = false;
671
603
  /**
672
604
  * Node of the directory tree.
673
605
  * @sealed
@@ -690,7 +622,6 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
690
622
  this.runtime = runtime;
691
623
  this.serializer = serializer;
692
624
  this.absolutePath = absolutePath;
693
- this.logger = logger;
694
625
  /**
695
626
  * Tells if the sub directory is deleted or not.
696
627
  */
@@ -700,21 +631,10 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
700
631
  */
701
632
  this[_b] = "SubDirectory";
702
633
  /**
703
- * The subdirectories the directory is holding.
634
+ * The sequenced subdirectories the directory is holding independent of any pending
635
+ * create/delete subdirectory operations.
704
636
  */
705
- this._subdirectories = new Map();
706
- /**
707
- * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
708
- * of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
709
- * deleted sub directory.
710
- */
711
- this.pendingDeleteSubDirectoriesTracker = new Map();
712
- /**
713
- * Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
714
- * of create op that are pending or yet to be acked from server. This is maintained just to track the locally
715
- * created sub directory.
716
- */
717
- this.pendingCreateSubDirectoriesTracker = new Map();
637
+ this._sequencedSubdirectories = new Map();
718
638
  /**
719
639
  * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
720
640
  * of the creation order.
@@ -733,6 +653,11 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
733
653
  * even across remote operations and rollbacks.
734
654
  */
735
655
  this.pendingStorageData = [];
656
+ /**
657
+ * A data structure containing all local pending subdirectory create/deletes, which is used in combination
658
+ * with the _sequencedSubdirectories to compute optimistic values.
659
+ */
660
+ this.pendingSubDirectoryData = [];
736
661
  /**
737
662
  * An internal iterator that iterates over the entries in the directory.
738
663
  */
@@ -821,8 +746,33 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
821
746
  ? this.sequencedStorageData.has(key)
822
747
  : latestPendingEntry.type === "lifetime";
823
748
  };
824
- this.localCreationSeqTracker = new DirectoryCreationTracker();
825
- this.ackedCreationSeqTracker = new DirectoryCreationTracker();
749
+ /**
750
+ * Get the optimistic local subdirectory. This combines the sequenced data with
751
+ * any pending changes that have not yet been sequenced. By default, we do not
752
+ * consider disposed directories as optimistically existing, but if `getIfDisposed`
753
+ * is true, we will include them since some scenarios require this.
754
+ */
755
+ this.getOptimisticSubDirectory = (subdirName, getIfDisposed = false) => {
756
+ const latestPendingEntry = (0, utils_js_1.findLast)(this.pendingSubDirectoryData, (entry) => entry.subdirName === subdirName);
757
+ let subdir;
758
+ if (latestPendingEntry === undefined) {
759
+ subdir = this._sequencedSubdirectories.get(subdirName);
760
+ }
761
+ else if (latestPendingEntry.type === "createSubDirectory") {
762
+ subdir = latestPendingEntry.subdir;
763
+ (0, internal_1.assert)(subdir !== undefined, 0xc2f /* Subdirectory should exist in pending data */);
764
+ }
765
+ else {
766
+ // Pending delete
767
+ return undefined;
768
+ }
769
+ // If the subdirectory is disposed, treat it as non-existent for optimistic reads (unless specified otherwise)
770
+ if (subdir?.disposed && !getIfDisposed) {
771
+ return undefined;
772
+ }
773
+ return subdir;
774
+ };
775
+ this.mc = (0, internal_6.createChildMonitoringContext)({ logger, namespace: "Directory" });
826
776
  }
827
777
  dispose(error) {
828
778
  this._deleted = true;
@@ -840,7 +790,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
840
790
  }
841
791
  throwIfDisposed() {
842
792
  if (this._deleted) {
843
- throw new internal_7.UsageError("Cannot access Disposed subDirectory");
793
+ throw new internal_6.UsageError("Cannot access Disposed subDirectory");
844
794
  }
845
795
  }
846
796
  /**
@@ -868,8 +818,12 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
868
818
  throw new Error("Undefined and null keys are not supported");
869
819
  }
870
820
  const previousOptimisticLocalValue = this.getOptimisticValue(key);
871
- // Create a local value and serialize it.
872
- (0, internal_6.bindHandles)(value, this.serializer, this.directory.handle);
821
+ const detachedBind = this.mc.config.getBoolean("Fluid.Directory.AllowDetachedResolve") ?? false;
822
+ if (detachedBind) {
823
+ // Create a local value and serialize it.
824
+ // AB#47081: This will be removed once we can validate that it is no longer needed.
825
+ (0, internal_5.bindHandles)(value, this.serializer, this.directory.handle);
826
+ }
873
827
  // If we are not attached, don't submit the op.
874
828
  if (!this.directory.isAttached()) {
875
829
  this.sequencedStorageData.set(key, value);
@@ -894,20 +848,27 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
894
848
  if (latestPendingEntry === undefined ||
895
849
  latestPendingEntry.type === "delete" ||
896
850
  latestPendingEntry.type === "clear") {
897
- latestPendingEntry = { type: "lifetime", path: this.absolutePath, key, keySets: [] };
851
+ latestPendingEntry = {
852
+ type: "lifetime",
853
+ path: this.absolutePath,
854
+ key,
855
+ keySets: [],
856
+ subdir: this,
857
+ };
898
858
  this.pendingStorageData.push(latestPendingEntry);
899
859
  }
900
860
  const pendingKeySet = {
901
861
  type: "set",
902
862
  path: this.absolutePath,
903
863
  value,
864
+ subdir: this,
904
865
  };
905
866
  latestPendingEntry.keySets.push(pendingKeySet);
906
867
  const op = {
907
868
  key,
908
869
  path: this.absolutePath,
909
870
  type: "set",
910
- value: { type: internal_6.ValueType[internal_6.ValueType.Plain], value },
871
+ value: { type: internal_5.ValueType[internal_5.ValueType.Plain], value },
911
872
  };
912
873
  this.submitKeyMessage(op, pendingKeySet);
913
874
  const directoryValueChanged = {
@@ -927,7 +888,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
927
888
  * {@inheritDoc IDirectory.countSubDirectory}
928
889
  */
929
890
  countSubDirectory() {
930
- return this._subdirectories.size;
891
+ return [...this.subdirectories()].length;
931
892
  }
932
893
  /**
933
894
  * {@inheritDoc IDirectory.createSubDirectory}
@@ -941,22 +902,47 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
941
902
  if (subdirName.includes(posix.sep)) {
942
903
  throw new Error(`SubDirectory name may not contain ${posix.sep}`);
943
904
  }
944
- // Create the sub directory locally first.
945
- const isNew = this.createSubDirectoryCore(subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
946
- const subDir = this._subdirectories.get(subdirName);
947
- (0, internal_1.assert)(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
948
- // If we are not attached, don't submit the op.
949
- if (!this.directory.isAttached()) {
950
- return subDir;
905
+ let subDir = this.getOptimisticSubDirectory(subdirName, true);
906
+ const seqData = this.getLocalSeq();
907
+ const clientId = this.runtime.clientId ?? "detached";
908
+ const isNewSubDirectory = subDir === undefined;
909
+ if (subDir === undefined) {
910
+ // If we do not have optimistically have this subdirectory yet, we should create a new one
911
+ const absolutePath = posix.join(this.absolutePath, subdirName);
912
+ subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.mc.logger);
951
913
  }
952
- // Only submit the op, if it is newly created.
953
- if (isNew) {
954
- const op = {
955
- path: this.absolutePath,
956
- subdirName,
957
- type: "createSubDirectory",
958
- };
959
- this.submitCreateSubDirectoryMessage(op);
914
+ else {
915
+ if (subDir.disposed) {
916
+ // In the case that the subdir exists but is disposed, we should
917
+ // still use the existing subdir to maintain any pending changes but
918
+ // ensure it is no longer disposed.
919
+ this.undisposeSubdirectoryTree(subDir);
920
+ }
921
+ subDir.clientIds.add(clientId);
922
+ }
923
+ this.registerEventsOnSubDirectory(subDir, subdirName);
924
+ // Only submit the op/emit event if we actually created a new subdir.
925
+ if (isNewSubDirectory) {
926
+ if (this.directory.isAttached()) {
927
+ const pendingSubDirectoryCreate = {
928
+ type: "createSubDirectory",
929
+ subdirName,
930
+ subdir: subDir,
931
+ };
932
+ this.pendingSubDirectoryData.push(pendingSubDirectoryCreate);
933
+ const op = {
934
+ subdirName,
935
+ path: this.absolutePath,
936
+ type: "createSubDirectory",
937
+ };
938
+ this.submitCreateSubDirectoryMessage(op);
939
+ }
940
+ else {
941
+ // If we are detached, don't submit the op and directly commit
942
+ // the subdir to _sequencedSubdirectories.
943
+ this._sequencedSubdirectories.set(subdirName, subDir);
944
+ }
945
+ this.emit("subDirectoryCreated", subdirName, true, this);
960
946
  }
961
947
  return subDir;
962
948
  }
@@ -981,81 +967,88 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
981
967
  */
982
968
  getSubDirectory(subdirName) {
983
969
  this.throwIfDisposed();
984
- return this._subdirectories.get(subdirName);
970
+ return this.getOptimisticSubDirectory(subdirName);
985
971
  }
986
972
  /**
987
973
  * {@inheritDoc IDirectory.hasSubDirectory}
988
974
  */
989
975
  hasSubDirectory(subdirName) {
990
976
  this.throwIfDisposed();
991
- return this._subdirectories.has(subdirName);
977
+ return this.getOptimisticSubDirectory(subdirName) !== undefined;
992
978
  }
993
979
  /**
994
980
  * {@inheritDoc IDirectory.deleteSubDirectory}
995
981
  */
996
982
  deleteSubDirectory(subdirName) {
997
983
  this.throwIfDisposed();
998
- // Delete the sub directory locally first.
999
- const subDir = this.deleteSubDirectoryCore(subdirName, true);
1000
- // If we are not attached, don't submit the op.
1001
984
  if (!this.directory.isAttached()) {
1002
- return subDir !== undefined;
985
+ const previousValue = this._sequencedSubdirectories.get(subdirName);
986
+ const successfullyRemoved = this._sequencedSubdirectories.delete(subdirName);
987
+ // Only emit if we actually deleted something.
988
+ if (successfullyRemoved) {
989
+ this.disposeSubDirectoryTree(previousValue);
990
+ this.emit("subDirectoryDeleted", subdirName, true, this);
991
+ }
992
+ return successfullyRemoved;
1003
993
  }
1004
- // Only submit the op, if the directory existed and we deleted it.
1005
- if (subDir !== undefined) {
1006
- const op = {
1007
- path: this.absolutePath,
1008
- subdirName,
1009
- type: "deleteSubDirectory",
1010
- };
1011
- this.submitDeleteSubDirectoryMessage(op, subDir);
994
+ const previousOptimisticSubDirectory = this.getOptimisticSubDirectory(subdirName);
995
+ if (previousOptimisticSubDirectory === undefined) {
996
+ return false;
1012
997
  }
1013
- return subDir !== undefined;
998
+ const pendingSubdirDelete = {
999
+ type: "deleteSubDirectory",
1000
+ subdirName,
1001
+ subdir: this,
1002
+ };
1003
+ this.pendingSubDirectoryData.push(pendingSubdirDelete);
1004
+ const op = {
1005
+ subdirName,
1006
+ type: "deleteSubDirectory",
1007
+ path: this.absolutePath,
1008
+ };
1009
+ this.submitDeleteSubDirectoryMessage(op, previousOptimisticSubDirectory);
1010
+ this.emit("subDirectoryDeleted", subdirName, true, this);
1011
+ // We don't want to fully dispose the subdir tree since this is only a pending
1012
+ // local delete. Instead we will only emit the dispose event to reflect the
1013
+ // local state.
1014
+ this.emitDisposeForSubdirTree(previousOptimisticSubDirectory);
1015
+ return true;
1014
1016
  }
1015
1017
  /**
1016
1018
  * {@inheritDoc IDirectory.subdirectories}
1017
1019
  */
1018
1020
  subdirectories() {
1019
1021
  this.throwIfDisposed();
1020
- const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
1021
- const localSubdirsInOrder = this.localCreationSeqTracker.keys((key) => !this.ackedCreationSeqTracker.has(key));
1022
- const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1023
- if (subdirNames.length !== this._subdirectories.size) {
1024
- // TODO: AB#7022: Hitting this block indicates that the eventual consistency scheme for ordering subdirectories
1025
- // has failed. Fall back to previous directory behavior, which didn't guarantee ordering.
1026
- // It's not currently clear how to reach this state, so log some diagnostics to help understand the issue.
1027
- // This whole block should eventually be replaced by an assert that the two sizes align.
1028
- if (!hasLoggedDirectoryInconsistency) {
1029
- this.logger.sendTelemetryEvent({
1030
- eventName: "inconsistentSubdirectoryOrdering",
1031
- localKeyCount: this.localCreationSeqTracker.size,
1032
- ackedKeyCount: this.ackedCreationSeqTracker.size,
1033
- subdirNamesLength: subdirNames.length,
1034
- subdirectoriesSize: this._subdirectories.size,
1035
- });
1036
- hasLoggedDirectoryInconsistency = true;
1022
+ // subdirectories() should reflect the optimistic state of subdirectories.
1023
+ // This means that we should return both sequenced and pending subdirectories
1024
+ // that do not also have a pending deletion.
1025
+ const sequencedSubdirs = [];
1026
+ const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
1027
+ for (const subdirName of sequencedSubdirNames) {
1028
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
1029
+ if (optimisticSubdir !== undefined) {
1030
+ sequencedSubdirs.push([subdirName, optimisticSubdir]);
1037
1031
  }
1038
- return this._subdirectories.entries();
1039
- }
1040
- const entriesIterator = {
1041
- index: 0,
1042
- dirs: this._subdirectories,
1043
- next() {
1044
- if (this.index < subdirNames.length) {
1045
- // Bounds check above guarantees non-null (at least at compile time, assuming all types are respected)
1046
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1047
- const subdirName = subdirNames[this.index++];
1048
- const subdir = this.dirs.get(subdirName);
1049
- (0, internal_1.assert)(subdir !== undefined, 0x8ac /* Could not find expected sub-directory. */);
1050
- return { value: [subdirName, subdir], done: false };
1051
- }
1052
- return { value: undefined, done: true };
1053
- },
1054
- [Symbol.iterator]() {
1055
- return this;
1056
- },
1057
- };
1058
- return entriesIterator;
1032
+ }
1033
+ const pendingSubdirNames = [
1034
+ ...new Set(this.pendingSubDirectoryData
1035
+ .map((entry) => entry.subdirName)
1036
+ .filter((subdirName) => !sequencedSubdirNames.has(subdirName))),
1037
+ ];
1038
+ const pendingSubdirs = [];
1039
+ for (const subdirName of pendingSubdirNames) {
1040
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
1041
+ if (optimisticSubdir !== undefined) {
1042
+ pendingSubdirs.push([subdirName, optimisticSubdir]);
1043
+ }
1044
+ }
1045
+ const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
1046
+ const orderedSubdirs = allSubdirs.sort((a, b) => {
1047
+ const aSeqData = a[1].seqData;
1048
+ const bSeqData = b[1].seqData;
1049
+ return seqDataComparator(aSeqData, bSeqData);
1050
+ });
1051
+ return orderedSubdirs[Symbol.iterator]();
1059
1052
  }
1060
1053
  /**
1061
1054
  * {@inheritDoc IDirectory.getWorkingDirectory}
@@ -1070,10 +1063,10 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1070
1063
  * @returns true if there is pending delete.
1071
1064
  */
1072
1065
  isSubDirectoryDeletePending(subDirName) {
1073
- if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
1074
- return true;
1075
- }
1076
- return false;
1066
+ const lastPendingEntry = (0, utils_js_1.findLast)(this.pendingSubDirectoryData, (entry) => {
1067
+ return entry.subdirName === subDirName && entry.type === "deleteSubDirectory";
1068
+ });
1069
+ return lastPendingEntry !== undefined;
1077
1070
  }
1078
1071
  /**
1079
1072
  * Deletes the given key from within this IDirectory.
@@ -1105,6 +1098,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1105
1098
  type: "delete",
1106
1099
  path: this.absolutePath,
1107
1100
  key,
1101
+ subdir: this,
1108
1102
  };
1109
1103
  this.pendingStorageData.push(pendingKeyDelete);
1110
1104
  const op = {
@@ -1144,6 +1138,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1144
1138
  const pendingClear = {
1145
1139
  type: "clear",
1146
1140
  path: this.absolutePath,
1141
+ subdir: this,
1147
1142
  };
1148
1143
  this.pendingStorageData.push(pendingClear);
1149
1144
  this.directory.emit("clear", true, this.directory);
@@ -1248,6 +1243,10 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1248
1243
  this.throwIfDisposed();
1249
1244
  return this.internalIterator();
1250
1245
  }
1246
+ get sequencedSubdirectories() {
1247
+ this.throwIfDisposed();
1248
+ return this._sequencedSubdirectories;
1249
+ }
1251
1250
  /**
1252
1251
  * Process a clear operation.
1253
1252
  * @param msg - The message from the server to apply.
@@ -1258,7 +1257,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1258
1257
  */
1259
1258
  processClearMessage(msg, op, local, localOpMetadata) {
1260
1259
  this.throwIfDisposed();
1261
- if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1260
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
1262
1261
  return;
1263
1262
  }
1264
1263
  if (local) {
@@ -1302,7 +1301,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1302
1301
  */
1303
1302
  processDeleteMessage(msg, op, local, localOpMetadata) {
1304
1303
  this.throwIfDisposed();
1305
- if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1304
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
1306
1305
  return;
1307
1306
  }
1308
1307
  if (local) {
@@ -1340,7 +1339,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1340
1339
  */
1341
1340
  processSetMessage(msg, op, value, local, localOpMetadata) {
1342
1341
  this.throwIfDisposed();
1343
- if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1342
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
1344
1343
  return;
1345
1344
  }
1346
1345
  const { key } = op;
@@ -1378,12 +1377,58 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1378
1377
  */
1379
1378
  processCreateSubDirectoryMessage(msg, op, local, localOpMetadata) {
1380
1379
  this.throwIfDisposed();
1381
- if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1382
- this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
1380
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
1383
1381
  return;
1384
1382
  }
1385
1383
  assertNonNullClientId(msg.clientId);
1386
- this.createSubDirectoryCore(op.subdirName, local, { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, msg.clientId);
1384
+ let subDir;
1385
+ if (local) {
1386
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
1387
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1388
+ (0, internal_1.assert)(pendingEntry !== undefined && pendingEntry.type === "createSubDirectory", 0xc30 /* Got a local subdir create message we weren't expecting */);
1389
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1390
+ subDir = pendingEntry.subdir;
1391
+ const existingSubdir = this._sequencedSubdirectories.get(op.subdirName);
1392
+ if (existingSubdir !== undefined) {
1393
+ // If the subdirectory already exists, we don't need to create it again.
1394
+ // This can happen if remote clients also create the same subdir and we processed
1395
+ // that message first.
1396
+ return;
1397
+ }
1398
+ if (subDir.disposed) {
1399
+ this.undisposeSubdirectoryTree(subDir);
1400
+ }
1401
+ this._sequencedSubdirectories.set(op.subdirName, subDir);
1402
+ }
1403
+ else {
1404
+ subDir = this.getOptimisticSubDirectory(op.subdirName, true);
1405
+ if (subDir === undefined) {
1406
+ const absolutePath = posix.join(this.absolutePath, op.subdirName);
1407
+ subDir = new SubDirectory({ seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, new Set([msg.clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.mc.logger);
1408
+ }
1409
+ else {
1410
+ // If the subdirectory already optimistically exists, we don't need to create it again.
1411
+ // This can happen if remote clients also created the same subdir.
1412
+ if (subDir.disposed) {
1413
+ this.undisposeSubdirectoryTree(subDir);
1414
+ }
1415
+ subDir.clientIds.add(msg.clientId);
1416
+ }
1417
+ this.registerEventsOnSubDirectory(subDir, op.subdirName);
1418
+ this._sequencedSubdirectories.set(op.subdirName, subDir);
1419
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
1420
+ if (!this.pendingSubDirectoryData.some((entry) => entry.subdirName === op.subdirName)) {
1421
+ this.emit("subDirectoryCreated", op.subdirName, local, this);
1422
+ }
1423
+ }
1424
+ // Ensure correct seqData. This can be necessary if in scenarios where a subdir was created, deleted, and
1425
+ // then later recreated.
1426
+ if (this.seqData.seq !== -1 &&
1427
+ this.seqData.seq <= msg.sequenceNumber &&
1428
+ subDir.seqData.seq === -1) {
1429
+ subDir.seqData.seq = msg.sequenceNumber;
1430
+ subDir.seqData.clientSeq = msg.clientSequenceNumber;
1431
+ }
1387
1432
  }
1388
1433
  /**
1389
1434
  * Process a delete subdirectory operation.
@@ -1395,11 +1440,43 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1395
1440
  */
1396
1441
  processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata) {
1397
1442
  this.throwIfDisposed();
1398
- if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1399
- this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
1443
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
1400
1444
  return;
1401
1445
  }
1402
- this.deleteSubDirectoryCore(op.subdirName, local);
1446
+ const previousValue = this._sequencedSubdirectories.get(op.subdirName);
1447
+ if (previousValue === undefined) {
1448
+ // We are trying to delete a subdirectory that does not exist.
1449
+ // If this is a local delete, we should remove the pending delete entry.
1450
+ // This could happen if we already processed a remote delete op for
1451
+ // the same subdirectory.
1452
+ if (local) {
1453
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
1454
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1455
+ (0, internal_1.assert)(pendingEntry !== undefined &&
1456
+ pendingEntry.type === "deleteSubDirectory" &&
1457
+ pendingEntry.subdirName === op.subdirName, 0xc31 /* Got a local deleteSubDirectory message we weren't expecting */);
1458
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1459
+ }
1460
+ return;
1461
+ }
1462
+ this._sequencedSubdirectories.delete(op.subdirName);
1463
+ this.disposeSubDirectoryTree(previousValue);
1464
+ if (local) {
1465
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
1466
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1467
+ (0, internal_1.assert)(pendingEntry !== undefined &&
1468
+ pendingEntry.type === "deleteSubDirectory" &&
1469
+ pendingEntry.subdirName === op.subdirName, 0xc32 /* Got a local deleteSubDirectory message we weren't expecting */);
1470
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1471
+ }
1472
+ else {
1473
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
1474
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory");
1475
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1476
+ if (pendingEntry === undefined) {
1477
+ this.emit("subDirectoryDeleted", op.subdirName, local, this);
1478
+ }
1479
+ }
1403
1480
  }
1404
1481
  /**
1405
1482
  * Submit a clear operation.
@@ -1440,44 +1517,28 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1440
1517
  resubmitKeyMessage(op, localOpMetadata) {
1441
1518
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1442
1519
  // is already deleted, in which case we don't need to submit the op.
1443
- const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === op.key);
1520
+ const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => {
1521
+ return op.type === "set"
1522
+ ? entry.type === "lifetime" &&
1523
+ entry.key === op.key &&
1524
+ // We also check that the keySets include the localOpMetadata. It's possible we have new
1525
+ // pending key sets that are not the op we are looking for.
1526
+ entry.keySets.includes(localOpMetadata)
1527
+ : entry.type === "delete" && entry.key === op.key;
1528
+ });
1444
1529
  const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1445
1530
  if (pendingEntry !== undefined) {
1446
1531
  this.submitKeyMessage(op, localOpMetadata);
1447
1532
  }
1448
1533
  }
1449
- incrementPendingSubDirCount(map, subDirName) {
1450
- const count = map.get(subDirName) ?? 0;
1451
- map.set(subDirName, count + 1);
1452
- }
1453
- decrementPendingSubDirCount(map, subDirName) {
1454
- const count = map.get(subDirName) ?? 0;
1455
- map.set(subDirName, count - 1);
1456
- if (count <= 1) {
1457
- map.delete(subDirName);
1458
- }
1459
- }
1460
- /**
1461
- * Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
1462
- * or while resubmitting the op.
1463
- */
1464
- updatePendingSubDirMessageCount(op) {
1465
- if (op.type === "deleteSubDirectory") {
1466
- this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1467
- }
1468
- else if (op.type === "createSubDirectory") {
1469
- this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1470
- }
1471
- }
1472
1534
  /**
1473
1535
  * Submit a create subdirectory operation.
1474
1536
  * @param op - The operation
1475
1537
  */
1476
1538
  submitCreateSubDirectoryMessage(op) {
1477
- this.throwIfDisposed();
1478
- this.updatePendingSubDirMessageCount(op);
1479
1539
  const localOpMetadata = {
1480
1540
  type: "createSubDir",
1541
+ parentSubdir: this,
1481
1542
  };
1482
1543
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1483
1544
  }
@@ -1487,11 +1548,10 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1487
1548
  * @param subDir - Any subdirectory deleted by the op
1488
1549
  */
1489
1550
  submitDeleteSubDirectoryMessage(op, subDir) {
1490
- this.throwIfDisposed();
1491
- this.updatePendingSubDirMessageCount(op);
1492
1551
  const localOpMetadata = {
1493
1552
  type: "deleteSubDir",
1494
1553
  subDirectory: subDir,
1554
+ parentSubdir: this,
1495
1555
  };
1496
1556
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1497
1557
  }
@@ -1503,22 +1563,25 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1503
1563
  resubmitSubDirectoryMessage(op, localOpMetadata) {
1504
1564
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1505
1565
  // is already deleted, in which case we don't need to submit the op.
1506
- if (localOpMetadata.type === "createSubDir" &&
1507
- !this.pendingCreateSubDirectoriesTracker.has(op.subdirName)) {
1508
- return;
1509
- }
1510
- else if (localOpMetadata.type === "deleteSubDir" &&
1511
- !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1512
- return;
1513
- }
1514
1566
  if (localOpMetadata.type === "createSubDir") {
1515
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1516
- this.submitCreateSubDirectoryMessage(op);
1567
+ // For create operations, look specifically for createSubDirectory entries
1568
+ const pendingEntry = (0, utils_js_1.findLast)(this.pendingSubDirectoryData, (entry) => entry.subdirName === op.subdirName && entry.type === "createSubDirectory");
1569
+ if (pendingEntry !== undefined) {
1570
+ (0, internal_1.assert)(pendingEntry.type === "createSubDirectory", 0xc33 /* pending entry should be createSubDirectory */);
1571
+ // We should add the client id, since when reconnecting it can have a different client id.
1572
+ pendingEntry.subdir.clientIds.add(this.runtime.clientId ?? "detached");
1573
+ // We also need to undelete the subdirectory tree if it was previously deleted
1574
+ this.undisposeSubdirectoryTree(pendingEntry.subdir);
1575
+ this.submitCreateSubDirectoryMessage(op);
1576
+ }
1517
1577
  }
1518
1578
  else if (localOpMetadata.type === "deleteSubDir") {
1519
- this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1520
- (0, internal_1.assert)(localOpMetadata.subDirectory !== undefined, 0xc08 /* localOpMetadata.subDirectory should be defined */);
1521
- this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1579
+ (0, internal_1.assert)(localOpMetadata.subDirectory !== undefined, 0xc34 /* Subdirectory should exist */);
1580
+ // For delete operations, look specifically for deleteSubDirectory entries
1581
+ const pendingEntry = (0, utils_js_1.findLast)(this.pendingSubDirectoryData, (entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory");
1582
+ if (pendingEntry !== undefined) {
1583
+ this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1584
+ }
1522
1585
  }
1523
1586
  }
1524
1587
  /**
@@ -1559,7 +1622,7 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1559
1622
  populateSubDirectory(subdirName, newSubDir) {
1560
1623
  this.throwIfDisposed();
1561
1624
  this.registerEventsOnSubDirectory(newSubDir, subdirName);
1562
- this._subdirectories.set(subdirName, newSubDir);
1625
+ this._sequencedSubdirectories.set(subdirName, newSubDir);
1563
1626
  }
1564
1627
  /**
1565
1628
  * Rollback a local op
@@ -1572,9 +1635,13 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1572
1635
  if (directoryOp.type === "clear") {
1573
1636
  // A pending clear will be last in the list, since it terminates all prior lifetimes.
1574
1637
  const pendingClear = this.pendingStorageData.pop();
1575
- (0, internal_1.assert)(pendingClear !== undefined &&
1576
- pendingClear.type === "clear" &&
1577
- localOpMetadata.type === "clear", 0xc09 /* Unexpected clear rollback */);
1638
+ if (pendingClear === undefined) {
1639
+ // If we can't find a pending entry then it's possible that we deleted an ack'd subdir
1640
+ // from a remote delete subdir op. If that's the case then there is nothing to rollback
1641
+ // since the pending data was removed with the subdirectory deletion.
1642
+ return;
1643
+ }
1644
+ (0, internal_1.assert)(pendingClear.type === "clear" && localOpMetadata.type === "clear", 0xc35 /* Unexpected clear rollback */);
1578
1645
  for (const [key] of this.internalIterator()) {
1579
1646
  const event = {
1580
1647
  key,
@@ -1592,8 +1659,13 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1592
1659
  // they were created, not when they were last modified.
1593
1660
  const pendingEntryIndex = (0, utils_js_1.findLastIndex)(this.pendingStorageData, (entry) => entry.type !== "clear" && entry.key === directoryOp.key);
1594
1661
  const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1595
- (0, internal_1.assert)(pendingEntry !== undefined &&
1596
- (pendingEntry.type === "delete" || pendingEntry.type === "lifetime"), 0xc0a /* Unexpected pending data for set/delete op */);
1662
+ if (pendingEntry === undefined) {
1663
+ // If we can't find a pending entry then it's possible that we deleted an ack'd subdir
1664
+ // from a remote delete subdir op. If that's the case then there is nothing to rollback
1665
+ // since the pending data was removed with the subdirectory deletion.
1666
+ return;
1667
+ }
1668
+ (0, internal_1.assert)(pendingEntry.type === "delete" || pendingEntry.type === "lifetime", 0xc36 /* Unexpected pending data for set/delete op */);
1597
1669
  if (pendingEntry.type === "delete") {
1598
1670
  (0, internal_1.assert)(pendingEntry === localOpMetadata, 0xc0b /* Unexpected delete rollback */);
1599
1671
  this.pendingStorageData.splice(pendingEntryIndex, 1);
@@ -1634,34 +1706,33 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1634
1706
  else if (directoryOp.type === "createSubDirectory" &&
1635
1707
  localOpMetadata.type === "createSubDir") {
1636
1708
  const subdirName = directoryOp.subdirName;
1637
- (0, internal_1.assert)(subdirName !== undefined, 0x8af /* "subdirName" property is missing from "createSubDirectory" operation. */);
1638
- (0, internal_1.assert)(typeof subdirName === "string", 0x8b0 /* "subdirName" property in "createSubDirectory" operation is misconfigured. Expected a string. */);
1639
- this.deleteSubDirectoryCore(subdirName, true);
1640
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, subdirName);
1709
+ const pendingEntryIndex = (0, utils_js_1.findLastIndex)(this.pendingSubDirectoryData, (entry) => entry.type === "createSubDirectory" && entry.subdirName === subdirName);
1710
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1711
+ (0, internal_1.assert)(pendingEntry !== undefined && pendingEntry.type === "createSubDirectory", 0xc37 /* Unexpected pending data for createSubDirectory op */);
1712
+ // We still need to emit the disposed event for any locally created (and now
1713
+ // rolled back) subdirectory trees so listeners can observer the lifecycle
1714
+ // changes properly. We don't want to fully delete in case there is another
1715
+ // operation that references the same subdirectory.
1716
+ this.emitDisposeForSubdirTree(pendingEntry.subdir);
1717
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1718
+ this.emit("subDirectoryDeleted", subdirName, true, this);
1641
1719
  }
1642
1720
  else if (directoryOp.type === "deleteSubDirectory" &&
1643
1721
  localOpMetadata.type === "deleteSubDir") {
1644
1722
  const subdirName = directoryOp.subdirName;
1645
- (0, internal_1.assert)(subdirName !== undefined, 0x8b1 /* "subdirName" property is missing from "deleteSubDirectory" operation. */);
1646
- (0, internal_1.assert)(typeof subdirName === "string", 0x8b2 /* "subdirName" property in "deleteSubDirectory" operation is misconfigured. Expected a string. */);
1647
- if (localOpMetadata.subDirectory !== undefined) {
1648
- this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
1649
- // don't need to register events because deleting never unregistered
1650
- this._subdirectories.set(subdirName, localOpMetadata.subDirectory);
1651
- // Restore the record in creation tracker
1652
- if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
1653
- this.ackedCreationSeqTracker.set(subdirName, {
1654
- ...localOpMetadata.subDirectory.seqData,
1655
- });
1656
- }
1657
- else {
1658
- this.localCreationSeqTracker.set(subdirName, {
1659
- ...localOpMetadata.subDirectory.seqData,
1660
- });
1661
- }
1662
- this.emit("subDirectoryCreated", subdirName, true, this);
1663
- }
1664
- this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, subdirName);
1723
+ const pendingEntryIndex = (0, utils_js_1.findLastIndex)(this.pendingSubDirectoryData, (entry) => entry.type === "deleteSubDirectory" && entry.subdirName === subdirName);
1724
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1725
+ (0, internal_1.assert)(pendingEntry !== undefined && pendingEntry.type === "deleteSubDirectory", 0xc38 /* Unexpected pending data for deleteSubDirectory op */);
1726
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1727
+ // Restore the subdirectory
1728
+ const subDirectoryToRestore = localOpMetadata.subDirectory;
1729
+ (0, internal_1.assert)(subDirectoryToRestore !== undefined, 0xc39 /* Subdirectory should exist */);
1730
+ // Recursively undispose all nested subdirectories before adding to the map
1731
+ // This ensures the subdirectory is properly restored before being exposed
1732
+ this.undisposeSubdirectoryTree(subDirectoryToRestore);
1733
+ // Re-register events
1734
+ this.registerEventsOnSubDirectory(subDirectoryToRestore, subdirName);
1735
+ this.emit("subDirectoryCreated", subdirName, true, this);
1665
1736
  }
1666
1737
  else {
1667
1738
  throw new Error("Unsupported op for rollback");
@@ -1679,138 +1750,17 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1679
1750
  * This return true if the message is for the current instance of this sub directory. As the sub directory
1680
1751
  * can be deleted and created again, then this finds if the message is for current instance of directory or not.
1681
1752
  * @param msg - message for the directory
1682
- */
1683
- isMessageForCurrentInstanceOfSubDirectory(msg) {
1684
- // If the message is either from the creator of directory or this directory was created when
1685
- // container was detached or in case this directory is already live(known to other clients)
1686
- // and the op was created after the directory was created then apply this op.
1687
- return ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
1688
- this.clientIds.has("detached") ||
1689
- (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber));
1690
- }
1691
- /**
1692
- * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1693
- * not process the incoming operation.
1694
- * @param op - Operation to check
1695
- * @param local - Whether the message originated from the local client
1696
- * @param message - The message
1697
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1698
- * For messages from a remote client, this will be undefined.
1699
- * @returns True if the operation should be processed, false otherwise
1700
- */
1701
- needProcessSubDirectoryOperation(msg, op, local, localOpMetadata) {
1702
- assertNonNullClientId(msg.clientId);
1703
- const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
1704
- const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
1705
- if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
1706
- (pendingCreateCount !== undefined && pendingCreateCount > 0)) {
1707
- if (local) {
1708
- (0, internal_1.assert)(localOpMetadata !== undefined, 0xc0d /* localOpMetadata should be defined */);
1709
- if (localOpMetadata.type === "deleteSubDir") {
1710
- (0, internal_1.assert)(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
1711
- this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1712
- }
1713
- else if (localOpMetadata.type === "createSubDir") {
1714
- (0, internal_1.assert)(pendingCreateCount !== undefined && pendingCreateCount > 0, 0x6c3 /* pendingCreateCount should exist */);
1715
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1716
- }
1717
- }
1718
- if (op.type === "deleteSubDirectory") {
1719
- const resetSubDirectoryTree = (directory) => {
1720
- if (!directory) {
1721
- return;
1722
- }
1723
- // If this is delete op and we have keys in this subDirectory, then we need to delete these
1724
- // keys except the pending ones as they will be sequenced after this delete.
1725
- directory.sequencedStorageData.clear();
1726
- directory.emit("clear", true, directory);
1727
- // In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
1728
- // creators as the previous directory is getting deleted and we will initialize again when
1729
- // we will receive op for the create again.
1730
- directory.seqData.seq = -1;
1731
- directory.seqData.clientSeq = -1;
1732
- directory.clientIds.clear();
1733
- // Do the same thing for the subtree of the directory. If create is not pending for a child, then just
1734
- // delete it.
1735
- const subDirectories = directory.subdirectories();
1736
- for (const [subDirName, subDir] of subDirectories) {
1737
- if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
1738
- resetSubDirectoryTree(subDir);
1739
- continue;
1740
- }
1741
- directory.deleteSubDirectoryCore(subDirName, false);
1742
- }
1743
- };
1744
- const subDirectory = this._subdirectories.get(op.subdirName);
1745
- // Clear the creation tracker record
1746
- this.ackedCreationSeqTracker.delete(op.subdirName);
1747
- resetSubDirectoryTree(subDirectory);
1748
- }
1749
- if (op.type === "createSubDirectory") {
1750
- const dir = this._subdirectories.get(op.subdirName);
1751
- // Child sub directory create seq number can't be lower than the parent subdirectory.
1752
- // The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
1753
- if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
1754
- if (dir?.seqData.seq === -1) {
1755
- // Only set the sequence data based on the first message
1756
- dir.seqData.seq = msg.sequenceNumber;
1757
- dir.seqData.clientSeq = msg.clientSequenceNumber;
1758
- // set the creation seq in tracker
1759
- if (!this.ackedCreationSeqTracker.has(op.subdirName) &&
1760
- !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1761
- this.ackedCreationSeqTracker.set(op.subdirName, {
1762
- seq: msg.sequenceNumber,
1763
- clientSeq: msg.clientSequenceNumber,
1764
- });
1765
- if (local) {
1766
- this.localCreationSeqTracker.delete(op.subdirName);
1767
- }
1768
- }
1769
- }
1770
- // The client created the dir at or after the dirs seq, so list its client id as a creator.
1771
- if (dir !== undefined &&
1772
- !dir.clientIds.has(msg.clientId) &&
1773
- dir.seqData.seq <= msg.sequenceNumber) {
1774
- dir.clientIds.add(msg.clientId);
1775
- }
1776
- }
1777
- }
1778
- return false;
1779
- }
1780
- return !local;
1781
- }
1782
- /**
1783
- * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1784
- * @param subdirName - The name of the subdirectory being created
1785
- * @param local - Whether the message originated from the local client
1786
- * @param seqData - Sequence number and client sequence number at which this directory is created
1787
- * @param clientId - Id of client which created this directory.
1788
- * @returns True if is newly created, false if it already existed.
1789
- */
1790
- createSubDirectoryCore(subdirName, local, seqData, clientId) {
1791
- const subdir = this._subdirectories.get(subdirName);
1792
- if (subdir === undefined) {
1793
- const absolutePath = posix.join(this.absolutePath, subdirName);
1794
- const subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.logger);
1795
- /**
1796
- * Store the sequence numbers of newly created subdirectory to the proper creation tracker, based
1797
- * on whether the creation behavior has been ack'd or not
1798
- */
1799
- if (isAcknowledgedOrDetached(seqData)) {
1800
- this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
1801
- }
1802
- else {
1803
- this.localCreationSeqTracker.set(subdirName, { ...seqData });
1804
- }
1805
- this.registerEventsOnSubDirectory(subDir, subdirName);
1806
- this._subdirectories.set(subdirName, subDir);
1807
- this.emit("subDirectoryCreated", subdirName, local, this);
1808
- return true;
1809
- }
1810
- else {
1811
- subdir.clientIds.add(clientId);
1812
- }
1813
- return false;
1753
+ * @param targetSubdir - subdirectory instance we are targeting from local op metadata (if a local op)
1754
+ */
1755
+ isMessageForCurrentInstanceOfSubDirectory(msg, targetSubdir) {
1756
+ // The message must be from this instance of the directory (if a local op) AND one of the following must be true:
1757
+ // 1. The message was from the creator of this directory
1758
+ // 2. This directory was created while detached
1759
+ // 3. This directory was already live (known to other clients) and the op was created after the directory was created.
1760
+ return ((targetSubdir === undefined || targetSubdir === this) &&
1761
+ ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
1762
+ this.clientIds.has("detached") ||
1763
+ (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)));
1814
1764
  }
1815
1765
  registerEventsOnSubDirectory(subDirectory, subDirName) {
1816
1766
  subDirectory.on("subDirectoryCreated", (relativePath, local) => {
@@ -1820,34 +1770,8 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1820
1770
  this.emit("subDirectoryDeleted", posix.join(subDirName, relativePath), local, this);
1821
1771
  });
1822
1772
  }
1823
- /**
1824
- * Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1825
- * @param subdirName - The name of the subdirectory being deleted
1826
- * @param local - Whether the message originated from the local client
1827
- */
1828
- deleteSubDirectoryCore(subdirName, local) {
1829
- const previousValue = this._subdirectories.get(subdirName);
1830
- // This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
1831
- // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
1832
- if (previousValue !== undefined) {
1833
- this._subdirectories.delete(subdirName);
1834
- /**
1835
- * Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
1836
- * ack'd already or still not committed yet (could be both).
1837
- */
1838
- if (this.ackedCreationSeqTracker.has(subdirName)) {
1839
- this.ackedCreationSeqTracker.delete(subdirName);
1840
- }
1841
- if (this.localCreationSeqTracker.has(subdirName)) {
1842
- this.localCreationSeqTracker.delete(subdirName);
1843
- }
1844
- this.disposeSubDirectoryTree(previousValue);
1845
- this.emit("subDirectoryDeleted", subdirName, local, this);
1846
- }
1847
- return previousValue;
1848
- }
1849
1773
  disposeSubDirectoryTree(directory) {
1850
- if (!directory) {
1774
+ if (directory === undefined) {
1851
1775
  return;
1852
1776
  }
1853
1777
  // Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
@@ -1855,17 +1779,76 @@ class SubDirectory extends client_utils_1.TypedEventEmitter {
1855
1779
  for (const [_, subDirectory] of subDirectories) {
1856
1780
  this.disposeSubDirectoryTree(subDirectory);
1857
1781
  }
1782
+ // We need to reset the sequenced data as the previous directory is getting deleted and we will
1783
+ // initialize again when we will receive op for the create again.
1784
+ directory.clearSubDirectorySequencedData();
1785
+ directory.dispose();
1786
+ }
1787
+ emitDisposeForSubdirTree(directory) {
1788
+ if (directory === undefined || directory.disposed) {
1789
+ return;
1790
+ }
1791
+ // Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
1792
+ const subDirectories = directory.subdirectories();
1793
+ for (const [_, subDirectory] of subDirectories) {
1794
+ this.emitDisposeForSubdirTree(subDirectory);
1795
+ }
1858
1796
  if (typeof directory.dispose === "function") {
1859
- directory.dispose();
1797
+ directory.emit("disposed", directory);
1860
1798
  }
1861
1799
  }
1862
- undeleteSubDirectoryTree(directory) {
1863
- // Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
1864
- // This will unmark "deleted" from the subdirectories from top to bottom.
1800
+ undisposeSubdirectoryTree(directory) {
1801
+ // This will unmark "deleted" from the subdirectories from bottom to top.
1802
+ for (const [_, subDirectory] of directory.getSubdirectoriesEvenIfDisposed()) {
1803
+ this.undisposeSubdirectoryTree(subDirectory);
1804
+ }
1865
1805
  directory.undispose();
1866
- for (const [_, subDirectory] of directory.subdirectories()) {
1867
- this.undeleteSubDirectoryTree(subDirectory);
1806
+ }
1807
+ /**
1808
+ * Similar to {@link subdirectories}, but also includes subdirectories that are disposed.
1809
+ */
1810
+ getSubdirectoriesEvenIfDisposed() {
1811
+ const sequencedSubdirs = [];
1812
+ const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
1813
+ for (const subdirName of sequencedSubdirNames) {
1814
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
1815
+ if (optimisticSubdir !== undefined) {
1816
+ sequencedSubdirs.push([subdirName, optimisticSubdir]);
1817
+ }
1868
1818
  }
1819
+ const pendingSubdirNames = [
1820
+ ...new Set(this.pendingSubDirectoryData
1821
+ .map((entry) => entry.subdirName)
1822
+ .filter((subdirName) => !sequencedSubdirNames.has(subdirName))),
1823
+ ];
1824
+ const pendingSubdirs = [];
1825
+ for (const subdirName of pendingSubdirNames) {
1826
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
1827
+ if (optimisticSubdir !== undefined) {
1828
+ pendingSubdirs.push([subdirName, optimisticSubdir]);
1829
+ }
1830
+ }
1831
+ const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
1832
+ const orderedSubdirs = allSubdirs.sort((a, b) => {
1833
+ const aSeqData = a[1].seqData;
1834
+ const bSeqData = b[1].seqData;
1835
+ (0, internal_1.assert)(aSeqData !== undefined && bSeqData !== undefined, 0xc3a /* seqData should be defined */);
1836
+ return seqDataComparator(aSeqData, bSeqData);
1837
+ });
1838
+ return orderedSubdirs[Symbol.iterator]();
1839
+ }
1840
+ /**
1841
+ * Clears the sequenced data of a subdirectory but notably retains the pending
1842
+ * storage data. This is done when disposing of a directory so if we need to
1843
+ * re-create it, then we still have the pending ops.
1844
+ */
1845
+ clearSubDirectorySequencedData() {
1846
+ this.seqData.seq = -1;
1847
+ this.seqData.clientSeq = -1;
1848
+ this.sequencedStorageData.clear();
1849
+ this._sequencedSubdirectories.clear();
1850
+ this.clientIds.clear();
1851
+ this.clientIds.add(this.runtime.clientId ?? "detached");
1869
1852
  }
1870
1853
  }
1871
1854
  //# sourceMappingURL=directory.js.map