@fluidframework/map 1.1.0 → 1.2.0-77818

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
@@ -387,6 +387,17 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
387
387
  handler.process(op, local, localOpMetadata);
388
388
  }
389
389
  }
390
+ /**
391
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
392
+ * @internal
393
+ */
394
+ rollback(content, localOpMetadata) {
395
+ const op = content;
396
+ const subdir = this.getWorkingDirectory(op.path);
397
+ if (subdir) {
398
+ subdir.rollback(op, localOpMetadata);
399
+ }
400
+ }
390
401
  /**
391
402
  * Converts the given relative path to absolute against the root.
392
403
  * @param relativePath - The path to convert
@@ -422,8 +433,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
422
433
  submit: (op, localOpMetadata) => {
423
434
  const subdir = this.getWorkingDirectory(op.path);
424
435
  if (subdir) {
425
- // We don't reuse the metadata but send a new one on each submit.
426
- subdir.submitClearMessage(op);
436
+ subdir.resubmitClearMessage(op, localOpMetadata);
427
437
  }
428
438
  },
429
439
  });
@@ -437,8 +447,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
437
447
  submit: (op, localOpMetadata) => {
438
448
  const subdir = this.getWorkingDirectory(op.path);
439
449
  if (subdir) {
440
- // We don't reuse the metadata but send a new one on each submit.
441
- subdir.submitKeyMessage(op);
450
+ subdir.resubmitKeyMessage(op, localOpMetadata);
442
451
  }
443
452
  },
444
453
  });
@@ -453,8 +462,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
453
462
  submit: (op, localOpMetadata) => {
454
463
  const subdir = this.getWorkingDirectory(op.path);
455
464
  if (subdir) {
456
- // We don't reuse the metadata but send a new one on each submit.
457
- subdir.submitKeyMessage(op);
465
+ subdir.resubmitKeyMessage(op, localOpMetadata);
458
466
  }
459
467
  },
460
468
  });
@@ -469,7 +477,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
469
477
  const parentSubdir = this.getWorkingDirectory(op.path);
470
478
  if (parentSubdir) {
471
479
  // We don't reuse the metadata but send a new one on each submit.
472
- parentSubdir.submitSubDirectoryMessage(op);
480
+ parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
473
481
  }
474
482
  },
475
483
  });
@@ -484,7 +492,7 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
484
492
  const parentSubdir = this.getWorkingDirectory(op.path);
485
493
  if (parentSubdir) {
486
494
  // We don't reuse the metadata but send a new one on each submit.
487
- parentSubdir.submitSubDirectoryMessage(op);
495
+ parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
488
496
  }
489
497
  },
490
498
  });
@@ -553,6 +561,24 @@ class SharedDirectory extends shared_object_base_1.SharedObject {
553
561
  }
554
562
  }
555
563
  exports.SharedDirectory = SharedDirectory;
564
+ function isKeyEditLocalOpMetadata(metadata) {
565
+ return metadata !== undefined && typeof metadata.pendingMessageId === "number" && metadata.type === "edit";
566
+ }
567
+ function isClearLocalOpMetadata(metadata) {
568
+ return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number" &&
569
+ typeof metadata.previousStorage === "object";
570
+ }
571
+ function isSubDirLocalOpMetadata(metadata) {
572
+ return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
573
+ ((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
574
+ metadata.type === "deleteSubDir");
575
+ }
576
+ function isDirectoryLocalOpMetadata(metadata) {
577
+ return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
578
+ (metadata.type === "edit" || metadata.type === "deleteSubDir" ||
579
+ (metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
580
+ (metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"));
581
+ }
556
582
  /**
557
583
  * Node of the directory tree.
558
584
  * @sealed
@@ -572,9 +598,9 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
572
598
  this.serializer = serializer;
573
599
  this.absolutePath = absolutePath;
574
600
  /**
575
- * Tells if the sub directory is disposed or not.
601
+ * Tells if the sub directory is deleted or not.
576
602
  */
577
- this._disposed = false;
603
+ this._deleted = false;
578
604
  /**
579
605
  * String representation for the class.
580
606
  */
@@ -600,20 +626,25 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
600
626
  */
601
627
  this.pendingMessageId = -1;
