@fluidframework/map 2.52.0 → 2.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/directory.js CHANGED
@@ -13,6 +13,7 @@ import { SharedObject, ValueType, bindHandles, parseHandles, } from "@fluidframe
13
13
  import { UsageError, } from "@fluidframework/telemetry-utils/internal";
14
14
  import path from "path-browserify";
15
15
  import { serializeValue, migrateIfSharedSerializable } from "./localValues.js";
16
+ import { findLast, findLastIndex } from "./utils.js";
16
17
  // We use path-browserify since this code can run safely on the server or the browser.
17
18
  // We standardize on using posix slashes everywhere.
18
19
  const posix = path.posix;
@@ -340,7 +341,7 @@ export class SharedDirectory extends SharedObject {
340
341
  const message = content;
341
342
  const handler = this.messageHandlers.get(message.type);
342
343
  assert(handler !== undefined, 0x00d /* Missing message handler for message type */);
343
- handler.submit(message, localOpMetadata);
344
+ handler.resubmit(message, localOpMetadata);
344
345
  }
345
346
  /**
346
347
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
@@ -488,7 +489,7 @@ export class SharedDirectory extends SharedObject {
488
489
  subdir.processClearMessage(msg, op, local, localOpMetadata);
489
490
  }
490
491
  },
491
- submit: (op, localOpMetadata) => {
492
+ resubmit: (op, localOpMetadata) => {
492
493
  const subdir = this.getWorkingDirectory(op.path);
493
494
  if (subdir) {
494
495
  subdir.resubmitClearMessage(op, localOpMetadata);
@@ -504,7 +505,7 @@ export class SharedDirectory extends SharedObject {
504
505
  subdir.processDeleteMessage(msg, op, local, localOpMetadata);
505
506
  }
506
507
  },
507
- submit: (op, localOpMetadata) => {
508
+ resubmit: (op, localOpMetadata) => {
508
509
  const subdir = this.getWorkingDirectory(op.path);
509
510
  if (subdir) {
510
511
  subdir.resubmitKeyMessage(op, localOpMetadata);
@@ -522,7 +523,7 @@ export class SharedDirectory extends SharedObject {
522
523
  subdir.processSetMessage(msg, op, localValue, local, localOpMetadata);
523
524
  }
524
525
  },
525
- submit: (op, localOpMetadata) => {
526
+ resubmit: (op, localOpMetadata) => {
526
527
  const subdir = this.getWorkingDirectory(op.path);
527
528
  if (subdir) {
528
529
  subdir.resubmitKeyMessage(op, localOpMetadata);
@@ -538,7 +539,7 @@ export class SharedDirectory extends SharedObject {
538
539
  parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
539
540
  }
540
541
  },
541
- submit: (op, localOpMetadata) => {
542
+ resubmit: (op, localOpMetadata) => {
542
543
  const parentSubdir = this.getWorkingDirectory(op.path);
543
544
  if (parentSubdir) {
544
545
  // We don't reuse the metadata but send a new one on each submit.
@@ -555,7 +556,7 @@ export class SharedDirectory extends SharedObject {
555
556
  parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
556
557
  }
557
558
  },
558
- submit: (op, localOpMetadata) => {
559
+ resubmit: (op, localOpMetadata) => {
559
560
  const parentSubdir = this.getWorkingDirectory(op.path);
560
561
  if (parentSubdir) {
561
562
  // We don't reuse the metadata but send a new one on each submit.
@@ -655,28 +656,6 @@ export class SharedDirectory extends SharedObject {
655
656
  return builder.getSummaryTree();
656
657
  }
657
658
  }
658
- /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
659
- function isKeyEditLocalOpMetadata(metadata) {
660
- return (metadata !== undefined &&
661
- typeof metadata.pendingMessageId === "number" &&
662
- metadata.type === "edit");
663
- }
664
- function isClearLocalOpMetadata(metadata) {
665
- return (metadata !== undefined &&
666
- metadata.type === "clear" &&
667
- typeof metadata.pendingMessageId === "number" &&
668
- typeof metadata.previousStorage === "object");
669
- }
670
- function isSubDirLocalOpMetadata(metadata) {
671
- return (metadata !== undefined &&
672
- (metadata.type === "createSubDir" || metadata.type === "deleteSubDir"));
673
- }
674
- function isDirectoryLocalOpMetadata(metadata) {
675
- return (isKeyEditLocalOpMetadata(metadata) ||
676
- isClearLocalOpMetadata(metadata) ||
677
- isSubDirLocalOpMetadata(metadata));
678
- }
679
- /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
680
659
  // eslint-disable-next-line @rushstack/no-new-null
681
660
  function assertNonNullClientId(clientId) {
682
661
  assert(clientId !== null, 0x6af /* client id should never be null */);
