@fluidframework/map 2.53.0 → 2.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +448 -465
  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 +441 -458
  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 +18 -19
  46. package/src/directory.ts +565 -574
  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/lib/directory.js CHANGED
@@ -7,10 +7,9 @@ import { TypedEventEmitter } from "@fluid-internal/client-utils";
7
7
  import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
8
8
  import { MessageType, } from "@fluidframework/driver-definitions/internal";
9
9
  import { readAndParse } from "@fluidframework/driver-utils/internal";
10
- import { RedBlackTree } from "@fluidframework/merge-tree/internal";
11
10
  import { SummaryTreeBuilder } from "@fluidframework/runtime-utils/internal";
12
11
  import { SharedObject, ValueType, bindHandles, parseHandles, } from "@fluidframework/shared-object-base/internal";
13
- import { UsageError, } from "@fluidframework/telemetry-utils/internal";
12
+ import { createChildMonitoringContext, UsageError, } from "@fluidframework/telemetry-utils/internal";
14
13
  import path from "path-browserify";
15
14
  import { serializeValue, migrateIfSharedSerializable } from "./localValues.js";
16
15
  import { findLast, findLastIndex } from "./utils.js";
@@ -61,60 +60,6 @@ const seqDataComparator = (a, b) => {
61
60
  function isAcknowledgedOrDetached(seqData) {
62
61
  return seqData.seq >= 0;
63
62
  }
64
- /**
65
- * A utility class for tracking associations between keys and their creation indices.
66
- * This is relevant to support map iteration in insertion order, see
67
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
68
- *
69
- * TODO: It can be combined with the creation tracker utilized in SharedMap
70
- */
71
- class DirectoryCreationTracker {
72
- constructor() {
73
- this.indexToKey = new RedBlackTree(seqDataComparator);
74
- this.keyToIndex = new Map();
75
- }
76
- set(key, seqData) {
77
- this.indexToKey.put(seqData, key);
78
- this.keyToIndex.set(key, seqData);
79
- }
80
- has(keyOrSeqData) {
81
- return typeof keyOrSeqData === "string"
82
- ? this.keyToIndex.has(keyOrSeqData)
83
- : this.indexToKey.get(keyOrSeqData) !== undefined;
84
- }
85
- delete(keyOrSeqData) {
86
- if (this.has(keyOrSeqData)) {
87
- if (typeof keyOrSeqData === "string") {
88
- const seqData = this.keyToIndex.get(keyOrSeqData);
89
- this.keyToIndex.delete(keyOrSeqData);
90
- this.indexToKey.remove(seqData);
91
- }
92
- else {
93
- const key = this.indexToKey.get(keyOrSeqData)?.data;
94
- this.indexToKey.remove(keyOrSeqData);
95
- this.keyToIndex.delete(key);
96
- }
97
- }
98
- }
99
- /**
100
- * Retrieves all subdirectories with creation order that satisfy an optional constraint function.
101
- * @param constraint - An optional constraint function that filters keys.
102
- * @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
103
- */
104
- keys(constraint) {
105
- const keys = [];
106
- this.indexToKey.mapRange((node) => {
107
- if (!constraint || constraint(node.data)) {
108
- keys.push(node.data);
109
- }
110
- return true;
111
- }, keys);
112
- return keys;
113
- }
114
- get size() {
115
- return this.keyToIndex.size;
116
- }
117
- }
118
63
  /**
119
64
  * {@inheritDoc ISharedDirectory}
120
65
  *
@@ -315,6 +260,25 @@ export class SharedDirectory extends SharedObject {
315
260
  }
316
261
  return currentSubDir;
317
262
  }
263
+ /**
264
+ * Similar to `getWorkingDirectory`, but only returns directories that are sequenced.
265
+ * This can be useful for op processing since we only process ops on sequenced directories.
266
+ */
267
+ getSequencedWorkingDirectory(relativePath) {
268
+ const absolutePath = this.makeAbsolute(relativePath);
269
+ if (absolutePath === posix.sep) {
270
+ return this.root;
271
+ }
272
+ let currentSubDir = this.root;
273
+ const subdirs = absolutePath.slice(1).split(posix.sep);
274
+ for (const subdir of subdirs) {
275
+ currentSubDir = currentSubDir.sequencedSubdirectories.get(subdir);
276
+ if (!currentSubDir) {
277
+ return undefined;
278
+ }
279
+ }
280
+ return currentSubDir;
281
+ }
318
282
  /**
319
283
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
320
284
  */
@@ -407,10 +371,6 @@ export class SharedDirectory extends SharedObject {
407
371
  }
408
372
  newSubDir = new SubDirectory(seqData, createInfo === undefined ? new Set() : new Set(createInfo.ccIds), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName), this.logger);
409
373
  currentSubDir.populateSubDirectory(subdirName, newSubDir);
410
- // Record the newly inserted subdirectory to the creation tracker
411
- currentSubDir.ackedCreationSeqTracker.set(subdirName, {
412
- ...seqData,
413
- });
414
374
  }
415
375
  stack.push([newSubDir, subdirObject]);
416
376
  }