602
628
  /**
603
- * If a clear has been performed locally but not yet ack'd from the server, then this stores the pending id
604
- * of that clear operation. Otherwise, is -1.
629
+ * The pending ids of any clears that have been performed locally but not yet ack'd from the server
605
630
  */
606
- this.pendingClearMessageId = -1;
631
+ this.pendingClearMessageIds = [];
607
632
  }
608
633
  dispose(error) {
609
- this._disposed = true;
634
+ this._deleted = true;
610
635
  this.emit("disposed", this);
611
636
  }
637
+ /**
638
+ * Unmark the deleted property when rolling back delete.
639
+ */
640
+ undispose() {
641
+ this._deleted = false;
642
+ }
612
643
  get disposed() {
613
- return this._disposed;
644
+ return this._deleted;
614
645
  }
615
646
  throwIfDisposed() {
616
- if (this._disposed) {
647
+ if (this._deleted) {
617
648
  throw new container_utils_1.UsageError("Cannot access Disposed subDirectory");
618
649
  }
619
650
  }
@@ -647,7 +678,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
647
678
  const localValue = this.directory.localValueMaker.fromInMemory(value);
648
679
  const serializableValue = (0, localValues_1.makeSerializable)(localValue, this.serializer, this.directory.handle);
649
680
  // Set the value locally.
650
- this.setCore(key, localValue, true);
681
+ const previousValue = this.setCore(key, localValue, true);
651
682
  // If we are not attached, don't submit the op.
652
683
  if (!this.directory.isAttached()) {
653
684
  return this;
@@ -658,7 +689,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
658
689
  type: "set",
659
690
  value: serializableValue,
660
691
  };
661
- this.submitKeyMessage(op);
692
+ this.submitKeyMessage(op, previousValue);
662
693
  return this;
663
694
  }
664
695
  /**
@@ -680,7 +711,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
680
711
  throw new Error(`SubDirectory name may not contain ${posix.sep}`);
681
712
  }
682
713
  // Create the sub directory locally first.
683
- this.createSubDirectoryCore(subdirName, true);
714
+ const isNew = this.createSubDirectoryCore(subdirName, true);
684
715
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
685
716
  const subDir = this._subdirectories.get(subdirName);
686
717
  // If we are not attached, don't submit the op.
@@ -692,7 +723,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
692
723
  subdirName,
693
724
  type: "createSubDirectory",
694
725
  };
695
- this.submitSubDirectoryMessage(op);
726
+ this.submitCreateSubDirectoryMessage(op, !isNew);
696
727
  return subDir;
697
728
  }
698
729
  /**
@@ -715,18 +746,18 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
715
746
  deleteSubDirectory(subdirName) {
716
747
  this.throwIfDisposed();
717
748
  // Delete the sub directory locally first.
718
- const successfullyRemoved = this.deleteSubDirectoryCore(subdirName, true);
749
+ const subDir = this.deleteSubDirectoryCore(subdirName, true);
719
750
  // If we are not attached, don't submit the op.
720
751
  if (!this.directory.isAttached()) {
721
- return successfullyRemoved;
752
+ return subDir !== undefined;
722
753
  }
723
754
  const op = {
724
755
  path: this.absolutePath,
725
756
  subdirName,
726
757
  type: "deleteSubDirectory",
727
758
  };
728
- this.submitSubDirectoryMessage(op);
729
- return successfullyRemoved;
759
+ this.submitDeleteSubDirectoryMessage(op, subDir);
760
+ return subDir !== undefined;
730
761
  }
731
762
  /**
732
763
  * {@inheritDoc IDirectory.subdirectories}
@@ -750,35 +781,36 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
750
781
  delete(key) {
751
782
  this.throwIfDisposed();
752
783
  // Delete the key locally first.
753
- const successfullyRemoved = this.deleteCore(key, true);
784
+ const previousValue = this.deleteCore(key, true);
754
785
  // If we are not attached, don't submit the op.
755
786
  if (!this.directory.isAttached()) {
756
- return successfullyRemoved;
787
+ return previousValue !== undefined;
757
788
  }
758
789
  const op = {
759
790
  key,
760
791
  path: this.absolutePath,
761
792
  type: "delete",
762
793
  };
763
- this.submitKeyMessage(op);
764
- return successfullyRemoved;
794
+ this.submitKeyMessage(op, previousValue);
795
+ return previousValue !== undefined;
765
796
  }
766
797
  /**
767
798
  * Deletes all keys from within this IDirectory.
768
799
  */