@@ -713,21 +692,10 @@ class SubDirectory extends TypedEventEmitter {
713
692
  * String representation for the class.
714
693
  */
715
694
  this[_b] = "SubDirectory";
716
- /**
717
- * The in-memory data the directory is storing.
718
- */
719
- this._storage = new Map();
720
695
  /**
721
696
  * The subdirectories the directory is holding.
722
697
  */
723
698
  this._subdirectories = new Map();
724
- /**
725
- * Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
726
- * set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
727
- * was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
728
- * directory ops, so we have separate map from subDirectories tracker.
729
- */
730
- this.pendingKeys = new Map();
731
699
  /**
732
700
  * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
733
701
  * of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
@@ -741,18 +709,111 @@ class SubDirectory extends TypedEventEmitter {
741
709
  */
742
710
  this.pendingCreateSubDirectoriesTracker = new Map();
743
711
  /**
744
- * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
712
+ * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
713
+ * of the creation order.
745
714
  */
746
- this.pendingMessageId = -1;
715
+ this.localCreationSeq = 0;
747
716
  /**
748
- * The pending ids of any clears that have been performed locally but not yet ack'd from the server
717
+ * The data this SubDirectory instance is storing, but only including sequenced values (no local pending
718
+ * modifications are included).
749
719
  */
750
- this.pendingClearMessageIds = [];
720
+ this.sequencedStorageData = new Map();
751
721
  /**
752
- * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
753
- * of the creation order.
722
+ * A data structure containing all local pending storage modifications, which is used in combination
723
+ * with the sequencedStorageData to compute optimistic values.
724
+ *
725
+ * Pending sets are aggregated into "lifetimes", which permit correct relative iteration order
726
+ * even across remote operations and rollbacks.
754
727
  */
755
- this.localCreationSeq = 0;
728
+ this.pendingStorageData = [];
729
+ /**
730
+ * An internal iterator that iterates over the entries in the directory.
731
+ */
732
+ this.internalIterator = () => {
733
+ // We perform iteration in two steps - first by iterating over members of the sequenced storage data that are not
734
+ // optimistically deleted or cleared, and then over the pending data lifetimes that have not subsequently
735
+ // been deleted or cleared. In total, this give an ordering of members based on when they were initially
736
+ // added to the sub directory (even if they were later modified), similar to the native Map.
737
+ const sequencedStorageDataIterator = this.sequencedStorageData.keys();
738
+ const pendingStorageDataIterator = this.pendingStorageData.values();
739
+ const next = () => {
740
+ let nextSequencedKey = sequencedStorageDataIterator.next();
741
+ while (!nextSequencedKey.done) {
742
+ const key = nextSequencedKey.value;
743
+ // If we have any pending deletes or clears, then we won't iterate to this key yet (if at all).
744
+ // Either it is optimistically deleted and will not be part of the iteration, or it was
745
+ // re-added later and we'll iterate to it when we get to the pending data.
746
+ if (!this.pendingStorageData.some((entry) => entry.type === "clear" || (entry.type === "delete" && entry.key === key))) {
747
+ assert(this.has(key), 0xc03 /* key should exist in sequenced or pending data */);
748
+ const optimisticValue = this.getOptimisticValue(key);
749
+ return { value: [key, optimisticValue], done: false };
750
+ }
751
+ nextSequencedKey = sequencedStorageDataIterator.next();
752
+ }
753
+ let nextPending = pendingStorageDataIterator.next();
754
+ while (!nextPending.done) {
755
+ const nextPendingEntry = nextPending.value;
756
+ // A lifetime entry may need to be iterated.
757
+ if (nextPendingEntry.type === "lifetime") {
758
+ const nextPendingEntryIndex = this.pendingStorageData.indexOf(nextPendingEntry);
759
+ const mostRecentDeleteOrClearIndex = findLastIndex(this.pendingStorageData, (entry) => entry.type === "clear" ||
760
+ (entry.type === "delete" && entry.key === nextPendingEntry.key));
761
+ // Only iterate the pending entry now if it hasn't been deleted or cleared.
762
+ if (nextPendingEntryIndex > mostRecentDeleteOrClearIndex) {
763
+ const latestPendingValue =
764
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
765
+ nextPendingEntry.keySets[nextPendingEntry.keySets.length - 1];
766
+ // Skip iterating if we would have would have already iterated it as part of the sequenced data.
767
+ // This is not a perfect check in the case the map has changed since the iterator was created
768
+ // (e.g. if a remote client added the same key in the meantime).
769
+ if (!this.sequencedStorageData.has(nextPendingEntry.key) ||
770
+ mostRecentDeleteOrClearIndex !== -1) {
771
+ return { value: [nextPendingEntry.key, latestPendingValue.value], done: false };
772
+ }
773
+ }
774
+ }
775
+ nextPending = pendingStorageDataIterator.next();
776
+ }
777
+ return { value: undefined, done: true };
778
+ };
779
+ const iterator = {
780
+ next,
781
+ [Symbol.iterator]() {
782
+ return this;
783
+ },
784
+ };
785
+ return iterator;
786
+ };
787
+ /**
788
+ * Compute the optimistic local value for a given key. This combines the sequenced data with
789
+ * any pending changes that have not yet been sequenced.
790
+ */
791
+ this.getOptimisticValue = (key) => {
792
+ const latestPendingEntry = findLast(this.pendingStorageData, (entry) => entry.type === "clear" || entry.key === key);
793
+ if (latestPendingEntry === undefined) {
794
+ return this.sequencedStorageData.get(key);
795
+ }
796
+ else if (latestPendingEntry.type === "lifetime") {
797
+ const latestPendingSet =
798
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
799
+ latestPendingEntry.keySets[latestPendingEntry.keySets.length - 1];
800
+ return latestPendingSet.value;
801
+ }
802
+ else {
803
+ // Delete or clear
804
+ return undefined;
805
+ }
806
+ };
807
+ /**
808
+ * Determine if the directory optimistically has the key.
809
+ * This will return true even if the value is undefined.
810
+ */
811
+ this.optimisticallyHas = (key) => {
812
+ const latestPendingEntry = findLast(this.pendingStorageData, (entry) => entry.type === "clear" || entry.key === key);
813
+ return latestPendingEntry === undefined
814
+ ? this.sequencedStorageData.has(key)
815
+ : latestPendingEntry.type === "lifetime";
816
+ };
756
817
  this.localCreationSeqTracker = new DirectoryCreationTracker();
757
818
  this.ackedCreationSeqTracker = new DirectoryCreationTracker();
758
819
  }