@@ -453,114 +413,87 @@ export class SharedDirectory extends SharedObject {
453
413
  makeAbsolute(relativePath) {
454
414
  return posix.resolve(posix.sep, relativePath);
455
415
  }
456
- /**
457
- * This checks if there is pending delete op for local delete for a any subdir in the relative path.
458
- * @param relativePath - path of sub directory.
459
- * @returns `true` if there is pending delete, `false` otherwise.
460
- */
461
- isSubDirectoryDeletePending(relativePath) {
462
- const absolutePath = this.makeAbsolute(relativePath);
463
- if (absolutePath === posix.sep) {
464
- return false;
465
- }
466
- let currentParent = this.root;
467
- const pathParts = absolutePath.split(posix.sep).slice(1);
468
- for (const dirName of pathParts) {
469
- if (currentParent.isSubDirectoryDeletePending(dirName)) {
470
- return true;
471
- }
472
- currentParent = currentParent.getSubDirectory(dirName);
473
- if (currentParent === undefined) {
474
- return true;
475
- }
476
- }
477
- return false;
478
- }
479
416
  /**
480
417
  * Set the message handlers for the directory.
481
418
  */
482
419
  setMessageHandlers() {
420
+ // Notes on how we target the correct subdirectory:
421
+ // `process`: When processing ops, we only ever want to process ops on sequenced directories. This prevents
422
+ // scenarios where ops could be processed on a pending directory instead of a sequenced directory,
423
+ // leading to ops effectively being processed out of order.
424
+ // `resubmit`: When resubmitting ops, we use `localOpMetadata` to get a reference to the subdirectory that
425
+ // the op was originally targeting.
483
426
  this.messageHandlers.set("clear", {
484
427
  process: (msg, op, local, localOpMetadata) => {
485
- const subdir = this.getWorkingDirectory(op.path);
486
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
487
- // as we are going to delete this subDirectory.
488
- if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
428
+ const subdir = this.getSequencedWorkingDirectory(op.path);
429
+ if (subdir !== undefined && !subdir?.disposed) {
489
430
  subdir.processClearMessage(msg, op, local, localOpMetadata);
490
431
  }
491
432
  },
492
433
  resubmit: (op, localOpMetadata) => {
493
- const subdir = this.getWorkingDirectory(op.path);
494
- if (subdir) {
495
- subdir.resubmitClearMessage(op, localOpMetadata);
434
+ const targetSubdir = localOpMetadata.subdir;
435
+ if (!targetSubdir.disposed) {
436
+ targetSubdir.resubmitClearMessage(op, localOpMetadata);
496
437
  }
497
438
  },
498
439
  });
499
440
  this.messageHandlers.set("delete", {
500
441
  process: (msg, op, local, localOpMetadata) => {
501
- const subdir = this.getWorkingDirectory(op.path);
502
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
503
- // as we are going to delete this subDirectory.
504
- if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
442
+ const subdir = this.getSequencedWorkingDirectory(op.path);
443
+ if (subdir !== undefined && !subdir?.disposed) {
505
444
  subdir.processDeleteMessage(msg, op, local, localOpMetadata);
506
445
  }
507
446
  },
508
447
  resubmit: (op, localOpMetadata) => {
509
- const subdir = this.getWorkingDirectory(op.path);
510
- if (subdir) {
511
- subdir.resubmitKeyMessage(op, localOpMetadata);
448
+ const targetSubdir = localOpMetadata.subdir;
449
+ if (!targetSubdir.disposed) {
450
+ targetSubdir.resubmitKeyMessage(op, localOpMetadata);
512
451
  }
513
452
  },
514
453
  });
515
454
  this.messageHandlers.set("set", {
516
455
  process: (msg, op, local, localOpMetadata) => {
517
- const subdir = this.getWorkingDirectory(op.path);
518
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
519
- // as we are going to delete this subDirectory.
520
- if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
456
+ const subdir = this.getSequencedWorkingDirectory(op.path);
457
+ if (subdir !== undefined && !subdir?.disposed) {
521
458
  migrateIfSharedSerializable(op.value, this.serializer, this.handle);
522
459
  const localValue = local ? undefined : op.value.value;
523
460
  subdir.processSetMessage(msg, op, localValue, local, localOpMetadata);
524
461
  }
525
462
  },
526
463
  resubmit: (op, localOpMetadata) => {
527
- const subdir = this.getWorkingDirectory(op.path);
528
- if (subdir) {
529
- subdir.resubmitKeyMessage(op, localOpMetadata);
464
+ const targetSubdir = localOpMetadata.subdir;
465
+ if (!targetSubdir.disposed) {
466
+ targetSubdir.resubmitKeyMessage(op, localOpMetadata);
530
467
  }
531
468
  },
532
469
  });
533
470
  this.messageHandlers.set("createSubDirectory", {
534
471
  process: (msg, op, local, localOpMetadata) => {
535
- const parentSubdir = this.getWorkingDirectory(op.path);
536
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
537
- // as we are going to delete this subDirectory.
538
- if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
472
+ const parentSubdir = this.getSequencedWorkingDirectory(op.path);
473
+ if (parentSubdir !== undefined && !parentSubdir?.disposed) {
539
474
  parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
540
475
  }
541
476
  },
542
477
  resubmit: (op, localOpMetadata) => {
543
- const parentSubdir = this.getWorkingDirectory(op.path);
544
- if (parentSubdir) {
478
+ const targetSubdir = localOpMetadata.parentSubdir;
479
+ if (!targetSubdir.disposed) {
545
480
  // We don't reuse the metadata but send a new one on each submit.
546
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
481
+ targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
547
482
  }
548
483
  },
549
484
  });
550
485
  this.messageHandlers.set("deleteSubDirectory", {
551
486
  process: (msg, op, local, localOpMetadata) => {
552
- const parentSubdir = this.getWorkingDirectory(op.path);
553
- // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
554
- // as we are going to delete this subDirectory.
555
- if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
487
+ const parentSubdir = this.getSequencedWorkingDirectory(op.path);
488
+ if (parentSubdir !== undefined && !parentSubdir?.disposed) {
556
489
  parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
557
490
  }
558
491
  },
559
492
  resubmit: (op, localOpMetadata) => {
560
- const parentSubdir = this.getWorkingDirectory(op.path);
561
- if (parentSubdir) {
493
+ const targetSubdir = localOpMetadata.parentSubdir;
494
+ if (!targetSubdir.disposed) {
562
495
  // We don't reuse the metadata but send a new one on each submit.
563
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
496
+ targetSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
564
497
  }
565
498
  },
566
499
  });
@@ -660,7 +593,6 @@ export class SharedDirectory extends SharedObject {
660
593
  function assertNonNullClientId(clientId) {
661
594
  assert(clientId !== null, 0x6af /* client id should never be null */);
662
595
  }
663
- let hasLoggedDirectoryInconsistency = false;
664
596
  /**
665
597
  * Node of the directory tree.
666
598
  * @sealed
@@ -683,7 +615,6 @@ class SubDirectory extends TypedEventEmitter {
683
615
  this.runtime = runtime;
684
616
  this.serializer = serializer;
685
617
  this.absolutePath = absolutePath;
686
- this.logger = logger;
687
618
  /**
688
619
  * Tells if the sub directory is deleted or not.
689
620
  */
@@ -693,21 +624,10 @@ class SubDirectory extends TypedEventEmitter {
693
624
  */
694
625
  this[_b] = "SubDirectory";
695
626
  /**
696
- * The subdirectories the directory is holding.
627
+ * The sequenced subdirectories the directory is holding independent of any pending
628
+ * create/delete subdirectory operations.
697
629
  */
698
- this._subdirectories = new Map();
699
- /**
700
- * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
701
- * of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
702
- * deleted sub directory.
703
- */
704
- this.pendingDeleteSubDirectoriesTracker = new Map();
705
- /**
706
- * Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
707
- * of create op that are pending or yet to be acked from server. This is maintained just to track the locally
708
- * created sub directory.
709
- */
710
- this.pendingCreateSubDirectoriesTracker = new Map();
630
+ this._sequencedSubdirectories = new Map();
711
631
  /**
712
632
  * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
713
633
  * of the creation order.
@@ -726,6 +646,11 @@ class SubDirectory extends TypedEventEmitter {
726
646
  * even across remote operations and rollbacks.
727
647
  */
728
648
  this.pendingStorageData = [];
649
+ /**
650
+ * A data structure containing all local pending subdirectory create/deletes, which is used in combination
651
+ * with the _sequencedSubdirectories to compute optimistic values.
652
+ */
653
+ this.pendingSubDirectoryData = [];
729
654
  /**
730
655
  * An internal iterator that iterates over the entries in the directory.
731
656
  */
@@ -814,8 +739,33 @@ class SubDirectory extends TypedEventEmitter {
814
739
  ? this.sequencedStorageData.has(key)
815
740
  : latestPendingEntry.type === "lifetime";
816
741
  };
817
- this.localCreationSeqTracker = new DirectoryCreationTracker();
818
- this.ackedCreationSeqTracker = new DirectoryCreationTracker();
742
+ /**
743
+ * Get the optimistic local subdirectory. This combines the sequenced data with
744
+ * any pending changes that have not yet been sequenced. By default, we do not
745
+ * consider disposed directories as optimistically existing, but if `getIfDisposed`
746
+ * is true, we will include them since some scenarios require this.
747
+ */
748
+ this.getOptimisticSubDirectory = (subdirName, getIfDisposed = false) => {
749
+ const latestPendingEntry = findLast(this.pendingSubDirectoryData, (entry) => entry.subdirName === subdirName);
750
+ let subdir;
751
+ if (latestPendingEntry === undefined) {
752
+ subdir = this._sequencedSubdirectories.get(subdirName);
753
+ }
754
+ else if (latestPendingEntry.type === "createSubDirectory") {
755
+ subdir = latestPendingEntry.subdir;
756
+ assert(subdir !== undefined, 0xc2f /* Subdirectory should exist in pending data */);
757
+ }
758
+ else {
759
+ // Pending delete
760
+ return undefined;
761
+ }
762
+ // If the subdirectory is disposed, treat it as non-existent for optimistic reads (unless specified otherwise)
763
+ if (subdir?.disposed && !getIfDisposed) {
764
+ return undefined;
765
+ }
766
+ return subdir;
767
+ };
768
+ this.mc = createChildMonitoringContext({ logger, namespace: "Directory" });
819
769
  }
820
770
  dispose(error) {
821
771
  this._deleted = true;
@@ -861,8 +811,12 @@ class SubDirectory extends TypedEventEmitter {
861
811
  throw new Error("Undefined and null keys are not supported");
862
812
  }
863
813
  const previousOptimisticLocalValue = this.getOptimisticValue(key);
864
- // Create a local value and serialize it.
865
- bindHandles(value, this.serializer, this.directory.handle);
814
+ const detachedBind = this.mc.config.getBoolean("Fluid.Directory.AllowDetachedResolve") ?? false;
815
+ if (detachedBind) {
816
+ // Create a local value and serialize it.
817
+ // AB#47081: This will be removed once we can validate that it is no longer needed.
818
+ bindHandles(value, this.serializer, this.directory.handle);
819
+ }
866
820
  // If we are not attached, don't submit the op.
867
821
  if (!this.directory.isAttached()) {
868
822
  this.sequencedStorageData.set(key, value);
@@ -887,13 +841,20 @@ class SubDirectory extends TypedEventEmitter {
887
841
  if (latestPendingEntry === undefined ||
888
842
  latestPendingEntry.type === "delete" ||
889
843
  latestPendingEntry.type === "clear") {
890
- latestPendingEntry = { type: "lifetime", path: this.absolutePath, key, keySets: [] };
844
+ latestPendingEntry = {
845
+ type: "lifetime",
846
+ path: this.absolutePath,
847
+ key,
848
+ keySets: [],
849
+ subdir: this,
850
+ };
891
851
  this.pendingStorageData.push(latestPendingEntry);
892
852
  }
893
853
  const pendingKeySet = {
894
854
  type: "set",
895
855
  path: this.absolutePath,
896
856
  value,
857
+ subdir: this,
897
858
  };
898
859
  latestPendingEntry.keySets.push(pendingKeySet);
899
860
  const op = {
@@ -920,7 +881,7 @@ class SubDirectory extends TypedEventEmitter {
920
881
  * {@inheritDoc IDirectory.countSubDirectory}
921
882
  */
922
883
  countSubDirectory() {
923
- return this._subdirectories.size;
884
+ return [...this.subdirectories()].length;
924
885
  }
925
886
  /**
926
887
  * {@inheritDoc IDirectory.createSubDirectory}
@@ -934,22 +895,47 @@ class SubDirectory extends TypedEventEmitter {
934
895
  if (subdirName.includes(posix.sep)) {
935
896
  throw new Error(`SubDirectory name may not contain ${posix.sep}`);
936
897
  }
937
- // Create the sub directory locally first.
938
- const isNew = this.createSubDirectoryCore(subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
939
- const subDir = this._subdirectories.get(subdirName);
940
- assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
941
- // If we are not attached, don't submit the op.
942
- if (!this.directory.isAttached()) {
943
- return subDir;
898
+ let subDir = this.getOptimisticSubDirectory(subdirName, true);
899
+ const seqData = this.getLocalSeq();
900
+ const clientId = this.runtime.clientId ?? "detached";
901
+ const isNewSubDirectory = subDir === undefined;
902
+ if (subDir === undefined) {
903
+ // If we do not have optimistically have this subdirectory yet, we should create a new one
904
+ const absolutePath = posix.join(this.absolutePath, subdirName);
905
+ subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.mc.logger);
944
906
  }
945
- // Only submit the op, if it is newly created.
946
- if (isNew) {
947
- const op = {
948
- path: this.absolutePath,
949
- subdirName,
950
- type: "createSubDirectory",
951
- };
952
- this.submitCreateSubDirectoryMessage(op);
907
+ else {
908
+ if (subDir.disposed) {
909
+ // In the case that the subdir exists but is disposed, we should
910
+ // still use the existing subdir to maintain any pending changes but
911
+ // ensure it is no longer disposed.
912
+ this.undisposeSubdirectoryTree(subDir);
913
+ }
914
+ subDir.clientIds.add(clientId);
915
+ }
916
+ this.registerEventsOnSubDirectory(subDir, subdirName);
917
+ // Only submit the op/emit event if we actually created a new subdir.
918
+ if (isNewSubDirectory) {
919
+ if (this.directory.isAttached()) {
920
+ const pendingSubDirectoryCreate = {
921
+ type: "createSubDirectory",
922
+ subdirName,
923
+ subdir: subDir,
924
+ };
925
+ this.pendingSubDirectoryData.push(pendingSubDirectoryCreate);
926
+ const op = {
927
+ subdirName,
928
+ path: this.absolutePath,
929
+ type: "createSubDirectory",
930
+ };
931
+ this.submitCreateSubDirectoryMessage(op);
932
+ }
933
+ else {
934
+ // If we are detached, don't submit the op and directly commit
935
+ // the subdir to _sequencedSubdirectories.
936
+ this._sequencedSubdirectories.set(subdirName, subDir);
937
+ }
938
+ this.emit("subDirectoryCreated", subdirName, true, this);
953
939
  }
954
940
  return subDir;
955
941
  }
@@ -974,81 +960,88 @@ class SubDirectory extends TypedEventEmitter {
974
960
  */
975
961
  getSubDirectory(subdirName) {
976
962
  this.throwIfDisposed();
977
- return this._subdirectories.get(subdirName);
963
+ return this.getOptimisticSubDirectory(subdirName);
978
964
  }
979
965
  /**
980
966
  * {@inheritDoc IDirectory.hasSubDirectory}
981
967
  */
982
968
  hasSubDirectory(subdirName) {
983
969
  this.throwIfDisposed();
984
- return this._subdirectories.has(subdirName);
970
+ return this.getOptimisticSubDirectory(subdirName) !== undefined;
985
971
  }
986
972
  /**
987
973
  * {@inheritDoc IDirectory.deleteSubDirectory}
988
974
  */
989
975
  deleteSubDirectory(subdirName) {
990
976
  this.throwIfDisposed();
991
- // Delete the sub directory locally first.
992
- const subDir = this.deleteSubDirectoryCore(subdirName, true);
993
- // If we are not attached, don't submit the op.
994
977
  if (!this.directory.isAttached()) {
995
- return subDir !== undefined;
978
+ const previousValue = this._sequencedSubdirectories.get(subdirName);
979
+ const successfullyRemoved = this._sequencedSubdirectories.delete(subdirName);
980
+ // Only emit if we actually deleted something.
981
+ if (successfullyRemoved) {
982
+ this.disposeSubDirectoryTree(previousValue);
983
+ this.emit("subDirectoryDeleted", subdirName, true, this);
984
+ }
985
+ return successfullyRemoved;
996
986
  }
997
- // Only submit the op, if the directory existed and we deleted it.
998
- if (subDir !== undefined) {
999
- const op = {
1000
- path: this.absolutePath,
1001
- subdirName,
1002
- type: "deleteSubDirectory",
1003
- };
1004
- this.submitDeleteSubDirectoryMessage(op, subDir);
987
+ const previousOptimisticSubDirectory = this.getOptimisticSubDirectory(subdirName);
988
+ if (previousOptimisticSubDirectory === undefined) {
989
+ return false;
1005
990
  }
1006
- return subDir !== undefined;
991
+ const pendingSubdirDelete = {
992
+ type: "deleteSubDirectory",
993
+ subdirName,
994
+ subdir: this,
995
+ };
996
+ this.pendingSubDirectoryData.push(pendingSubdirDelete);
997
+ const op = {
998
+ subdirName,
999
+ type: "deleteSubDirectory",
1000
+ path: this.absolutePath,
1001
+ };
1002
+ this.submitDeleteSubDirectoryMessage(op, previousOptimisticSubDirectory);
1003
+ this.emit("subDirectoryDeleted", subdirName, true, this);
1004
+ // We don't want to fully dispose the subdir tree since this is only a pending
1005
+ // local delete. Instead we will only emit the dispose event to reflect the
1006
+ // local state.
1007
+ this.emitDisposeForSubdirTree(previousOptimisticSubDirectory);
1008
+ return true;
1007
1009
  }
1008
1010
  /**
1009
1011
  * {@inheritDoc IDirectory.subdirectories}
1010
1012
  */
1011
1013
  subdirectories() {
1012
1014
  this.throwIfDisposed();
1013
- const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
1014
- const localSubdirsInOrder = this.localCreationSeqTracker.keys((key) => !this.ackedCreationSeqTracker.has(key));
1015
- const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1016
- if (subdirNames.length !== this._subdirectories.size) {
1017
- // TODO: AB#7022: Hitting this block indicates that the eventual consistency scheme for ordering subdirectories
1018
- // has failed. Fall back to previous directory behavior, which didn't guarantee ordering.
1019
- // It's not currently clear how to reach this state, so log some diagnostics to help understand the issue.
1020
- // This whole block should eventually be replaced by an assert that the two sizes align.
1021
- if (!hasLoggedDirectoryInconsistency) {
1022
- this.logger.sendTelemetryEvent({
1023
- eventName: "inconsistentSubdirectoryOrdering",
1024
- localKeyCount: this.localCreationSeqTracker.size,
1025
- ackedKeyCount: this.ackedCreationSeqTracker.size,
1026
- subdirNamesLength: subdirNames.length,
1027
- subdirectoriesSize: this._subdirectories.size,
1028
- });
1029
- hasLoggedDirectoryInconsistency = true;
1015
+ // subdirectories() should reflect the optimistic state of subdirectories.
1016
+ // This means that we should return both sequenced and pending subdirectories
1017
+ // that do not also have a pending deletion.
1018
+ const sequencedSubdirs = [];
1019
+ const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
1020
+ for (const subdirName of sequencedSubdirNames) {
1021
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
1022
+ if (optimisticSubdir !== undefined) {
1023
+ sequencedSubdirs.push([subdirName, optimisticSubdir]);
1030
1024
  }
1031
- return this._subdirectories.entries();
1032
- }
1033
- const entriesIterator = {
1034
- index: 0,
1035
- dirs: this._subdirectories,
1036
- next() {
1037
- if (this.index < subdirNames.length) {
1038
- // Bounds check above guarantees non-null (at least at compile time, assuming all types are respected)
1039
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1040
- const subdirName = subdirNames[this.index++];
1041
- const subdir = this.dirs.get(subdirName);
1042
- assert(subdir !== undefined, 0x8ac /* Could not find expected sub-directory. */);
1043
- return { value: [subdirName, subdir], done: false };
1044
- }
1045
- return { value: undefined, done: true };
1046
- },
1047
- [Symbol.iterator]() {
1048
- return this;
1049
- },
1050
- };
1051
- return entriesIterator;
1025
+ }
1026
+ const pendingSubdirNames = [
1027
+ ...new Set(this.pendingSubDirectoryData
1028
+ .map((entry) => entry.subdirName)
1029
+ .filter((subdirName) => !sequencedSubdirNames.has(subdirName))),
1030
+ ];
1031
+ const pendingSubdirs = [];
1032
+ for (const subdirName of pendingSubdirNames) {
1033
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName);
1034
+ if (optimisticSubdir !== undefined) {
1035
+ pendingSubdirs.push([subdirName, optimisticSubdir]);
1036
+ }
1037
+ }
1038
+ const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
1039
+ const orderedSubdirs = allSubdirs.sort((a, b) => {
1040
+ const aSeqData = a[1].seqData;
1041
+ const bSeqData = b[1].seqData;
1042
+ return seqDataComparator(aSeqData, bSeqData);
1043
+ });
1044
+ return orderedSubdirs[Symbol.iterator]();
1052
1045
  }
1053
1046
  /**
1054
1047
  * {@inheritDoc IDirectory.getWorkingDirectory}
@@ -1063,10 +1056,10 @@ class SubDirectory extends TypedEventEmitter {
1063
1056
  * @returns true if there is pending delete.
1064
1057
  */
1065
1058
  isSubDirectoryDeletePending(subDirName) {
1066
- if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
1067
- return true;
1068
- }
1069
- return false;
1059
+ const lastPendingEntry = findLast(this.pendingSubDirectoryData, (entry) => {
1060
+ return entry.subdirName === subDirName && entry.type === "deleteSubDirectory";
1061
+ });
1062
+ return lastPendingEntry !== undefined;
1070
1063
  }
1071
1064
  /**
1072
1065
  * Deletes the given key from within this IDirectory.
@@ -1098,6 +1091,7 @@ class SubDirectory extends TypedEventEmitter {
1098
1091
  type: "delete",
1099
1092
  path: this.absolutePath,
1100
1093
  key,
1094
+ subdir: this,
1101
1095
  };
1102
1096
  this.pendingStorageData.push(pendingKeyDelete);
1103
1097
  const op = {
@@ -1137,6 +1131,7 @@ class SubDirectory extends TypedEventEmitter {
1137
1131
  const pendingClear = {
1138
1132
  type: "clear",
1139
1133
  path: this.absolutePath,
1134
+ subdir: this,
1140
1135
  };
1141
1136
  this.pendingStorageData.push(pendingClear);
1142
1137
  this.directory.emit("clear", true, this.directory);
@@ -1153,7 +1148,7 @@ class SubDirectory extends TypedEventEmitter {
1153
1148
  forEach(callback) {
1154
1149
  this.throwIfDisposed();
1155
1150
  for (const [key, localValue] of this.internalIterator()) {
1156
- callback(localValue.value, key, this);
1151
+ callback(localValue, key, this);
1157
1152
  }
1158
1153
  }
1159
1154
  /**
@@ -1241,6 +1236,10 @@ class SubDirectory extends TypedEventEmitter {
1241
1236
  this.throwIfDisposed();
1242
1237
  return this.internalIterator();
1243
1238
  }
1239
+ get sequencedSubdirectories() {
1240
+ this.throwIfDisposed();
1241
+ return this._sequencedSubdirectories;
1242
+ }
1244
1243
  /**
1245
1244
  * Process a clear operation.
1246
1245
  * @param msg - The message from the server to apply.
@@ -1251,7 +1250,7 @@ class SubDirectory extends TypedEventEmitter {
1251
1250
  */
1252
1251
  processClearMessage(msg, op, local, localOpMetadata) {
1253
1252
  this.throwIfDisposed();
1254
- if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1253
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
1255
1254
  return;
1256
1255
  }
1257
1256
  if (local) {
@@ -1295,7 +1294,7 @@ class SubDirectory extends TypedEventEmitter {
1295
1294
  */
1296
1295
  processDeleteMessage(msg, op, local, localOpMetadata) {
1297
1296
  this.throwIfDisposed();
1298
- if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1297
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
1299
1298
  return;
1300
1299
  }
1301
1300
  if (local) {
@@ -1333,7 +1332,7 @@ class SubDirectory extends TypedEventEmitter {
1333
1332
  */
1334
1333
  processSetMessage(msg, op, value, local, localOpMetadata) {
1335
1334
  this.throwIfDisposed();
1336
- if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1335
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.subdir)) {
1337
1336
  return;
1338
1337
  }
1339
1338
  const { key } = op;
@@ -1371,12 +1370,58 @@ class SubDirectory extends TypedEventEmitter {
1371
1370
  */
1372
1371
  processCreateSubDirectoryMessage(msg, op, local, localOpMetadata) {
1373
1372
  this.throwIfDisposed();
1374
- if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1375
- this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
1373
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
1376
1374
  return;
1377
1375
  }
1378
1376
  assertNonNullClientId(msg.clientId);
1379
- this.createSubDirectoryCore(op.subdirName, local, { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, msg.clientId);
1377
+ let subDir;
1378
+ if (local) {
1379
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
1380
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1381
+ assert(pendingEntry !== undefined && pendingEntry.type === "createSubDirectory", 0xc30 /* Got a local subdir create message we weren't expecting */);
1382
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1383
+ subDir = pendingEntry.subdir;
1384
+ const existingSubdir = this._sequencedSubdirectories.get(op.subdirName);
1385
+ if (existingSubdir !== undefined) {
1386
+ // If the subdirectory already exists, we don't need to create it again.
1387
+ // This can happen if remote clients also create the same subdir and we processed
1388
+ // that message first.
1389
+ return;
1390
+ }
1391
+ if (subDir.disposed) {
1392
+ this.undisposeSubdirectoryTree(subDir);
1393
+ }
1394
+ this._sequencedSubdirectories.set(op.subdirName, subDir);
1395
+ }
1396
+ else {
1397
+ subDir = this.getOptimisticSubDirectory(op.subdirName, true);
1398
+ if (subDir === undefined) {
1399
+ const absolutePath = posix.join(this.absolutePath, op.subdirName);
1400
+ subDir = new SubDirectory({ seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, new Set([msg.clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.mc.logger);
1401
+ }
1402
+ else {
1403
+ // If the subdirectory already optimistically exists, we don't need to create it again.
1404
+ // This can happen if remote clients also created the same subdir.
1405
+ if (subDir.disposed) {
1406
+ this.undisposeSubdirectoryTree(subDir);
1407
+ }
1408
+ subDir.clientIds.add(msg.clientId);
1409
+ }
1410
+ this.registerEventsOnSubDirectory(subDir, op.subdirName);
1411
+ this._sequencedSubdirectories.set(op.subdirName, subDir);
1412
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
1413
+ if (!this.pendingSubDirectoryData.some((entry) => entry.subdirName === op.subdirName)) {
1414
+ this.emit("subDirectoryCreated", op.subdirName, local, this);
1415
+ }
1416
+ }
1417
+ // Ensure correct seqData. This can be necessary if in scenarios where a subdir was created, deleted, and
1418
+ // then later recreated.
1419
+ if (this.seqData.seq !== -1 &&
1420
+ this.seqData.seq <= msg.sequenceNumber &&
1421
+ subDir.seqData.seq === -1) {
1422
+ subDir.seqData.seq = msg.sequenceNumber;
1423
+ subDir.seqData.clientSeq = msg.clientSequenceNumber;
1424
+ }
1380
1425
  }
1381
1426
  /**
1382
1427
  * Process a delete subdirectory operation.
@@ -1388,11 +1433,43 @@ class SubDirectory extends TypedEventEmitter {
1388
1433
  */
1389
1434
  processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata) {
1390
1435
  this.throwIfDisposed();
1391
- if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1392
- this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
1436
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg, localOpMetadata?.parentSubdir)) {
1393
1437
  return;
1394
1438
  }
1395
- this.deleteSubDirectoryCore(op.subdirName, local);
1439
+ const previousValue = this._sequencedSubdirectories.get(op.subdirName);
1440
+ if (previousValue === undefined) {
1441
+ // We are trying to delete a subdirectory that does not exist.
1442
+ // If this is a local delete, we should remove the pending delete entry.
1443
+ // This could happen if we already processed a remote delete op for
1444
+ // the same subdirectory.
1445
+ if (local) {
1446
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
1447
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1448
+ assert(pendingEntry !== undefined &&
1449
+ pendingEntry.type === "deleteSubDirectory" &&
1450
+ pendingEntry.subdirName === op.subdirName, 0xc31 /* Got a local deleteSubDirectory message we weren't expecting */);
1451
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1452
+ }
1453
+ return;
1454
+ }
1455
+ this._sequencedSubdirectories.delete(op.subdirName);
1456
+ this.disposeSubDirectoryTree(previousValue);
1457
+ if (local) {
1458
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName);
1459
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1460
+ assert(pendingEntry !== undefined &&
1461
+ pendingEntry.type === "deleteSubDirectory" &&
1462
+ pendingEntry.subdirName === op.subdirName, 0xc32 /* Got a local deleteSubDirectory message we weren't expecting */);
1463
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1464
+ }
1465
+ else {
1466
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
1467
+ const pendingEntryIndex = this.pendingSubDirectoryData.findIndex((entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory");
1468
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1469
+ if (pendingEntry === undefined) {
1470
+ this.emit("subDirectoryDeleted", op.subdirName, local, this);
1471
+ }
1472
+ }
1396
1473
  }
1397
1474
  /**
1398
1475
  * Submit a clear operation.
@@ -1433,44 +1510,28 @@ class SubDirectory extends TypedEventEmitter {
1433
1510
  resubmitKeyMessage(op, localOpMetadata) {
1434
1511
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1435
1512
  // is already deleted, in which case we don't need to submit the op.
1436
- const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === op.key);
1513
+ const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => {
1514
+ return op.type === "set"
1515
+ ? entry.type === "lifetime" &&
1516
+ entry.key === op.key &&
1517
+ // We also check that the keySets include the localOpMetadata. It's possible we have new
1518
+ // pending key sets that are not the op we are looking for.
1519
+ entry.keySets.includes(localOpMetadata)
1520
+ : entry.type === "delete" && entry.key === op.key;
1521
+ });
1437
1522
  const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1438
1523
  if (pendingEntry !== undefined) {
1439
1524
  this.submitKeyMessage(op, localOpMetadata);
1440
1525
  }
1441
1526
  }
1442
- incrementPendingSubDirCount(map, subDirName) {
1443
- const count = map.get(subDirName) ?? 0;
1444
- map.set(subDirName, count + 1);
1445
- }
1446
- decrementPendingSubDirCount(map, subDirName) {
1447
- const count = map.get(subDirName) ?? 0;
1448
- map.set(subDirName, count - 1);
1449
- if (count <= 1) {
1450
- map.delete(subDirName);
1451
- }
1452
- }
1453
- /**
1454
- * Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
1455
- * or while resubmitting the op.
1456
- */
1457
- updatePendingSubDirMessageCount(op) {
1458
- if (op.type === "deleteSubDirectory") {
1459
- this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1460
- }
1461
- else if (op.type === "createSubDirectory") {
1462
- this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1463
- }
1464
- }
1465
1527
  /**
1466
1528
  * Submit a create subdirectory operation.
1467
1529
  * @param op - The operation
1468
1530
  */
1469
1531
  submitCreateSubDirectoryMessage(op) {
1470
- this.throwIfDisposed();
1471
- this.updatePendingSubDirMessageCount(op);
1472
1532
  const localOpMetadata = {
1473
1533
  type: "createSubDir",
1534
+ parentSubdir: this,
1474
1535
  };
1475
1536
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1476
1537
  }
@@ -1480,11 +1541,10 @@ class SubDirectory extends TypedEventEmitter {
1480
1541
  * @param subDir - Any subdirectory deleted by the op
1481
1542
  */
1482
1543
  submitDeleteSubDirectoryMessage(op, subDir) {
1483
- this.throwIfDisposed();
1484
- this.updatePendingSubDirMessageCount(op);
1485
1544
  const localOpMetadata = {
1486
1545
  type: "deleteSubDir",
1487
1546
  subDirectory: subDir,
1547
+ parentSubdir: this,
1488
1548
  };
1489
1549
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1490
1550
  }
@@ -1496,22 +1556,25 @@ class SubDirectory extends TypedEventEmitter {
1496
1556
  resubmitSubDirectoryMessage(op, localOpMetadata) {
1497
1557
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1498
1558
  // is already deleted, in which case we don't need to submit the op.
1499
- if (localOpMetadata.type === "createSubDir" &&
1500
- !this.pendingCreateSubDirectoriesTracker.has(op.subdirName)) {
1501
- return;
1502
- }
1503
- else if (localOpMetadata.type === "deleteSubDir" &&
1504
- !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1505
- return;
1506
- }
1507
1559
  if (localOpMetadata.type === "createSubDir") {
1508
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1509
- this.submitCreateSubDirectoryMessage(op);
1560
+ // For create operations, look specifically for createSubDirectory entries
1561
+ const pendingEntry = findLast(this.pendingSubDirectoryData, (entry) => entry.subdirName === op.subdirName && entry.type === "createSubDirectory");
1562
+ if (pendingEntry !== undefined) {
1563
+ assert(pendingEntry.type === "createSubDirectory", 0xc33 /* pending entry should be createSubDirectory */);
1564
+ // We should add the client id, since when reconnecting it can have a different client id.
1565
+ pendingEntry.subdir.clientIds.add(this.runtime.clientId ?? "detached");
1566
+ // We also need to undelete the subdirectory tree if it was previously deleted
1567
+ this.undisposeSubdirectoryTree(pendingEntry.subdir);
1568
+ this.submitCreateSubDirectoryMessage(op);
1569
+ }
1510
1570
  }
1511
1571
  else if (localOpMetadata.type === "deleteSubDir") {
1512
- this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1513
- assert(localOpMetadata.subDirectory !== undefined, 0xc08 /* localOpMetadata.subDirectory should be defined */);
1514
- this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1572
+ assert(localOpMetadata.subDirectory !== undefined, 0xc34 /* Subdirectory should exist */);
1573
+ // For delete operations, look specifically for deleteSubDirectory entries
1574
+ const pendingEntry = findLast(this.pendingSubDirectoryData, (entry) => entry.subdirName === op.subdirName && entry.type === "deleteSubDirectory");
1575
+ if (pendingEntry !== undefined) {
1576
+ this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1577
+ }
1515
1578
  }
1516
1579
  }
1517
1580
  /**
@@ -1552,7 +1615,7 @@ class SubDirectory extends TypedEventEmitter {
1552
1615
  populateSubDirectory(subdirName, newSubDir) {
1553
1616
  this.throwIfDisposed();
1554
1617
  this.registerEventsOnSubDirectory(newSubDir, subdirName);
1555
- this._subdirectories.set(subdirName, newSubDir);
1618
+ this._sequencedSubdirectories.set(subdirName, newSubDir);
1556
1619
  }
1557
1620
  /**
1558
1621
  * Rollback a local op
@@ -1565,9 +1628,13 @@ class SubDirectory extends TypedEventEmitter {
1565
1628
  if (directoryOp.type === "clear") {
1566
1629
  // A pending clear will be last in the list, since it terminates all prior lifetimes.
1567
1630
  const pendingClear = this.pendingStorageData.pop();
1568
- assert(pendingClear !== undefined &&
1569
- pendingClear.type === "clear" &&
1570
- localOpMetadata.type === "clear", 0xc09 /* Unexpected clear rollback */);
1631
+ if (pendingClear === undefined) {
1632
+ // If we can't find a pending entry then it's possible that we deleted an ack'd subdir
1633
+ // from a remote delete subdir op. If that's the case then there is nothing to rollback
1634
+ // since the pending data was removed with the subdirectory deletion.
1635
+ return;
1636
+ }
1637
+ assert(pendingClear.type === "clear" && localOpMetadata.type === "clear", 0xc35 /* Unexpected clear rollback */);
1571
1638
  for (const [key] of this.internalIterator()) {
1572
1639
  const event = {
1573
1640
  key,
@@ -1585,8 +1652,13 @@ class SubDirectory extends TypedEventEmitter {
1585
1652
  // they were created, not when they were last modified.
1586
1653
  const pendingEntryIndex = findLastIndex(this.pendingStorageData, (entry) => entry.type !== "clear" && entry.key === directoryOp.key);
1587
1654
  const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1588
- assert(pendingEntry !== undefined &&
1589
- (pendingEntry.type === "delete" || pendingEntry.type === "lifetime"), 0xc0a /* Unexpected pending data for set/delete op */);
1655
+ if (pendingEntry === undefined) {
1656
+ // If we can't find a pending entry then it's possible that we deleted an ack'd subdir
1657
+ // from a remote delete subdir op. If that's the case then there is nothing to rollback
1658
+ // since the pending data was removed with the subdirectory deletion.
1659
+ return;
1660
+ }
1661
+ assert(pendingEntry.type === "delete" || pendingEntry.type === "lifetime", 0xc36 /* Unexpected pending data for set/delete op */);
1590
1662
  if (pendingEntry.type === "delete") {
1591
1663
  assert(pendingEntry === localOpMetadata, 0xc0b /* Unexpected delete rollback */);
1592
1664
  this.pendingStorageData.splice(pendingEntryIndex, 1);
@@ -1627,34 +1699,33 @@ class SubDirectory extends TypedEventEmitter {
1627
1699
  else if (directoryOp.type === "createSubDirectory" &&
1628
1700
  localOpMetadata.type === "createSubDir") {
1629
1701
  const subdirName = directoryOp.subdirName;
1630
- assert(subdirName !== undefined, 0x8af /* "subdirName" property is missing from "createSubDirectory" operation. */);
1631
- assert(typeof subdirName === "string", 0x8b0 /* "subdirName" property in "createSubDirectory" operation is misconfigured. Expected a string. */);
1632
- this.deleteSubDirectoryCore(subdirName, true);
1633
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, subdirName);
1702
+ const pendingEntryIndex = findLastIndex(this.pendingSubDirectoryData, (entry) => entry.type === "createSubDirectory" && entry.subdirName === subdirName);
1703
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1704
+ assert(pendingEntry !== undefined && pendingEntry.type === "createSubDirectory", 0xc37 /* Unexpected pending data for createSubDirectory op */);
1705
+ // We still need to emit the disposed event for any locally created (and now
1706
+ // rolled back) subdirectory trees so listeners can observer the lifecycle
1707
+ // changes properly. We don't want to fully delete in case there is another
1708
+ // operation that references the same subdirectory.
1709
+ this.emitDisposeForSubdirTree(pendingEntry.subdir);
1710
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1711
+ this.emit("subDirectoryDeleted", subdirName, true, this);
1634
1712
  }
1635
1713
  else if (directoryOp.type === "deleteSubDirectory" &&
1636
1714
  localOpMetadata.type === "deleteSubDir") {
1637
1715
  const subdirName = directoryOp.subdirName;
1638
- assert(subdirName !== undefined, 0x8b1 /* "subdirName" property is missing from "deleteSubDirectory" operation. */);
1639
- assert(typeof subdirName === "string", 0x8b2 /* "subdirName" property in "deleteSubDirectory" operation is misconfigured. Expected a string. */);
1640
- if (localOpMetadata.subDirectory !== undefined) {
1641
- this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
1642
- // don't need to register events because deleting never unregistered
1643
- this._subdirectories.set(subdirName, localOpMetadata.subDirectory);
1644
- // Restore the record in creation tracker
1645
- if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
1646
- this.ackedCreationSeqTracker.set(subdirName, {
1647
- ...localOpMetadata.subDirectory.seqData,
1648
- });
1649
- }
1650
- else {
1651
- this.localCreationSeqTracker.set(subdirName, {
1652
- ...localOpMetadata.subDirectory.seqData,
1653
- });
1654
- }
1655
- this.emit("subDirectoryCreated", subdirName, true, this);
1656
- }
1657
- this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, subdirName);
1716
+ const pendingEntryIndex = findLastIndex(this.pendingSubDirectoryData, (entry) => entry.type === "deleteSubDirectory" && entry.subdirName === subdirName);
1717
+ const pendingEntry = this.pendingSubDirectoryData[pendingEntryIndex];
1718
+ assert(pendingEntry !== undefined && pendingEntry.type === "deleteSubDirectory", 0xc38 /* Unexpected pending data for deleteSubDirectory op */);
1719
+ this.pendingSubDirectoryData.splice(pendingEntryIndex, 1);
1720
+ // Restore the subdirectory
1721
+ const subDirectoryToRestore = localOpMetadata.subDirectory;
1722
+ assert(subDirectoryToRestore !== undefined, 0xc39 /* Subdirectory should exist */);
1723
+ // Recursively undispose all nested subdirectories before adding to the map
1724
+ // This ensures the subdirectory is properly restored before being exposed
1725
+ this.undisposeSubdirectoryTree(subDirectoryToRestore);
1726
+ // Re-register events
1727
+ this.registerEventsOnSubDirectory(subDirectoryToRestore, subdirName);
1728
+ this.emit("subDirectoryCreated", subdirName, true, this);
1658
1729
  }
1659
1730
  else {
1660
1731
  throw new Error("Unsupported op for rollback");
@@ -1672,138 +1743,17 @@ class SubDirectory extends TypedEventEmitter {
1672
1743
  * This return true if the message is for the current instance of this sub directory. As the sub directory
1673
1744
  * can be deleted and created again, then this finds if the message is for current instance of directory or not.
1674
1745
  * @param msg - message for the directory
1675
- */
1676
- isMessageForCurrentInstanceOfSubDirectory(msg) {
1677
- // If the message is either from the creator of directory or this directory was created when
1678
- // container was detached or in case this directory is already live(known to other clients)
1679
- // and the op was created after the directory was created then apply this op.
1680
- return ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
1681
- this.clientIds.has("detached") ||
1682
- (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber));
1683
- }
1684
- /**
1685
- * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1686
- * not process the incoming operation.
1687
- * @param op - Operation to check
1688
- * @param local - Whether the message originated from the local client
1689
- * @param message - The message
1690
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1691
- * For messages from a remote client, this will be undefined.
1692
- * @returns True if the operation should be processed, false otherwise
1693
- */
1694
- needProcessSubDirectoryOperation(msg, op, local, localOpMetadata) {
1695
- assertNonNullClientId(msg.clientId);
1696
- const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
1697
- const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
1698
- if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
1699
- (pendingCreateCount !== undefined && pendingCreateCount > 0)) {
1700
- if (local) {
1701
- assert(localOpMetadata !== undefined, 0xc0d /* localOpMetadata should be defined */);
1702
- if (localOpMetadata.type === "deleteSubDir") {
1703
- assert(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
1704
- this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1705
- }
1706
- else if (localOpMetadata.type === "createSubDir") {
1707
- assert(pendingCreateCount !== undefined && pendingCreateCount > 0, 0x6c3 /* pendingCreateCount should exist */);
1708
- this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1709
- }
1710
- }
1711
- if (op.type === "deleteSubDirectory") {
1712
- const resetSubDirectoryTree = (directory) => {
1713
- if (!directory) {
1714
- return;
1715
- }
1716
- // If this is delete op and we have keys in this subDirectory, then we need to delete these
1717
- // keys except the pending ones as they will be sequenced after this delete.
1718
- directory.sequencedStorageData.clear();
1719
- directory.emit("clear", true, directory);
1720
- // In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
1721
- // creators as the previous directory is getting deleted and we will initialize again when
1722
- // we will receive op for the create again.
1723
- directory.seqData.seq = -1;
1724
- directory.seqData.clientSeq = -1;
1725
- directory.clientIds.clear();
1726
- // Do the same thing for the subtree of the directory. If create is not pending for a child, then just
1727
- // delete it.
1728
- const subDirectories = directory.subdirectories();
1729
- for (const [subDirName, subDir] of subDirectories) {
1730
- if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
1731
- resetSubDirectoryTree(subDir);
1732
- continue;
1733
- }
1734
- directory.deleteSubDirectoryCore(subDirName, false);
1735
- }
1736
- };
1737
- const subDirectory = this._subdirectories.get(op.subdirName);
1738
- // Clear the creation tracker record
1739
- this.ackedCreationSeqTracker.delete(op.subdirName);
1740
- resetSubDirectoryTree(subDirectory);
1741
- }
1742
- if (op.type === "createSubDirectory") {
1743
- const dir = this._subdirectories.get(op.subdirName);
1744
- // Child sub directory create seq number can't be lower than the parent subdirectory.
1745
- // The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
1746
- if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
1747
- if (dir?.seqData.seq === -1) {
1748
- // Only set the sequence data based on the first message
1749
- dir.seqData.seq = msg.sequenceNumber;
1750
- dir.seqData.clientSeq = msg.clientSequenceNumber;
1751
- // set the creation seq in tracker
1752
- if (!this.ackedCreationSeqTracker.has(op.subdirName) &&
1753
- !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1754
- this.ackedCreationSeqTracker.set(op.subdirName, {
1755
- seq: msg.sequenceNumber,
1756
- clientSeq: msg.clientSequenceNumber,
1757
- });
1758
- if (local) {
1759
- this.localCreationSeqTracker.delete(op.subdirName);
1760
- }
1761
- }
1762
- }
1763
- // The client created the dir at or after the dirs seq, so list its client id as a creator.
1764
- if (dir !== undefined &&
1765
- !dir.clientIds.has(msg.clientId) &&
1766
- dir.seqData.seq <= msg.sequenceNumber) {
1767
- dir.clientIds.add(msg.clientId);
1768
- }
1769
- }
1770
- }
1771
- return false;
1772
- }
1773
- return !local;
1774
- }
1775
- /**
1776
- * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1777
- * @param subdirName - The name of the subdirectory being created
1778
- * @param local - Whether the message originated from the local client
1779
- * @param seqData - Sequence number and client sequence number at which this directory is created
1780
- * @param clientId - Id of client which created this directory.
1781
- * @returns True if is newly created, false if it already existed.
1782
- */
1783
- createSubDirectoryCore(subdirName, local, seqData, clientId) {
1784
- const subdir = this._subdirectories.get(subdirName);
1785
- if (subdir === undefined) {
1786
- const absolutePath = posix.join(this.absolutePath, subdirName);
1787
- const subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath, this.logger);
1788
- /**
1789
- * Store the sequence numbers of newly created subdirectory to the proper creation tracker, based
1790
- * on whether the creation behavior has been ack'd or not
1791
- */
1792
- if (isAcknowledgedOrDetached(seqData)) {
1793
- this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
1794
- }
1795
- else {
1796
- this.localCreationSeqTracker.set(subdirName, { ...seqData });
1797
- }
1798
- this.registerEventsOnSubDirectory(subDir, subdirName);
1799
- this._subdirectories.set(subdirName, subDir);
1800
- this.emit("subDirectoryCreated", subdirName, local, this);
1801
- return true;
1802
- }
1803
- else {
1804
- subdir.clientIds.add(clientId);
1805
- }
1806
- return false;
1746
+ * @param targetSubdir - subdirectory instance we are targeting from local op metadata (if a local op)
1747
+ */
1748
+ isMessageForCurrentInstanceOfSubDirectory(msg, targetSubdir) {
1749
+ // The message must be from this instance of the directory (if a local op) AND one of the following must be true:
1750
+ // 1. The message was from the creator of this directory
1751
+ // 2. This directory was created while detached
1752
+ // 3. This directory was already live (known to other clients) and the op was created after the directory was created.
1753
+ return ((targetSubdir === undefined || targetSubdir === this) &&
1754
+ ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
1755
+ this.clientIds.has("detached") ||
1756
+ (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)));
1807
1757
  }
1808
1758
  registerEventsOnSubDirectory(subDirectory, subDirName) {
1809
1759
  subDirectory.on("subDirectoryCreated", (relativePath, local) => {
@@ -1813,34 +1763,8 @@ class SubDirectory extends TypedEventEmitter {
1813
1763
  this.emit("subDirectoryDeleted", posix.join(subDirName, relativePath), local, this);
1814
1764
  });
1815
1765
  }
1816
- /**
1817
- * Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1818
- * @param subdirName - The name of the subdirectory being deleted
1819
- * @param local - Whether the message originated from the local client
1820
- */
1821
- deleteSubDirectoryCore(subdirName, local) {
1822
- const previousValue = this._subdirectories.get(subdirName);
1823
- // This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
1824
- // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
1825
- if (previousValue !== undefined) {
1826
- this._subdirectories.delete(subdirName);
1827
- /**
1828
- * Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
1829
- * ack'd already or still not committed yet (could be both).
1830
- */
1831
- if (this.ackedCreationSeqTracker.has(subdirName)) {
1832
- this.ackedCreationSeqTracker.delete(subdirName);
1833
- }
1834
- if (this.localCreationSeqTracker.has(subdirName)) {
1835
- this.localCreationSeqTracker.delete(subdirName);
1836
- }
1837
- this.disposeSubDirectoryTree(previousValue);
1838
- this.emit("subDirectoryDeleted", subdirName, local, this);
1839
- }
1840
- return previousValue;
1841
- }
1842
1766
  disposeSubDirectoryTree(directory) {
1843
- if (!directory) {
1767
+ if (directory === undefined) {
1844
1768
  return;
1845
1769
  }
1846
1770
  // Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
@@ -1848,17 +1772,76 @@ class SubDirectory extends TypedEventEmitter {
1848
1772
  for (const [_, subDirectory] of subDirectories) {
1849
1773
  this.disposeSubDirectoryTree(subDirectory);
1850
1774
  }
1775
+ // We need to reset the sequenced data as the previous directory is getting deleted and we will
1776
+ // initialize again when we will receive op for the create again.
1777
+ directory.clearSubDirectorySequencedData();
1778
+ directory.dispose();
1779
+ }
1780
+ emitDisposeForSubdirTree(directory) {
1781
+ if (directory === undefined || directory.disposed) {
1782
+ return;
1783
+ }
1784
+ // Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
1785
+ const subDirectories = directory.subdirectories();
1786
+ for (const [_, subDirectory] of subDirectories) {
1787
+ this.emitDisposeForSubdirTree(subDirectory);
1788
+ }
1851
1789
  if (typeof directory.dispose === "function") {
1852
- directory.dispose();
1790
+ directory.emit("disposed", directory);
1853
1791
  }
1854
1792
  }
1855
- undeleteSubDirectoryTree(directory) {
1856
- // Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
1857
- // This will unmark "deleted" from the subdirectories from top to bottom.
1793
+ undisposeSubdirectoryTree(directory) {
1794
+ // This will unmark "deleted" from the subdirectories from bottom to top.
1795
+ for (const [_, subDirectory] of directory.getSubdirectoriesEvenIfDisposed()) {
1796
+ this.undisposeSubdirectoryTree(subDirectory);
1797
+ }
1858
1798
  directory.undispose();
1859
- for (const [_, subDirectory] of directory.subdirectories()) {
1860
- this.undeleteSubDirectoryTree(subDirectory);
1799
+ }
1800
+ /**
1801
+ * Similar to {@link subdirectories}, but also includes subdirectories that are disposed.
1802
+ */
1803
+ getSubdirectoriesEvenIfDisposed() {
1804
+ const sequencedSubdirs = [];
1805
+ const sequencedSubdirNames = new Set([...this._sequencedSubdirectories.keys()]);
1806
+ for (const subdirName of sequencedSubdirNames) {
1807
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
1808
+ if (optimisticSubdir !== undefined) {
1809
+ sequencedSubdirs.push([subdirName, optimisticSubdir]);
1810
+ }
1861
1811
  }
1812
+ const pendingSubdirNames = [
1813
+ ...new Set(this.pendingSubDirectoryData
1814
+ .map((entry) => entry.subdirName)
1815
+ .filter((subdirName) => !sequencedSubdirNames.has(subdirName))),
1816
+ ];
1817
+ const pendingSubdirs = [];
1818
+ for (const subdirName of pendingSubdirNames) {
1819
+ const optimisticSubdir = this.getOptimisticSubDirectory(subdirName, true);
1820
+ if (optimisticSubdir !== undefined) {
1821
+ pendingSubdirs.push([subdirName, optimisticSubdir]);
1822
+ }
1823
+ }
1824
+ const allSubdirs = [...sequencedSubdirs, ...pendingSubdirs];
1825
+ const orderedSubdirs = allSubdirs.sort((a, b) => {
1826
+ const aSeqData = a[1].seqData;
1827
+ const bSeqData = b[1].seqData;
1828
+ assert(aSeqData !== undefined && bSeqData !== undefined, 0xc3a /* seqData should be defined */);
1829
+ return seqDataComparator(aSeqData, bSeqData);
1830
+ });
1831
+ return orderedSubdirs[Symbol.iterator]();
1832
+ }
1833
+ /**
1834
+ * Clears the sequenced data of a subdirectory but notably retains the pending
1835
+ * storage data. This is done when disposing of a directory so if we need to
1836
+ * re-create it, then we still have the pending ops.
1837
+ */
1838
+ clearSubDirectorySequencedData() {
1839
+ this.seqData.seq = -1;
1840
+ this.seqData.clientSeq = -1;
1841
+ this.sequencedStorageData.clear();
1842
+ this._sequencedSubdirectories.clear();
1843
+ this.clientIds.clear();
1844
+ this.clientIds.add(this.runtime.clientId ?? "detached");
1862
1845
  }
1863
1846
  }
1864
1847
  //# sourceMappingURL=directory.js.map