769
800
  clear() {
770
801
  this.throwIfDisposed();
771
- // Clear the data locally first.
772
- this.clearCore(true);
773
802
  // If we are not attached, don't submit the op.
774
803
  if (!this.directory.isAttached()) {
804
+ this.clearCore(true);
775
805
  return;
776
806
  }
807
+ const copy = new Map(this._storage);
808
+ this.clearCore(true);
777
809
  const op = {
778
810
  path: this.absolutePath,
779
811
  type: "clear",
780
812
  };
781
- this.submitClearMessage(op);
813
+ this.submitClearMessage(op, copy);
782
814
  }
783
815
  /**
784
816
  * Issue a callback on each entry under this IDirectory.
@@ -873,15 +905,12 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
873
905
  processClearMessage(op, local, localOpMetadata) {
874
906
  this.throwIfDisposed();
875
907
  if (local) {
876
- (0, common_utils_1.assert)(localOpMetadata !== undefined, 0x00f /* pendingMessageId is missing from the local client's operation */);
877
- const pendingMessageId = localOpMetadata;
878
- if (this.pendingClearMessageId === pendingMessageId) {
879
- this.pendingClearMessageId = -1;
880
- }
908
+ (0, common_utils_1.assert)(isClearLocalOpMetadata(localOpMetadata), 0x00f /* `pendingMessageId is missing from the local client's ${op.type} operation` */);
909
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
910
+ (0, common_utils_1.assert)(pendingClearMessageId === localOpMetadata.pendingMessageId, "pendingMessageId does not match");
881
911
  return;
882
912
  }
883
913
  this.clearExceptPendingKeys();
884
- this.directory.emit("clear", local, this.directory);
885
914
  }
886
915
  /**
887
916
  * Process a delete operation.
@@ -929,7 +958,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
929
958
  */