@@ -782,14 +843,13 @@ class SubDirectory extends TypedEventEmitter {
782
843
  */
783
844
  has(key) {
784
845
  this.throwIfDisposed();
785
- return this._storage.has(key);
846
+ return this.optimisticallyHas(key);
786
847
  }
787
848
  /**
788
849
  * {@inheritDoc IDirectory.get}
789
850
  */
790
851
  get(key) {
791
- this.throwIfDisposed();
792
- return this._storage.get(key);
852
+ return this.getOptimisticValue(key);
793
853
  }
794
854
  /**
795
855
  * {@inheritDoc IDirectory.set}
@@ -800,21 +860,60 @@ class SubDirectory extends TypedEventEmitter {
800
860
  if (key === undefined || key === null) {
801
861
  throw new Error("Undefined and null keys are not supported");
802
862
  }
863
+ const previousOptimisticLocalValue = this.getOptimisticValue(key);
803
864
  // Create a local value and serialize it.
804
865
  bindHandles(value, this.serializer, this.directory.handle);
805
- // Set the value locally.
806
- const previousValue = this.setCore(key, value, true);
807
866
  // If we are not attached, don't submit the op.
808
867
  if (!this.directory.isAttached()) {
868
+ this.sequencedStorageData.set(key, value);
869
+ const event = {
870
+ key,
871
+ path: this.absolutePath,
872
+ previousValue: previousOptimisticLocalValue,
873
+ };
874
+ this.directory.emit("valueChanged", event, true, this.directory);
875
+ const containedEvent = {
876
+ key,
877
+ previousValue: previousOptimisticLocalValue,
878
+ };
879
+ this.emit("containedValueChanged", containedEvent, true, this);
809
880
  return this;
810
881
  }
882
+ // A new pending key lifetime is created if:
883
+ // 1. There isn't any pending entry for the key yet
884
+ // 2. The most recent pending entry for the key was a deletion (as this terminates the prior lifetime)
885
+ // 3. A clear was sent after the last pending entry for the key (which also terminates the prior lifetime)
886
+ let latestPendingEntry = findLast(this.pendingStorageData, (entry) => entry.type === "clear" || entry.key === key);
887
+ if (latestPendingEntry === undefined ||
888
+ latestPendingEntry.type === "delete" ||
889
+ latestPendingEntry.type === "clear") {
890
+ latestPendingEntry = { type: "lifetime", path: this.absolutePath, key, keySets: [] };
891
+ this.pendingStorageData.push(latestPendingEntry);
892
+ }
893
+ const pendingKeySet = {
894
+ type: "set",
895
+ path: this.absolutePath,
896
+ value,
897
+ };
898
+ latestPendingEntry.keySets.push(pendingKeySet);
811
899
  const op = {
812
900
  key,
813
901
  path: this.absolutePath,
814
902
  type: "set",
815
903
  value: { type: ValueType[ValueType.Plain], value },
816
904
  };
817
- this.submitKeyMessage(op, previousValue);
905
+ this.submitKeyMessage(op, pendingKeySet);
906
+ const directoryValueChanged = {
907
+ key,
908
+ path: this.absolutePath,
909
+ previousValue: previousOptimisticLocalValue,
910
+ };
911
+ this.directory.emit("valueChanged", directoryValueChanged, true, this.directory);
912
+ const valueChanged = {
913
+ key,
914
+ previousValue: previousOptimisticLocalValue,
915
+ };
916
+ this.emit("containedValueChanged", valueChanged, true, this);
818
917
  return this;
819
918
  }
820
919
  /**
@@ -976,37 +1075,76 @@ class SubDirectory extends TypedEventEmitter {
976
1075
  */
977
1076
  delete(key) {
978
1077
  this.throwIfDisposed();
979
- // Delete the key locally first.
980
- const previousValue = this.deleteCore(key, true);
981
- // If we are not attached, don't submit the op.
1078
+ const previousOptimisticLocalValue = this.getOptimisticValue(key);
982
1079
  if (!this.directory.isAttached()) {
983
- return previousValue !== undefined;
1080
+ const successfullyRemoved = this.sequencedStorageData.delete(key);
1081
+ // Only emit if we actually deleted something.
1082
+ if (previousOptimisticLocalValue !== undefined && successfullyRemoved) {
1083
+ const event = {
1084
+ key,
1085
+ path: this.absolutePath,
1086
+ previousValue: previousOptimisticLocalValue,
1087
+ };
1088
+ this.directory.emit("valueChanged", event, true, this.directory);
1089
+ const containedEvent = {
1090
+ key,
1091
+ previousValue: previousOptimisticLocalValue,
1092
+ };
1093
+ this.emit("containedValueChanged", containedEvent, true, this);
1094
+ }
1095
+ return successfullyRemoved;
984
1096
  }
1097
+ const pendingKeyDelete = {
1098
+ type: "delete",
1099
+ path: this.absolutePath,
1100
+ key,
1101
+ };
1102
+ this.pendingStorageData.push(pendingKeyDelete);
985
1103
  const op = {
986
1104
  key,
987
- path: this.absolutePath,
988
1105
  type: "delete",
1106
+ path: this.absolutePath,
989
1107
  };
990
- this.submitKeyMessage(op, previousValue);
991
- return previousValue !== undefined;
1108
+ this.submitKeyMessage(op, pendingKeyDelete);
1109
+ // Only emit if we locally believe we deleted something. Otherwise we still send the op
1110
+ // (permitting speculative deletion even if we don't see anything locally) but don't emit
1111
+ // a valueChanged since we in fact did not locally observe a value change.
1112
+ if (previousOptimisticLocalValue !== undefined) {
1113
+ const event = {
1114
+ key,
1115
+ path: this.absolutePath,
1116
+ previousValue: previousOptimisticLocalValue,
1117
+ };
1118
+ this.directory.emit("valueChanged", event, true, this.directory);
1119
+ const containedEvent = {
1120
+ key,
1121
+ previousValue: previousOptimisticLocalValue,
1122
+ };
1123
+ this.emit("containedValueChanged", containedEvent, true, this);
1124
+ }
1125
+ return true;
992
1126
  }
993
1127
  /**
994
1128
  * Deletes all keys from within this IDirectory.
995
1129
  */
996
1130
  clear() {
997
1131
  this.throwIfDisposed();
998
- // If we are not attached, don't submit the op.
999
1132
  if (!this.directory.isAttached()) {
1000
- this.clearCore(true);
1133
+ this.sequencedStorageData.clear();
1134
+ this.directory.emit("clear", true, this.directory);
1001
1135
  return;
1002
1136
  }
1003
- const copy = new Map(this._storage);
1004
- this.clearCore(true);
1005
- const op = {
1137
+ const pendingClear = {
1138
+ type: "clear",
1006
1139
  path: this.absolutePath,
1140
+ };
1141
+ this.pendingStorageData.push(pendingClear);
1142
+ this.directory.emit("clear", true, this.directory);
1143
+ const op = {
1007
1144
  type: "clear",
1145
+ path: this.absolutePath,
1008
1146
  };
1009
- this.submitClearMessage(op, copy);
1147
+ this.submitClearMessage(op, pendingClear);
1010
1148
  }
1011
1149
  /**
1012
1150
  * Issue a callback on each entry under this IDirectory.
@@ -1014,17 +1152,16 @@ class SubDirectory extends TypedEventEmitter {
1014
1152
  */
1015
1153
  forEach(callback) {
1016
1154
  this.throwIfDisposed();
1017
- // eslint-disable-next-line unicorn/no-array-for-each
1018
- this._storage.forEach((value, key) => {
1019
- callback(value, key, this);
1020
- });
1155
+ for (const [key, localValue] of this.internalIterator()) {
1156
+ callback(localValue.value, key, this);
1157
+ }
1021
1158
  }
1022
1159
  /**
1023
1160
  * The number of entries under this IDirectory.
1024
1161
  */
1025
1162
  get size() {
1026
1163
  this.throwIfDisposed();
1027
- return this._storage.size;
1164
+ return [...this.internalIterator()].length;
1028
1165
  }
1029
1166
  /**
1030
1167
  * Get an iterator over the entries under this IDirectory.
@@ -1032,7 +1169,23 @@ class SubDirectory extends TypedEventEmitter {
1032
1169
  */
1033
1170
  entries() {
1034
1171
  this.throwIfDisposed();
1035
- return this._storage.entries();
1172
+ const internalIterator = this.internalIterator();
1173
+ const next = () => {
1174
+ const nextResult = internalIterator.next();
1175
+ if (nextResult.done) {
1176
+ return { value: undefined, done: true };
1177
+ }
1178
+ // Unpack the stored value
1179
+ const [key, localValue] = nextResult.value;
1180
+ return { value: [key, localValue], done: false };
1181
+ };
1182
+ const iterator = {
1183
+ next,
1184
+ [Symbol.iterator]() {
1185
+ return this;
1186
+ },
1187
+ };
1188
+ return iterator;
1036
1189
  }
1037
1190
  /**
1038
1191
  * Get an iterator over the keys under this IDirectory.
@@ -1040,7 +1193,22 @@ class SubDirectory extends TypedEventEmitter {
1040
1193
  */
1041
1194
  keys() {
1042
1195
  this.throwIfDisposed();
1043
- return this._storage.keys();
1196
+ const internalIterator = this.internalIterator();
1197
+ const next = () => {
1198
+ const nextResult = internalIterator.next();
1199
+ if (nextResult.done) {
1200
+ return { value: undefined, done: true };
1201
+ }
1202
+ const [key] = nextResult.value;
1203
+ return { value: key, done: false };
1204
+ };
1205
+ const iterator = {
1206
+ next,
1207
+ [Symbol.iterator]() {
1208
+ return this;
1209
+ },
1210
+ };
1211
+ return iterator;
1044
1212
  }
1045
1213
  /**
1046
1214
  * Get an iterator over the values under this IDirectory.
@@ -1048,7 +1216,22 @@ class SubDirectory extends TypedEventEmitter {
1048
1216
  */
1049
1217
  values() {
1050
1218
  this.throwIfDisposed();
1051
- return this._storage.values();
1219
+ const internalIterator = this.internalIterator();
1220
+ const next = () => {
1221
+ const nextResult = internalIterator.next();
1222
+ if (nextResult.done) {
1223
+ return { value: undefined, done: true };
1224
+ }
1225
+ const [, localValue] = nextResult.value;
1226
+ return { value: localValue, done: false };
1227
+ };
1228
+ const iterator = {
1229
+ next,
1230
+ [Symbol.iterator]() {
1231
+ return this;
1232
+ },
1233
+ };
1234
+ return iterator;
1052
1235
  }
1053
1236
  /**
1054
1237
  * Get an iterator over the entries under this IDirectory.
@@ -1056,7 +1239,7 @@ class SubDirectory extends TypedEventEmitter {
1056
1239
  */
1057
1240
  [(_b = Symbol.toStringTag, Symbol.iterator)]() {
1058
1241
  this.throwIfDisposed();
1059
- return this.entries();
1242
+ return this.internalIterator();
1060
1243
  }
1061
1244
  /**
1062
1245
  * Process a clear operation.
@@ -1072,12 +1255,35 @@ class SubDirectory extends TypedEventEmitter {
1072
1255
  return;
1073
1256
  }
1074
1257
  if (local) {
1075
- assert(isClearLocalOpMetadata(localOpMetadata), 0x00f /* pendingMessageId is missing from the local client's operation */);
1076
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
1077
- assert(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32a /* pendingMessageId does not match */);
1078
- return;
1258
+ this.sequencedStorageData.clear();
1259
+ const pendingClear = this.pendingStorageData.shift();
1260
+ assert(pendingClear !== undefined &&
1261
+ pendingClear.type === "clear" &&
1262
+ pendingClear === localOpMetadata, 0xc04 /* Got a local clear message we weren't expecting */);
1263
+ }
1264
+ else {
1265
+ // For pending set operations, collect the previous values before clearing sequenced data
1266
+ const pendingSets = [];
1267
+ for (const entry of this.pendingStorageData) {
1268
+ if (entry.type === "lifetime") {
1269
+ const previousValue = this.sequencedStorageData.get(entry.key);
1270
+ pendingSets.push({ key: entry.key, previousValue });
1271
+ }
1272
+ }
1273
+ this.sequencedStorageData.clear();
1274
+ // Only emit for remote ops, we would have already emitted for local ops. Only emit if there
1275
+ // is no optimistically-applied local pending clear that would supersede this remote clear.
1276
+ if (!this.pendingStorageData.some((entry) => entry.type === "clear")) {
1277
+ this.directory.emit("clear", local, this.directory);
1278
+ }
1279
+ // For pending set operations, emit valueChanged events
1280
+ for (const { key, previousValue } of pendingSets) {
1281
+ this.directory.emit("valueChanged", {
1282
+ key,
1283
+ previousValue,
1284
+ }, local, this.directory);
1285
+ }
1079
1286
  }
