@fluidframework/map 2.53.0-350190 → 2.53.1

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