930
959
  processCreateSubDirectoryMessage(op, local, localOpMetadata) {
931
960
  this.throwIfDisposed();
932
- if (!this.needProcessSubDirectoryOperations(op, local, localOpMetadata)) {
961
+ if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
933
962
  return;
934
963
  }
935
964
  this.createSubDirectoryCore(op.subdirName, local);
@@ -945,7 +974,7 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
945
974
  */
946
975
  processDeleteSubDirectoryMessage(op, local, localOpMetadata) {
947
976
  this.throwIfDisposed();
948
- if (!this.needProcessSubDirectoryOperations(op, local, localOpMetadata)) {
977
+ if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
949
978
  return;
950
979
  }
951
980
  this.deleteSubDirectoryCore(op.subdirName, local);
@@ -953,35 +982,139 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
953
982
  /**
954
983
  * Submit a clear operation.
955
984
  * @param op - The operation
956
- * @internal
957
985
  */
958
- submitClearMessage(op) {
986
+ submitClearMessage(op, previousValue) {
959
987
  this.throwIfDisposed();
988
+ const pendingMsgId = ++this.pendingMessageId;
989
+ this.pendingClearMessageIds.push(pendingMsgId);
990
+ const metadata = {
991
+ type: "clear",
992
+ pendingMessageId: pendingMsgId,
993
+ previousStorage: previousValue,
994
+ };
995
+ this.directory.submitDirectoryMessage(op, metadata);
996
+ }
997
+ /**
998
+ * Resubmit a clear operation.
999
+ * @param op - The operation
1000
+ * @internal
1001
+ */
1002
+ resubmitClearMessage(op, localOpMetadata) {
1003
+ (0, common_utils_1.assert)(isClearLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata for clear");
1004
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1005
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
1006
+ (0, common_utils_1.assert)(pendingClearMessageId === localOpMetadata.pendingMessageId, "pendingMessageId does not match");
1007
+ this.submitClearMessage(op, localOpMetadata.previousStorage);
1008
+ }
1009
+ /**
1010
+ * Get a new pending message id for the op and cache it to track the pending op
1011
+ */
1012
+ getKeyMessageId(op) {
1013
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
960
1014
  const pendingMessageId = ++this.pendingMessageId;
961
- this.directory.submitDirectoryMessage(op, pendingMessageId);
962
- this.pendingClearMessageId = pendingMessageId;
1015
+ const pendingMessageIds = this.pendingKeys.get(op.key);
1016
+ if (pendingMessageIds !== undefined) {
1017
+ pendingMessageIds.push(pendingMessageId);
1018
+ }
1019
+ else {
1020
+ this.pendingKeys.set(op.key, [pendingMessageId]);
1021
+ }
1022
+ return pendingMessageId;
963
1023
  }
964
1024
  /**
965
1025
  * Submit a key operation.
966
1026
  * @param op - The operation
1027
+ * @param previousValue - The value of the key before this op
1028
+ */
1029
+ submitKeyMessage(op, previousValue) {
1030
+ this.throwIfDisposed();
1031
+ const pendingMessageId = this.getKeyMessageId(op);
1032
+ const localMetadata = { type: "edit", pendingMessageId, previousValue };
1033
+ this.directory.submitDirectoryMessage(op, localMetadata);
1034
+ }
1035
+ /**
1036
+ * Submit a key message to remote clients based on a previous submit.
1037
+ * @param op - The map key message
1038
+ * @param localOpMetadata - Metadata from the previous submit
967
1039
  * @internal
968
1040
  */
969
- submitKeyMessage(op) {
1041
+ resubmitKeyMessage(op, localOpMetadata) {
1042
+ (0, common_utils_1.assert)(isKeyEditLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata in submit");
1043
+ // clear the old pending message id
1044
+ const pendingMessageIds = this.pendingKeys.get(op.key);
1045
+ (0, common_utils_1.assert)(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
1046
+ pendingMessageIds.shift();
1047
+ if (pendingMessageIds.length === 0) {
1048
+ this.pendingKeys.delete(op.key);
1049
+ }
1050
+ this.submitKeyMessage(op, localOpMetadata.previousValue);
1051
+ }
1052
+ /**
1053
+ * Get a new pending message id for the op and cache it to track the pending op
1054
+ */
1055
+ getSubDirMessageId(op) {
1056
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1057
+ const newMessageId = ++this.pendingMessageId;
1058
+ const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1059
+ if (pendingMessageIds !== undefined) {
1060
+ pendingMessageIds.push(newMessageId);
1061
+ }
1062
+ else {
1063
+ this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
1064
+ }
1065
+ return newMessageId;
1066
+ }
1067
+ /**
1068
+ * Submit a create subdirectory operation.
1069
+ * @param op - The operation
1070
+ * @param prevExisted - Whether the subdirectory existed before the op
1071
+ */
1072
+ submitCreateSubDirectoryMessage(op, prevExisted) {
970
1073
  this.throwIfDisposed();
971
- const pendingMessageId = ++this.pendingMessageId;
972
- this.directory.submitDirectoryMessage(op, pendingMessageId);
973
- this.pendingKeys.set(op.key, pendingMessageId);
1074
+ const newMessageId = this.getSubDirMessageId(op);
1075
+ const localOpMetadata = {
1076
+ type: "createSubDir",
1077
+ pendingMessageId: newMessageId,
1078
+ previouslyExisted: prevExisted,
1079
+ };
1080
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
974
1081
  }
975
1082
  /**
976
- * Submit a subdirectory operation.
1083
+ * Submit a delete subdirectory operation.
977
1084
  * @param op - The operation
978
- * @internal
1085
+ * @param subDir - Any subdirectory deleted by the op
979
1086
  */
980
- submitSubDirectoryMessage(op) {
1087
+ submitDeleteSubDirectoryMessage(op, subDir) {
981
1088
  this.throwIfDisposed();
982
- const pendingMessageId = ++this.pendingMessageId;
983
- this.directory.submitDirectoryMessage(op, pendingMessageId);
984
- this.pendingSubDirectories.set(op.subdirName, pendingMessageId);
1089
+ const newMessageId = this.getSubDirMessageId(op);
1090
+ const localOpMetadata = {
1091
+ type: "deleteSubDir",
1092
+ pendingMessageId: newMessageId,
1093
+ subDirectory: subDir,
1094
+ };
1095
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
1096
+ }
1097
+ /**
1098
+ * Submit a subdirectory operation again
1099
+ * @param op - The operation
1100
+ * @param localOpMetadata - metadata submitted with the op originally
1101
+ * @internal
1102
+ */
1103
+ resubmitSubDirectoryMessage(op, localOpMetadata) {
1104
+ (0, common_utils_1.assert)(isSubDirLocalOpMetadata(localOpMetadata), "Invalid localOpMetadata for sub directory op");
1105
+ // clear the old pending message id
1106
+ const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1107
+ (0, common_utils_1.assert)(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
1108
+ pendingMessageIds.shift();
1109
+ if (pendingMessageIds.length === 0) {
1110
+ this.pendingSubDirectories.delete(op.subdirName);
1111
+ }
1112
+ if (localOpMetadata.type === "createSubDir") {
1113
+ this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
1114
+ }
1115
+ else {
1116
+ this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1117
+ }
985
1118
  }
986
1119
  /**
987
1120
  * Get the storage of this subdirectory in a serializable format, to be used in snapshotting.
@@ -1028,6 +1161,67 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1028
1161
  this.throwIfDisposed();
1029
1162
  return this._storage.get(key);
1030
1163
  }
1164
+ /**
1165
+ * Remove the pendingMessageId from the map tracking it on rollback
1166
+ * @param map - map tracking the pending messages
1167
+ * @param key - key of the edit in the op
1168
+ */
1169
+ rollbackPendingMessageId(map, key, pendingMessageId) {
1170
+ const pendingMessageIds = map.get(key);
1171
+ const lastPendingMessageId = pendingMessageIds === null || pendingMessageIds === void 0 ? void 0 : pendingMessageIds.pop();
1172
+ if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
1173
+ throw new Error("Rollback op does not match last pending");
1174
+ }
1175
+ if (pendingMessageIds.length === 0) {
1176
+ map.delete(key);
1177
+ }
1178
+ }
1179
+ /**
1180
+ * Rollback a local op
1181
+ * @param op - The operation to rollback
1182
+ * @param localOpMetadata - The local metadata associated with the op.
1183
+ */
1184
+ rollback(op, localOpMetadata) {
1185
+ if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
1186
+ throw new Error("Invalid localOpMetadata");
1187
+ }
1188
+ if (op.type === "clear" && localOpMetadata.type === "clear") {
1189
+ localOpMetadata.previousStorage.forEach((localValue, key) => {
1190
+ this.setCore(key, localValue, true);
1191
+ });
1192
+ const lastPendingClearId = this.pendingClearMessageIds.pop();
1193
+ if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
1194
+ throw new Error("Rollback op does match last clear");
1195
+ }
1196
+ }
1197
+ else if ((op.type === "delete" || op.type === "set") && localOpMetadata.type === "edit") {
1198
+ if (localOpMetadata.previousValue === undefined) {
1199
+ this.deleteCore(op.key, true);
1200
+ }
1201
+ else {
1202
+ this.setCore(op.key, localOpMetadata.previousValue, true);
1203
+ }
1204
+ this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
1205
+ }
1206
+ else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1207
+ if (!localOpMetadata.previouslyExisted) {
1208
+ this.deleteSubDirectoryCore(op.subdirName, true);
1209
+ }
1210
+ this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1211
+ }
1212
+ else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
1213
+ if (localOpMetadata.subDirectory !== undefined) {
1214
+ this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
1215
+ // don't need to register events because deleting never unregistered
1216
+ this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
1217
+ this.emit("subDirectoryCreated", op.subdirName, true, this);
1218
+ }
1219
+ this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1220
+ }
1221
+ else {
1222
+ throw new Error("Unsupported op for rollback");
1223
+ }
1224
+ }
1031
1225
  /**
1032
1226
  * Converts the given relative path into an absolute path.
1033
1227
  * @param path - Relative path to convert
@@ -1040,16 +1234,16 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1040
1234
  * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1041
1235
  * not process the incoming operation.
1042
1236
  * @param op - Operation to check
1043
- * @param local - Whether the message originated from the local client
1044
- * @param message - The message
1045
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1046
- * For messages from a remote client, this will be undefined.
1237
+ * @param local - Whether the operation originated from the local client
1238
+ * @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
1239
+ * For ops from a remote client, this will be undefined.
1047
1240
  * @returns True if the operation should be processed, false otherwise
1048
1241
  */
1049
1242
  needProcessStorageOperation(op, local, localOpMetadata) {
1050
- if (this.pendingClearMessageId !== -1) {
1243
+ if (this.pendingClearMessageIds.length > 0) {
1051
1244
  if (local) {
1052
- (0, common_utils_1.assert)(localOpMetadata !== undefined && localOpMetadata < this.pendingClearMessageId, 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
1245
+ (0, common_utils_1.assert)(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata) &&
1246
+ localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0], 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
1053
1247
  }
1054
1248
  // If I have a NACK clear, we can ignore all ops.
1055
1249
  return false;
@@ -1059,9 +1253,11 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1059
1253
  // Found an NACK op, clear it from the directory if the latest sequence number in the directory
1060
1254
  // match the message's and don't process the op.
1061
1255
  if (local) {
1062
- (0, common_utils_1.assert)(localOpMetadata !== undefined, 0x011 /* pendingMessageId is missing from the local client's operation */);
1063
- const pendingMessageId = localOpMetadata;
1064
- if (pendingKeyMessageId === pendingMessageId) {
1256
+ (0, common_utils_1.assert)(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
1257
+ const pendingMessageIds = this.pendingKeys.get(op.key);
1258
+ (0, common_utils_1.assert)(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
1259
+ pendingMessageIds.shift();
1260
+ if (pendingMessageIds.length === 0) {
1065
1261
  this.pendingKeys.delete(op.key);
1066
1262
  }
1067
1263
  }
@@ -1080,13 +1276,15 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1080
1276
  * For messages from a remote client, this will be undefined.
1081
1277
  * @returns True if the operation should be processed, false otherwise
1082
1278
  */
1083
- needProcessSubDirectoryOperations(op, local, localOpMetadata) {
1279
+ needProcessSubDirectoryOperation(op, local, localOpMetadata) {
1084
1280
  const pendingSubDirectoryMessageId = this.pendingSubDirectories.get(op.subdirName);
1085
1281
  if (pendingSubDirectoryMessageId !== undefined) {
1086
1282
  if (local) {
1087
- (0, common_utils_1.assert)(localOpMetadata !== undefined, 0x012 /* pendingMessageId is missing from the local client's operation */);
1088
- const pendingMessageId = localOpMetadata;
1089
- if (pendingSubDirectoryMessageId === pendingMessageId) {
1283
+ (0, common_utils_1.assert)(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
1284
+ const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1285
+ (0, common_utils_1.assert)(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, "Unexpected pending message received");
1286
+ pendingMessageIds.shift();
1287
+ if (pendingMessageIds.length === 0) {
1090
1288
  this.pendingSubDirectories.delete(op.subdirName);
1091
1289
  }
1092
1290
  }
@@ -1105,9 +1303,9 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1105
1303
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1106
1304
  temp.set(key, this._storage.get(key));
1107
1305
  });
1108
- this._storage.clear();
1306
+ this.clearCore(false);
1109
1307
  temp.forEach((value, key, map) => {
1110
- this._storage.set(key, value);
1308
+ this.setCore(key, value, true);
1111
1309
  });
1112
1310
  }
1113
1311
  /**
@@ -1123,11 +1321,11 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1123
1321
  * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
1124
1322
  * @param key - The key being deleted
1125
1323
  * @param local - Whether the message originated from the local client
1126
- * @param op - The message if from a remote delete, or null if from a local delete
1127
- * @returns True if the key existed and was deleted, false if it did not exist
1324
+ * @returns Previous local value of the key if it existed, undefined if it did not exist
1128
1325
  */
1129
1326
  deleteCore(key, local) {
1130
- const previousValue = this.get(key);
1327
+ const previousLocalValue = this._storage.get(key);
1328
+ const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
1131
1329
  const successfullyRemoved = this._storage.delete(key);
1132
1330
  if (successfullyRemoved) {
1133
1331
  const event = { key, path: this.absolutePath, previousValue };
@@ -1135,27 +1333,30 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1135
1333
  const containedEvent = { key, previousValue };
1136
1334
  this.emit("containedValueChanged", containedEvent, local, this);
1137
1335
  }
1138
- return successfullyRemoved;
1336
+ return previousLocalValue;
1139
1337
  }
1140
1338
  /**
1141
1339
  * Set implementation used for both locally sourced sets as well as incoming remote sets.
1142
1340
  * @param key - The key being set
1143
1341
  * @param value - The value being set
1144
1342
  * @param local - Whether the message originated from the local client
1145
- * @param op - The message if from a remote set, or null if from a local set
1343
+ * @returns Previous local value of the key, if any
1146
1344
  */
1147
1345
  setCore(key, value, local) {
1148
- const previousValue = this.get(key);
1346
+ const previousLocalValue = this._storage.get(key);
1347
+ const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
1149
1348
  this._storage.set(key, value);
1150
1349
  const event = { key, path: this.absolutePath, previousValue };
1151
1350
  this.directory.emit("valueChanged", event, local, this.directory);
1152
1351
  const containedEvent = { key, previousValue };
1153
1352
  this.emit("containedValueChanged", containedEvent, local, this);
1353
+ return previousLocalValue;
1154
1354
  }
1155
1355
  /**
1156
1356
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1157
1357
  * @param subdirName - The name of the subdirectory being created
1158
1358
  * @param local - Whether the message originated from the local client
1359
+ * @returns - True if is newly created, false if it already existed.
1159
1360
  */
1160
1361
  createSubDirectoryCore(subdirName, local) {
1161
1362
  if (!this._subdirectories.has(subdirName)) {
@@ -1164,7 +1365,9 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1164
1365
  this.registerEventsOnSubDirectory(subDir, subdirName);
1165
1366
  this._subdirectories.set(subdirName, subDir);
1166
1367
  this.emit("subDirectoryCreated", subdirName, local, this);
1368
+ return true;
1167
1369
  }
1370
+ return false;
1168
1371
  }
1169
1372
  registerEventsOnSubDirectory(subDirectory, subDirName) {
1170
1373
  subDirectory.on("subDirectoryCreated", (relativePath, local) => {
@@ -1178,18 +1381,17 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1178
1381
  * Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1179
1382
  * @param subdirName - The name of the subdirectory being deleted
1180
1383
  * @param local - Whether the message originated from the local client
1181
- * @param op - The message if from a remote delete, or null if from a local delete
1182
1384
  */
1183
1385
  deleteSubDirectoryCore(subdirName, local) {
1184
- const previousValue = this.getSubDirectory(subdirName);
1386
+ const previousValue = this._subdirectories.get(subdirName);
1185
1387
  // This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
1186
- // Might want to consider cleaning out the structure more exhaustively though?
1187
- const successfullyRemoved = this._subdirectories.delete(subdirName);
1388
+ // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
1188
1389
  if (previousValue !== undefined) {
1390
+ this._subdirectories.delete(subdirName);
1189
1391
  this.disposeSubDirectoryTree(previousValue);
1190
1392
  this.emit("subDirectoryDeleted", subdirName, local, this);
1191
1393
  }
1192
- return successfullyRemoved;
1394
+ return previousValue;
1193
1395
  }
1194
1396
  disposeSubDirectoryTree(directory) {
1195
1397
  if (!directory) {
@@ -1204,5 +1406,12 @@ class SubDirectory extends common_utils_1.TypedEventEmitter {
1204
1406
  directory.dispose();
1205
1407
  }
1206
1408
  }
1409
+ undeleteSubDirectoryTree(directory) {
1410
+ // Restore deleted subdirectory tree. This will unmark "deleted" from the subdirectories from bottom to top.
1411
+ for (const [_, subDirectory] of this._subdirectories.entries()) {
1412
+ this.undeleteSubDirectoryTree(subDirectory);
1413
+ }
1414
+ directory.undispose();
1415
+ }
1207
1416
  }
1208
1417
  //# sourceMappingURL=directory.js.map