1080
- this.clearExceptPendingKeys(false);
1081
1287
  }
1082
1288
  /**
1083
1289
  * Process a delete operation.
@@ -1089,11 +1295,33 @@ class SubDirectory extends TypedEventEmitter {
1089
1295
  */
1090
1296
  processDeleteMessage(msg, op, local, localOpMetadata) {
1091
1297
  this.throwIfDisposed();
1092
- if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1093
- this.needProcessStorageOperation(op, local, localOpMetadata))) {
1298
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1094
1299
  return;
1095
1300
  }
1096
- this.deleteCore(op.key, local);
1301
+ if (local) {
1302
+ const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === op.key);
1303
+ const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1304
+ assert(pendingEntry !== undefined &&
1305
+ pendingEntry.type === "delete" &&
1306
+ pendingEntry.key === op.key, 0xc05 /* Got a local delete message we weren't expecting */);
1307
+ this.pendingStorageData.splice(pendingEntryIndex, 1);
1308
+ this.sequencedStorageData.delete(op.key);
1309
+ }
1310
+ else {
1311
+ const previousValue = this.sequencedStorageData.get(op.key);
1312
+ this.sequencedStorageData.delete(op.key);
1313
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
1314
+ if (!this.pendingStorageData.some((entry) => entry.type === "clear" || entry.key === op.key)) {
1315
+ const event = {
1316
+ key: op.key,
1317
+ path: this.absolutePath,
1318
+ previousValue,
1319
+ };
1320
+ this.directory.emit("valueChanged", event, local, this.directory);
1321
+ const containedEvent = { key: op.key, previousValue };
1322
+ this.emit("containedValueChanged", containedEvent, local, this);
1323
+ }
1324
+ }
1097
1325
  }
1098
1326
  /**
1099
1327
  * Process a set operation.
@@ -1105,13 +1333,33 @@ class SubDirectory extends TypedEventEmitter {
1105
1333
  */
1106
1334
  processSetMessage(msg, op, value, local, localOpMetadata) {
1107
1335
  this.throwIfDisposed();
1108
- if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1109
- this.needProcessStorageOperation(op, local, localOpMetadata))) {
1336
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1110
1337
  return;
1111
1338
  }
1112
- // needProcessStorageOperation should have returned false if local is true
1113
- // so we can assume localValue is not undefined
1114
- this.setCore(op.key, value, local);
1339
+ const { key } = op;
1340
+ if (local) {
1341
+ const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === key);
1342
+ const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1343
+ assert(pendingEntry !== undefined && pendingEntry.type === "lifetime", 0xc06 /* Couldn't match local set message to pending lifetime */);
1344
+ const pendingKeySet = pendingEntry.keySets.shift();
1345
+ assert(pendingKeySet !== undefined && pendingKeySet === localOpMetadata, 0xc07 /* Got a local set message we weren't expecting */);
1346
+ if (pendingEntry.keySets.length === 0) {
1347
+ this.pendingStorageData.splice(pendingEntryIndex, 1);
1348
+ }
1349
+ this.sequencedStorageData.set(key, pendingKeySet.value);
1350
+ }
1351
+ else {
1352
+ // Get the previous value before setting the new value
1353
+ const previousValue = this.sequencedStorageData.get(key);
1354
+ this.sequencedStorageData.set(key, value);
1355
+ // Suppress the event if local changes would cause the incoming change to be invisible optimistically.
1356
+ if (!this.pendingStorageData.some((entry) => entry.type === "clear" || entry.key === key)) {
1357
+ const event = { key, path: this.absolutePath, previousValue };
1358
+ this.directory.emit("valueChanged", event, local, this.directory);
1359
+ const containedEvent = { key, previousValue };
1360
+ this.emit("containedValueChanged", containedEvent, local, this);
1361
+ }
1362
+ }
1115
1363
  }
1116
1364
  /**
1117
1365
  * Process a create subdirectory operation.
@@ -1149,57 +1397,33 @@ class SubDirectory extends TypedEventEmitter {
1149
1397
  /**
1150
1398
  * Submit a clear operation.
1151
1399
  * @param op - The operation
1400
+ * @param localOpMetadata - The pending operation metadata
1152
1401
  */
1153
- submitClearMessage(op, previousValue) {
1402
+ submitClearMessage(op, localOpMetadata) {
1154
1403
  this.throwIfDisposed();
1155
- const pendingMsgId = ++this.pendingMessageId;
1156
- this.pendingClearMessageIds.push(pendingMsgId);
1157
- const metadata = {
1158
- type: "clear",
1159
- pendingMessageId: pendingMsgId,
1160
- previousStorage: previousValue,
1161
- };
1162
- this.directory.submitDirectoryMessage(op, metadata);
1404
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
1163
1405
  }
1164
1406
  /**
1165
1407
  * Resubmit a clear operation.
1166
1408
  * @param op - The operation
1167
1409
  */
1168
1410
  resubmitClearMessage(op, localOpMetadata) {
1169
- assert(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
1170
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1171
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
1172
1411
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1173
1412
  // is already deleted, in which case we don't need to submit the op.
1174
- if (pendingClearMessageId === localOpMetadata.pendingMessageId) {
1175
- this.submitClearMessage(op, localOpMetadata.previousStorage);
1413
+ const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type === "clear");
1414
+ const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1415
+ if (pendingEntry !== undefined) {
1416
+ this.submitClearMessage(op, localOpMetadata);
1176
1417
  }
1177
1418
  }
1178
- /**
1179
- * Get a new pending message id for the op and cache it to track the pending op
1180
- */
1181
- getKeyMessageId(op) {
1182
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1183
- const pendingMessageId = ++this.pendingMessageId;
1184
- const pendingMessageIds = this.pendingKeys.get(op.key);
1185
- if (pendingMessageIds === undefined) {
1186
- this.pendingKeys.set(op.key, [pendingMessageId]);
1187
- }
1188
- else {
1189
- pendingMessageIds.push(pendingMessageId);
1190
- }
1191
- return pendingMessageId;
1192
- }
1193
1419
  /**
1194
1420
  * Submit a key operation.
1195
1421
  * @param op - The operation
1196
- * @param previousValue - The value of the key before this op
1422
+ * @param localOpMetadata - The pending operation metadata
1197
1423
  */
1198
- submitKeyMessage(op, previousValue) {
1424
+ submitKeyMessage(op, localOpMetadata) {
1199
1425
  this.throwIfDisposed();
1200
- const pendingMessageId = this.getKeyMessageId(op);
1201
- const localMetadata = { type: "edit", pendingMessageId, previousValue };
1202
- this.directory.submitDirectoryMessage(op, localMetadata);
1426
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
1203
1427
  }
1204
1428
  /**
1205
1429
  * Submit a key message to remote clients based on a previous submit.
@@ -1207,21 +1431,12 @@ class SubDirectory extends TypedEventEmitter {
1207
1431
  * @param localOpMetadata - Metadata from the previous submit
1208
1432
  */
1209
1433
  resubmitKeyMessage(op, localOpMetadata) {
1210
- assert(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
1211
- // clear the old pending message id
1212
- const pendingMessageIds = this.pendingKeys.get(op.key);
1213
1434
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1214
1435
  // is already deleted, in which case we don't need to submit the op.
1215
- if (pendingMessageIds !== undefined) {
1216
- const index = pendingMessageIds.indexOf(localOpMetadata.pendingMessageId);
1217
- if (index === -1) {
1218
- return;
1219
- }
1220
- pendingMessageIds.splice(index, 1);
1221
- if (pendingMessageIds.length === 0) {
1222
- this.pendingKeys.delete(op.key);
1223
- }
1224
- this.submitKeyMessage(op, localOpMetadata.previousValue);
1436
+ const pendingEntryIndex = this.pendingStorageData.findIndex((entry) => entry.type !== "clear" && entry.key === op.key);
1437
+ const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1438
+ if (pendingEntry !== undefined) {
1439
+ this.submitKeyMessage(op, localOpMetadata);
1225
1440
  }
1226
1441
  }
1227
1442
  incrementPendingSubDirCount(map, subDirName) {
@@ -1279,7 +1494,6 @@ class SubDirectory extends TypedEventEmitter {
1279
1494
  * @param localOpMetadata - metadata submitted with the op originally
1280
1495
  */
1281
1496
  resubmitSubDirectoryMessage(op, localOpMetadata) {
1282
- assert(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
1283
1497
  // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1284
1498
  // is already deleted, in which case we don't need to submit the op.
1285
1499
  if (localOpMetadata.type === "createSubDir" &&
@@ -1294,8 +1508,9 @@ class SubDirectory extends TypedEventEmitter {
1294
1508
  this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1295
1509
  this.submitCreateSubDirectoryMessage(op);
1296
1510
  }
1297
- else {
1511
+ else if (localOpMetadata.type === "deleteSubDir") {
1298
1512
  this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1513
+ assert(localOpMetadata.subDirectory !== undefined, 0xc08 /* localOpMetadata.subDirectory should be defined */);
1299
1514
  this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1300
1515
  }
1301
1516
  }
@@ -1306,7 +1521,7 @@ class SubDirectory extends TypedEventEmitter {
1306
1521
  */
1307
1522
  *getSerializedStorage(serializer) {
1308
1523
  this.throwIfDisposed();
1309
- for (const [key, value] of this._storage) {
1524
+ for (const [key, value] of this.sequencedStorageData.entries()) {
1310
1525
  const serializedValue = serializeValue(value, serializer, this.directory.handle);
1311
1526
  const res = [key, serializedValue];
1312
1527
  yield res;
@@ -1327,7 +1542,7 @@ class SubDirectory extends TypedEventEmitter {
1327
1542
  */
1328
1543
  populateStorage(key, value) {
1329
1544
  this.throwIfDisposed();
1330
- this._storage.set(key, value);
1545
+ this.sequencedStorageData.set(key, value);
1331
1546
  }
1332
1547
  /**
1333
1548
  * Populate a subdirectory into this subdirectory, to be used when loading from snapshot.
@@ -1339,32 +1554,6 @@ class SubDirectory extends TypedEventEmitter {
1339
1554
  this.registerEventsOnSubDirectory(newSubDir, subdirName);
1340
1555
  this._subdirectories.set(subdirName, newSubDir);
1341
1556
  }
1342
- /**
1343
- * Retrieve the local value at the given key. This is used to get value type information stashed on the local
1344
- * value so op handlers can be retrieved
1345
- * @param key - The key to retrieve from
1346
- * @returns The local value
1347
- */
1348
- getLocalValue(key) {
1349
- this.throwIfDisposed();
1350
- return this._storage.get(key);
1351
- }
1352
- /**
1353
- * Remove the pendingMessageId from the map tracking it on rollback
1354
- * @param map - map tracking the pending messages
1355
- * @param key - key of the edit in the op
1356
- */
1357
- rollbackPendingMessageId(map, key, pendingMessageId) {
1358
- const pendingMessageIds = map.get(key);
1359
- const lastPendingMessageId = pendingMessageIds?.pop();
1360
- if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
1361
- throw new Error("Rollback op does not match last pending");
1362
- }
1363
- if (pendingMessageIds.length === 0) {
1364
- map.delete(key);
1365
- }
1366
- }
1367
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
1368
1557
  /**
1369
1558
  * Rollback a local op
1370
1559
  * @param op - The operation to rollback
@@ -1372,41 +1561,80 @@ class SubDirectory extends TypedEventEmitter {
1372
1561
  */
1373
1562
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1374
1563
  rollback(op, localOpMetadata) {
1375
- if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
1376
- throw new Error("Invalid localOpMetadata");
1377
- }
1378
- if (op.type === "clear" && localOpMetadata.type === "clear") {
1379
- for (const [key, localValue] of localOpMetadata.previousStorage.entries()) {
1380
- this.setCore(key, localValue, true);
1381
- }
1382
- const lastPendingClearId = this.pendingClearMessageIds.pop();
1383
- if (lastPendingClearId === undefined ||
1384
- lastPendingClearId !== localOpMetadata.pendingMessageId) {
1385
- throw new Error("Rollback op does match last clear");
1564
+ const directoryOp = op;
1565
+ if (directoryOp.type === "clear") {
1566
+ // A pending clear will be last in the list, since it terminates all prior lifetimes.
1567
+ const pendingClear = this.pendingStorageData.pop();
1568
+ assert(pendingClear !== undefined &&
1569
+ pendingClear.type === "clear" &&
1570
+ localOpMetadata.type === "clear", 0xc09 /* Unexpected clear rollback */);
1571
+ for (const [key] of this.internalIterator()) {
1572
+ const event = {
1573
+ key,
1574
+ path: this.absolutePath,
1575
+ previousValue: undefined,
1576
+ };
1577
+ this.directory.emit("valueChanged", event, true, this.directory);
1578
+ const containedEvent = { key, previousValue: undefined };
1579
+ this.emit("containedValueChanged", containedEvent, true, this);
1386
1580
  }
1387
1581
  }
1388
- else if ((op.type === "delete" || op.type === "set") &&
1389
- localOpMetadata.type === "edit") {
1390
- const key = op.key;
1391
- assert(key !== undefined, 0x8ad /* "key" property is missing from edit operation. */);
1392
- assert(typeof key === "string", 0x8ae /* "key" property in edit operation is misconfigured. Expected a string. */);
1393
- if (localOpMetadata.previousValue === undefined) {
1394
- this.deleteCore(key, true);
1582
+ else if ((directoryOp.type === "delete" || directoryOp.type === "set") &&
1583
+ (localOpMetadata.type === "set" || localOpMetadata.type === "delete")) {
1584
+ // A pending set/delete may not be last in the list, as the lifetimes' order is based on when
1585
+ // they were created, not when they were last modified.
1586
+ const pendingEntryIndex = findLastIndex(this.pendingStorageData, (entry) => entry.type !== "clear" && entry.key === directoryOp.key);
1587
+ const pendingEntry = this.pendingStorageData[pendingEntryIndex];
1588
+ assert(pendingEntry !== undefined &&
1589
+ (pendingEntry.type === "delete" || pendingEntry.type === "lifetime"), 0xc0a /* Unexpected pending data for set/delete op */);
1590
+ if (pendingEntry.type === "delete") {
1591
+ assert(pendingEntry === localOpMetadata, 0xc0b /* Unexpected delete rollback */);
1592
+ this.pendingStorageData.splice(pendingEntryIndex, 1);
1593
+ // Only emit if rolling back the delete actually results in a value becoming visible.
1594
+ if (this.getOptimisticValue(directoryOp.key) !== undefined) {
1595
+ const event = {
1596
+ key: directoryOp.key,
1597
+ path: this.absolutePath,
1598
+ previousValue: undefined,
1599
+ };
1600
+ this.directory.emit("valueChanged", event, true, this.directory);
1601
+ const containedEvent = {
1602
+ key: directoryOp.key,
1603
+ previousValue: undefined,
1604
+ };
1605
+ this.emit("containedValueChanged", containedEvent, true, this);
1606
+ }
1395
1607
  }
1396
- else {
1397
- this.setCore(key, localOpMetadata.previousValue, true);
1608
+ else if (pendingEntry.type === "lifetime") {
1609
+ const pendingKeySet = pendingEntry.keySets.pop();
1610
+ assert(pendingKeySet !== undefined && pendingKeySet === localOpMetadata, 0xc0c /* Unexpected set rollback */);
1611
+ if (pendingEntry.keySets.length === 0) {
1612
+ this.pendingStorageData.splice(pendingEntryIndex, 1);
1613
+ }
1614
+ const event = {
1615
+ key: directoryOp.key,
1616
+ path: this.absolutePath,
1617
+ previousValue: pendingKeySet.value,
1618
+ };
1619
+ this.directory.emit("valueChanged", event, true, this.directory);
1620
+ const containedEvent = {
1621
+ key: directoryOp.key,
1622
+ previousValue: pendingKeySet.value,
1623
+ };
1624
+ this.emit("containedValueChanged", containedEvent, true, this);
1398
1625
  }
1399
- this.rollbackPendingMessageId(this.pendingKeys, key, localOpMetadata.pendingMessageId);
1400
1626
  }
1401
- else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1402
- const subdirName = op.subdirName;
1627
+ else if (directoryOp.type === "createSubDirectory" &&
1628
+ localOpMetadata.type === "createSubDir") {
1629
+ const subdirName = directoryOp.subdirName;
1403
1630
  assert(subdirName !== undefined, 0x8af /* "subdirName" property is missing from "createSubDirectory" operation. */);
1404
1631
  assert(typeof subdirName === "string", 0x8b0 /* "subdirName" property in "createSubDirectory" operation is misconfigured. Expected a string. */);
1405
1632
  this.deleteSubDirectoryCore(subdirName, true);
1406
1633
  this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, subdirName);
1407
1634
  }
1408
- else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
1409
- const subdirName = op.subdirName;
1635
+ else if (directoryOp.type === "deleteSubDirectory" &&
1636
+ localOpMetadata.type === "deleteSubDir") {
1637
+ const subdirName = directoryOp.subdirName;
1410
1638
  assert(subdirName !== undefined, 0x8b1 /* "subdirName" property is missing from "deleteSubDirectory" operation. */);
1411
1639
  assert(typeof subdirName === "string", 0x8b2 /* "subdirName" property in "deleteSubDirectory" operation is misconfigured. Expected a string. */);
1412
1640
  if (localOpMetadata.subDirectory !== undefined) {
@@ -1432,7 +1660,6 @@ class SubDirectory extends TypedEventEmitter {
1432
1660
  throw new Error("Unsupported op for rollback");
1433
1661
  }
1434
1662
  }
1435
- /* eslint-enable @typescript-eslint/no-unsafe-member-access */
1436
1663
  /**
1437
1664
  * Converts the given relative path into an absolute path.
1438
1665
  * @param path - Relative path to convert
@@ -1441,71 +1668,6 @@ class SubDirectory extends TypedEventEmitter {
1441
1668
  makeAbsolute(relativePath) {
1442
1669
  return posix.resolve(this.absolutePath, relativePath);
1443
1670
  }
1444
- /**
1445
- * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1446
- * not process the incoming operation.
1447
- * @param op - Operation to check
1448
- * @param local - Whether the operation originated from the local client
1449
- * @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
1450
- * For ops from a remote client, this will be undefined.
1451
- * @returns True if the operation should be processed, false otherwise
1452
- */
1453
- needProcessStorageOperation(op, local, localOpMetadata) {
1454
- const firstPendingClearMessageId = this.pendingClearMessageIds[0];
1455
- if (firstPendingClearMessageId !== undefined) {
1456
- if (local) {
1457
- assert(localOpMetadata !== undefined &&
1458
- isKeyEditLocalOpMetadata(localOpMetadata) &&
1459
- localOpMetadata.pendingMessageId < firstPendingClearMessageId, 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
1460
- // Remove all pendingMessageIds lower than first pendingClearMessageId.
1461
- const lowestPendingClearMessageId = firstPendingClearMessageId;
1462
- const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
1463
- if (pendingKeyMessageIdArray !== undefined) {
1464
- let index = 0;
1465
- let pendingKeyMessageId = pendingKeyMessageIdArray[index];
1466
- while (pendingKeyMessageId !== undefined &&
1467
- pendingKeyMessageId < lowestPendingClearMessageId) {
1468
- index += 1;
1469
- pendingKeyMessageId = pendingKeyMessageIdArray[index];
1470
- }
1471
- const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
1472
- if (newPendingKeyMessageId.length === 0) {
1473
- this.pendingKeys.delete(op.key);
1474
- }
1475
- else {
1476
- this.pendingKeys.set(op.key, newPendingKeyMessageId);
1477
- }
1478
- }
1479
- }
1480
- // If I have a NACK clear, we can ignore all ops.
1481
- return false;
1482
- }
1483
- const pendingKeyMessageIds = this.pendingKeys.get(op.key);
1484
- if (pendingKeyMessageIds !== undefined) {
1485
- // Found an NACK op, clear it from the directory if the latest sequence number in the directory
1486
- // match the message's and don't process the op.
1487
- if (local) {
1488
- assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
1489
- if (pendingKeyMessageIds[0] !== localOpMetadata.pendingMessageId) {
1490
- // TODO: AB#7742: Hitting this block indicates that the pending message Id received
1491
- // is not consistent with the "next" local op
1492
- this.logger.sendTelemetryEvent({
1493
- eventName: "unexpectedPendingMessage",
1494
- expectedPendingMessage: pendingKeyMessageIds[0],
1495
- actualPendingMessage: localOpMetadata.pendingMessageId,
1496
- expectedPendingMessagesLength: pendingKeyMessageIds.length,
1497
- });
1498
- }
1499
- pendingKeyMessageIds.shift();
1500
- if (pendingKeyMessageIds.length === 0) {
1501
- this.pendingKeys.delete(op.key);
1502
- }
1503
- }
1504
- return false;
1505
- }
1506
- // If we don't have a NACK op on the key, we need to process the remote ops.
1507
- return !local;
1508
- }
1509
1671
  /**
1510
1672
  * This return true if the message is for the current instance of this sub directory. As the sub directory
1511
1673
  * can be deleted and created again, then this finds if the message is for current instance of directory or not.
@@ -1536,7 +1698,7 @@ class SubDirectory extends TypedEventEmitter {
1536
1698
  if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
1537
1699
  (pendingCreateCount !== undefined && pendingCreateCount > 0)) {
1538
1700
  if (local) {
1539
- assert(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
1701
+ assert(localOpMetadata !== undefined, 0xc0d /* localOpMetadata should be defined */);
1540
1702
  if (localOpMetadata.type === "deleteSubDir") {
1541
1703
  assert(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
1542
1704
  this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
@@ -1553,7 +1715,8 @@ class SubDirectory extends TypedEventEmitter {
1553
1715
  }
1554
1716
  // If this is delete op and we have keys in this subDirectory, then we need to delete these
1555
1717
  // keys except the pending ones as they will be sequenced after this delete.
1556
- directory.clearExceptPendingKeys(local);
1718
+ directory.sequencedStorageData.clear();
1719
+ directory.emit("clear", true, directory);
1557
1720
  // In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
1558
1721
  // creators as the previous directory is getting deleted and we will initialize again when
1559
1722
  // we will receive op for the create again.
@@ -1609,68 +1772,6 @@ class SubDirectory extends TypedEventEmitter {
1609
1772
  }
1610
1773
  return !local;
1611
1774
  }
1612
- /**
1613
- * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
1614
- */
1615
- clearExceptPendingKeys(local) {
1616
- // Assuming the pendingKeys is small and the map is large
1617
- // we will get the value for the pendingKeys and clear the map
1618
- const temp = new Map();
1619
- for (const [key] of this.pendingKeys) {
1620
- const value = this._storage.get(key);
1621
- // If this key is already deleted, then we don't need to add it again.
1622
- if (value !== undefined) {
1623
- temp.set(key, value);
1624
- }
1625
- }
1626
- this.clearCore(local);
1627
- for (const [key, value] of temp.entries()) {
1628
- this.setCore(key, value, true);
1629
- }
1630
- }
1631
- /**
1632
- * Clear implementation used for both locally sourced clears as well as incoming remote clears.
1633
- * @param local - Whether the message originated from the local client
1634
- */
1635
- clearCore(local) {
1636
- this._storage.clear();
1637
- this.directory.emit("clear", local, this.directory);
1638
- }
1639
- /**
1640
- * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
1641
- * @param key - The key being deleted
1642
- * @param local - Whether the message originated from the local client
1643
- * @returns Previous local value of the key if it existed, undefined if it did not exist
1644
- */
1645
- deleteCore(key, local) {
1646
- const previousLocalValue = this._storage.get(key);
1647
- const previousValue = previousLocalValue;
1648
- const successfullyRemoved = this._storage.delete(key);
1649
- if (successfullyRemoved) {
1650
- const event = { key, path: this.absolutePath, previousValue };
1651
- this.directory.emit("valueChanged", event, local, this.directory);
1652
- const containedEvent = { key, previousValue };
1653
- this.emit("containedValueChanged", containedEvent, local, this);
1654
- }
1655
- return previousLocalValue;
1656
- }
1657
- /**
1658
- * Set implementation used for both locally sourced sets as well as incoming remote sets.
1659
- * @param key - The key being set
1660
- * @param value - The value being set
1661
- * @param local - Whether the message originated from the local client
1662
- * @returns Previous local value of the key, if any
1663
- */
1664
- setCore(key, value, local) {
1665
- const previousLocalValue = this._storage.get(key);
1666
- const previousValue = previousLocalValue;
1667
- this._storage.set(key, value);
1668
- const event = { key, path: this.absolutePath, previousValue };
1669
- this.directory.emit("valueChanged", event, local, this.directory);
1670
- const containedEvent = { key, previousValue };
1671
- this.emit("containedValueChanged", containedEvent, local, this);
1672
- return previousLocalValue;
1673
- }
1674
1775
  /**
1675
1776
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1676
1777
  * @param subdirName - The name of the subdirectory being created