@fluidframework/map 1.1.0 → 1.2.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
@@ -68,7 +68,7 @@ DirectoryFactory.Attributes = {
68
68
  * SubDirectories can be retrieved for use as working directories.
69
69
  *
70
70
  * @example
71
- * ```ts
71
+ * ```typescript
72
72
  * mySharedDirectory.createSubDirectory("a").createSubDirectory("b").createSubDirectory("c").set("foo", val1);
73
73
  * const mySubDir = mySharedDirectory.getWorkingDirectory("/a/b/c");
74
74
  * mySubDir.get("foo"); // returns val1
@@ -364,6 +364,17 @@ export class SharedDirectory extends SharedObject {
364
364
  handler.process(op, local, localOpMetadata);
365
365
  }
366
366
  }
367
+ /**
368
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
369
+ * @internal
370
+ */
371
+ rollback(content, localOpMetadata) {
372
+ const op = content;
373
+ const subdir = this.getWorkingDirectory(op.path);
374
+ if (subdir) {
375
+ subdir.rollback(op, localOpMetadata);
376
+ }
377
+ }
367
378
  /**
368
379
  * Converts the given relative path to absolute against the root.
369
380
  * @param relativePath - The path to convert
@@ -399,8 +410,7 @@ export class SharedDirectory extends SharedObject {
399
410
  submit: (op, localOpMetadata) => {
400
411
  const subdir = this.getWorkingDirectory(op.path);
401
412
  if (subdir) {
402
- // We don't reuse the metadata but send a new one on each submit.
403
- subdir.submitClearMessage(op);
413
+ subdir.resubmitClearMessage(op, localOpMetadata);
404
414
  }
405
415
  },
406
416
  });
@@ -414,8 +424,7 @@ export class SharedDirectory extends SharedObject {
414
424
  submit: (op, localOpMetadata) => {
415
425
  const subdir = this.getWorkingDirectory(op.path);
416
426
  if (subdir) {
417
- // We don't reuse the metadata but send a new one on each submit.
418
- subdir.submitKeyMessage(op);
427
+ subdir.resubmitKeyMessage(op, localOpMetadata);
419
428
  }
420
429
  },
421
430
  });
@@ -430,8 +439,7 @@ export class SharedDirectory extends SharedObject {
430
439
  submit: (op, localOpMetadata) => {
431
440
  const subdir = this.getWorkingDirectory(op.path);
432
441
  if (subdir) {
433
- // We don't reuse the metadata but send a new one on each submit.
434
- subdir.submitKeyMessage(op);
442
+ subdir.resubmitKeyMessage(op, localOpMetadata);
435
443
  }
436
444
  },
437
445
  });
@@ -446,7 +454,7 @@ export class SharedDirectory extends SharedObject {
446
454
  const parentSubdir = this.getWorkingDirectory(op.path);
447
455
  if (parentSubdir) {
448
456
  // We don't reuse the metadata but send a new one on each submit.
449
- parentSubdir.submitSubDirectoryMessage(op);
457
+ parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
450
458
  }
451
459
  },
452
460
  });
@@ -461,7 +469,7 @@ export class SharedDirectory extends SharedObject {
461
469
  const parentSubdir = this.getWorkingDirectory(op.path);
462
470
  if (parentSubdir) {
463
471
  // We don't reuse the metadata but send a new one on each submit.
464
- parentSubdir.submitSubDirectoryMessage(op);
472
+ parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
465
473
  }
466
474
  },
467
475
  });
@@ -529,6 +537,24 @@ export class SharedDirectory extends SharedObject {
529
537
  return builder.getSummaryTree();
530
538
  }
531
539
  }
540
+ function isKeyEditLocalOpMetadata(metadata) {
541
+ return metadata !== undefined && typeof metadata.pendingMessageId === "number" && metadata.type === "edit";
542
+ }
543
+ function isClearLocalOpMetadata(metadata) {
544
+ return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number" &&
545
+ typeof metadata.previousStorage === "object";
546
+ }
547
+ function isSubDirLocalOpMetadata(metadata) {
548
+ return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
549
+ ((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
550
+ metadata.type === "deleteSubDir");
551
+ }
552
+ function isDirectoryLocalOpMetadata(metadata) {
553
+ return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
554
+ (metadata.type === "edit" || metadata.type === "deleteSubDir" ||
555
+ (metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
556
+ (metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"));
557
+ }
532
558
  /**
533
559
  * Node of the directory tree.
534
560
  * @sealed
@@ -548,9 +574,9 @@ class SubDirectory extends TypedEventEmitter {
548
574
  this.serializer = serializer;
549
575
  this.absolutePath = absolutePath;
550
576
  /**
551
- * Tells if the sub directory is disposed or not.
577
+ * Tells if the sub directory is deleted or not.
552
578
  */
553
- this._disposed = false;
579
+ this._deleted = false;
554
580
  /**
555
581
  * String representation for the class.
556
582
  */
@@ -576,20 +602,25 @@ class SubDirectory extends TypedEventEmitter {
576
602
  */
577
603
  this.pendingMessageId = -1;
578
604
  /**
579
- * If a clear has been performed locally but not yet ack'd from the server, then this stores the pending id
580
- * of that clear operation. Otherwise, is -1.
605
+ * The pending ids of any clears that have been performed locally but not yet ack'd from the server
581
606
  */
582
- this.pendingClearMessageId = -1;
607
+ this.pendingClearMessageIds = [];
583
608
  }
584
609
  dispose(error) {
585
- this._disposed = true;
610
+ this._deleted = true;
586
611
  this.emit("disposed", this);
587
612
  }
613
+ /**
614
+ * Unmark the deleted property when rolling back delete.
615
+ */
616
+ undispose() {
617
+ this._deleted = false;
618
+ }
588
619
  get disposed() {
589
- return this._disposed;
620
+ return this._deleted;
590
621
  }
591
622
  throwIfDisposed() {
592
- if (this._disposed) {
623
+ if (this._deleted) {
593
624
  throw new UsageError("Cannot access Disposed subDirectory");
594
625
  }
595
626
  }
@@ -623,7 +654,7 @@ class SubDirectory extends TypedEventEmitter {
623
654
  const localValue = this.directory.localValueMaker.fromInMemory(value);
624
655
  const serializableValue = makeSerializable(localValue, this.serializer, this.directory.handle);
625
656
  // Set the value locally.
626
- this.setCore(key, localValue, true);
657
+ const previousValue = this.setCore(key, localValue, true);
627
658
  // If we are not attached, don't submit the op.
628
659
  if (!this.directory.isAttached()) {
629
660
  return this;
@@ -634,7 +665,7 @@ class SubDirectory extends TypedEventEmitter {
634
665
  type: "set",
635
666
  value: serializableValue,
636
667
  };
637
- this.submitKeyMessage(op);
668
+ this.submitKeyMessage(op, previousValue);
638
669
  return this;
639
670
  }
640
671
  /**
@@ -656,7 +687,7 @@ class SubDirectory extends TypedEventEmitter {
656
687
  throw new Error(`SubDirectory name may not contain ${posix.sep}`);
657
688
  }
658
689
  // Create the sub directory locally first.
659
- this.createSubDirectoryCore(subdirName, true);
690
+ const isNew = this.createSubDirectoryCore(subdirName, true);
660
691
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
661
692
  const subDir = this._subdirectories.get(subdirName);
662
693
  // If we are not attached, don't submit the op.
@@ -668,7 +699,7 @@ class SubDirectory extends TypedEventEmitter {
668
699
  subdirName,
669
700
  type: "createSubDirectory",
670
701
  };
671
- this.submitSubDirectoryMessage(op);
702
+ this.submitCreateSubDirectoryMessage(op, !isNew);
672
703
  return subDir;
673
704
  }
674
705
  /**
@@ -691,18 +722,18 @@ class SubDirectory extends TypedEventEmitter {
691
722
  deleteSubDirectory(subdirName) {
692
723
  this.throwIfDisposed();
693
724
  // Delete the sub directory locally first.
694
- const successfullyRemoved = this.deleteSubDirectoryCore(subdirName, true);
725
+ const subDir = this.deleteSubDirectoryCore(subdirName, true);
695
726
  // If we are not attached, don't submit the op.
696
727
  if (!this.directory.isAttached()) {
697
- return successfullyRemoved;
728
+ return subDir !== undefined;
698
729
  }
699
730
  const op = {
700
731
  path: this.absolutePath,
701
732
  subdirName,
702
733
  type: "deleteSubDirectory",
703
734
  };
704
- this.submitSubDirectoryMessage(op);
705
- return successfullyRemoved;
735
+ this.submitDeleteSubDirectoryMessage(op, subDir);
736
+ return subDir !== undefined;
706
737
  }
707
738
  /**
708
739
  * {@inheritDoc IDirectory.subdirectories}
@@ -726,35 +757,36 @@ class SubDirectory extends TypedEventEmitter {
726
757
  delete(key) {
727
758
  this.throwIfDisposed();
728
759
  // Delete the key locally first.
729
- const successfullyRemoved = this.deleteCore(key, true);
760
+ const previousValue = this.deleteCore(key, true);
730
761
  // If we are not attached, don't submit the op.
731
762
  if (!this.directory.isAttached()) {
732
- return successfullyRemoved;
763
+ return previousValue !== undefined;
733
764
  }
734
765
  const op = {
735
766
  key,
736
767
  path: this.absolutePath,
737
768
  type: "delete",
738
769
  };
739
- this.submitKeyMessage(op);
740
- return successfullyRemoved;
770
+ this.submitKeyMessage(op, previousValue);
771
+ return previousValue !== undefined;
741
772
  }
742
773
  /**
743
774
  * Deletes all keys from within this IDirectory.
744
775
  */
745
776
  clear() {
746
777
  this.throwIfDisposed();
747
- // Clear the data locally first.
748
- this.clearCore(true);
749
778
  // If we are not attached, don't submit the op.
750
779
  if (!this.directory.isAttached()) {
780
+ this.clearCore(true);
751
781
  return;
752
782
  }
783
+ const copy = new Map(this._storage);
784
+ this.clearCore(true);
753
785
  const op = {
754
786
  path: this.absolutePath,
755
787
  type: "clear",
756
788
  };
757
- this.submitClearMessage(op);
789
+ this.submitClearMessage(op, copy);
758
790
  }
759
791
  /**
760
792
  * Issue a callback on each entry under this IDirectory.
@@ -849,15 +881,12 @@ class SubDirectory extends TypedEventEmitter {
849
881
  processClearMessage(op, local, localOpMetadata) {
850
882
  this.throwIfDisposed();
851
883
  if (local) {
852
- assert(localOpMetadata !== undefined, 0x00f /* pendingMessageId is missing from the local client's operation */);
853
- const pendingMessageId = localOpMetadata;
854
- if (this.pendingClearMessageId === pendingMessageId) {
855
- this.pendingClearMessageId = -1;
856
- }
884
+ assert(isClearLocalOpMetadata(localOpMetadata), 0x00f /* `pendingMessageId is missing from the local client's ${op.type} operation` */);
885
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
886
+ assert(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32a /* pendingMessageId does not match */);
857
887
  return;
858
888
  }
859
889
  this.clearExceptPendingKeys();
860
- this.directory.emit("clear", local, this.directory);
861
890
  }
862
891
  /**
863
892
  * Process a delete operation.
@@ -905,7 +934,7 @@ class SubDirectory extends TypedEventEmitter {
905
934
  */
906
935
  processCreateSubDirectoryMessage(op, local, localOpMetadata) {
907
936
  this.throwIfDisposed();
908
- if (!this.needProcessSubDirectoryOperations(op, local, localOpMetadata)) {
937
+ if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
909
938
  return;
910
939
  }
911
940
  this.createSubDirectoryCore(op.subdirName, local);
@@ -921,7 +950,7 @@ class SubDirectory extends TypedEventEmitter {
921
950
  */
922
951
  processDeleteSubDirectoryMessage(op, local, localOpMetadata) {
923
952
  this.throwIfDisposed();
924
- if (!this.needProcessSubDirectoryOperations(op, local, localOpMetadata)) {
953
+ if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
925
954
  return;
926
955
  }
927
956
  this.deleteSubDirectoryCore(op.subdirName, local);
@@ -929,35 +958,139 @@ class SubDirectory extends TypedEventEmitter {
929
958
  /**
930
959
  * Submit a clear operation.
931
960
  * @param op - The operation
932
- * @internal
933
961
  */
934
- submitClearMessage(op) {
962
+ submitClearMessage(op, previousValue) {
935
963
  this.throwIfDisposed();
964
+ const pendingMsgId = ++this.pendingMessageId;
965
+ this.pendingClearMessageIds.push(pendingMsgId);
966
+ const metadata = {
967
+ type: "clear",
968
+ pendingMessageId: pendingMsgId,
969
+ previousStorage: previousValue,
970
+ };
971
+ this.directory.submitDirectoryMessage(op, metadata);
972
+ }
973
+ /**
974
+ * Resubmit a clear operation.
975
+ * @param op - The operation
976
+ * @internal
977
+ */
978
+ resubmitClearMessage(op, localOpMetadata) {
979
+ assert(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
980
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
981
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
982
+ assert(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32c /* pendingMessageId does not match */);
983
+ this.submitClearMessage(op, localOpMetadata.previousStorage);
984
+ }
985
+ /**
986
+ * Get a new pending message id for the op and cache it to track the pending op
987
+ */
988
+ getKeyMessageId(op) {
989
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
936
990
  const pendingMessageId = ++this.pendingMessageId;
937
- this.directory.submitDirectoryMessage(op, pendingMessageId);
938
- this.pendingClearMessageId = pendingMessageId;
991
+ const pendingMessageIds = this.pendingKeys.get(op.key);
992
+ if (pendingMessageIds !== undefined) {
993
+ pendingMessageIds.push(pendingMessageId);
994
+ }
995
+ else {
996
+ this.pendingKeys.set(op.key, [pendingMessageId]);
997
+ }
998
+ return pendingMessageId;
939
999
  }
940
1000
  /**
941
1001
  * Submit a key operation.
942
1002
  * @param op - The operation
1003
+ * @param previousValue - The value of the key before this op
1004
+ */
1005
+ submitKeyMessage(op, previousValue) {
1006
+ this.throwIfDisposed();
1007
+ const pendingMessageId = this.getKeyMessageId(op);
1008
+ const localMetadata = { type: "edit", pendingMessageId, previousValue };
1009
+ this.directory.submitDirectoryMessage(op, localMetadata);
1010
+ }
1011
+ /**
1012
+ * Submit a key message to remote clients based on a previous submit.
1013
+ * @param op - The map key message
1014
+ * @param localOpMetadata - Metadata from the previous submit
943
1015
  * @internal
944
1016
  */
945
- submitKeyMessage(op) {
1017
+ resubmitKeyMessage(op, localOpMetadata) {
1018
+ assert(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
1019
+ // clear the old pending message id
1020
+ const pendingMessageIds = this.pendingKeys.get(op.key);
1021
+ assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x32e /* Unexpected pending message received */);
1022
+ pendingMessageIds.shift();
1023
+ if (pendingMessageIds.length === 0) {
1024
+ this.pendingKeys.delete(op.key);
1025
+ }
1026
+ this.submitKeyMessage(op, localOpMetadata.previousValue);
1027
+ }
1028
+ /**
1029
+ * Get a new pending message id for the op and cache it to track the pending op
1030
+ */
1031
+ getSubDirMessageId(op) {
1032
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1033
+ const newMessageId = ++this.pendingMessageId;
1034
+ const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1035
+ if (pendingMessageIds !== undefined) {
1036
+ pendingMessageIds.push(newMessageId);
1037
+ }
1038
+ else {
1039
+ this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
1040
+ }
1041
+ return newMessageId;
1042
+ }
1043
+ /**
1044
+ * Submit a create subdirectory operation.
1045
+ * @param op - The operation
1046
+ * @param prevExisted - Whether the subdirectory existed before the op
1047
+ */
1048
+ submitCreateSubDirectoryMessage(op, prevExisted) {
946
1049
  this.throwIfDisposed();
947
- const pendingMessageId = ++this.pendingMessageId;
948
- this.directory.submitDirectoryMessage(op, pendingMessageId);
949
- this.pendingKeys.set(op.key, pendingMessageId);
1050
+ const newMessageId = this.getSubDirMessageId(op);
1051
+ const localOpMetadata = {
1052
+ type: "createSubDir",
1053
+ pendingMessageId: newMessageId,
1054
+ previouslyExisted: prevExisted,
1055
+ };
1056
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
950
1057
  }
951
1058
  /**
952
- * Submit a subdirectory operation.
1059
+ * Submit a delete subdirectory operation.
953
1060
  * @param op - The operation
954
- * @internal
1061
+ * @param subDir - Any subdirectory deleted by the op
955
1062
  */
956
- submitSubDirectoryMessage(op) {
1063
+ submitDeleteSubDirectoryMessage(op, subDir) {
957
1064
  this.throwIfDisposed();
958
- const pendingMessageId = ++this.pendingMessageId;
959
- this.directory.submitDirectoryMessage(op, pendingMessageId);
960
- this.pendingSubDirectories.set(op.subdirName, pendingMessageId);
1065
+ const newMessageId = this.getSubDirMessageId(op);
1066
+ const localOpMetadata = {
1067
+ type: "deleteSubDir",
1068
+ pendingMessageId: newMessageId,
1069
+ subDirectory: subDir,
1070
+ };
1071
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
1072
+ }
1073
+ /**
1074
+ * Submit a subdirectory operation again
1075
+ * @param op - The operation
1076
+ * @param localOpMetadata - metadata submitted with the op originally
1077
+ * @internal
1078
+ */
1079
+ resubmitSubDirectoryMessage(op, localOpMetadata) {
1080
+ assert(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
1081
+ // clear the old pending message id
1082
+ const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1083
+ assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x330 /* Unexpected pending message received */);
1084
+ pendingMessageIds.shift();
1085
+ if (pendingMessageIds.length === 0) {
1086
+ this.pendingSubDirectories.delete(op.subdirName);
1087
+ }
1088
+ if (localOpMetadata.type === "createSubDir") {
1089
+ this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
1090
+ }
1091
+ else {
1092
+ this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1093
+ }
961
1094
  }
962
1095
  /**
963
1096
  * Get the storage of this subdirectory in a serializable format, to be used in snapshotting.
@@ -1004,6 +1137,67 @@ class SubDirectory extends TypedEventEmitter {
1004
1137
  this.throwIfDisposed();
1005
1138
  return this._storage.get(key);
1006
1139
  }
1140
+ /**
1141
+ * Remove the pendingMessageId from the map tracking it on rollback
1142
+ * @param map - map tracking the pending messages
1143
+ * @param key - key of the edit in the op
1144
+ */
1145
+ rollbackPendingMessageId(map, key, pendingMessageId) {
1146
+ const pendingMessageIds = map.get(key);
1147
+ const lastPendingMessageId = pendingMessageIds === null || pendingMessageIds === void 0 ? void 0 : pendingMessageIds.pop();
1148
+ if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
1149
+ throw new Error("Rollback op does not match last pending");
1150
+ }
1151
+ if (pendingMessageIds.length === 0) {
1152
+ map.delete(key);
1153
+ }
1154
+ }
1155
+ /**
1156
+ * Rollback a local op
1157
+ * @param op - The operation to rollback
1158
+ * @param localOpMetadata - The local metadata associated with the op.
1159
+ */
1160
+ rollback(op, localOpMetadata) {
1161
+ if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
1162
+ throw new Error("Invalid localOpMetadata");
1163
+ }
1164
+ if (op.type === "clear" && localOpMetadata.type === "clear") {
1165
+ localOpMetadata.previousStorage.forEach((localValue, key) => {
1166
+ this.setCore(key, localValue, true);
1167
+ });
1168
+ const lastPendingClearId = this.pendingClearMessageIds.pop();
1169
+ if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
1170
+ throw new Error("Rollback op does match last clear");
1171
+ }
1172
+ }
1173
+ else if ((op.type === "delete" || op.type === "set") && localOpMetadata.type === "edit") {
1174
+ if (localOpMetadata.previousValue === undefined) {
1175
+ this.deleteCore(op.key, true);
1176
+ }
1177
+ else {
1178
+ this.setCore(op.key, localOpMetadata.previousValue, true);
1179
+ }
1180
+ this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
1181
+ }
1182
+ else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1183
+ if (!localOpMetadata.previouslyExisted) {
1184
+ this.deleteSubDirectoryCore(op.subdirName, true);
1185
+ }
1186
+ this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1187
+ }
1188
+ else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
1189
+ if (localOpMetadata.subDirectory !== undefined) {
1190
+ this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
1191
+ // don't need to register events because deleting never unregistered
1192
+ this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
1193
+ this.emit("subDirectoryCreated", op.subdirName, true, this);
1194
+ }
1195
+ this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1196
+ }
1197
+ else {
1198
+ throw new Error("Unsupported op for rollback");
1199
+ }
1200
+ }
1007
1201
  /**
1008
1202
  * Converts the given relative path into an absolute path.
1009
1203
  * @param path - Relative path to convert
@@ -1016,16 +1210,16 @@ class SubDirectory extends TypedEventEmitter {
1016
1210
  * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1017
1211
  * not process the incoming operation.
1018
1212
  * @param op - Operation to check
1019
- * @param local - Whether the message originated from the local client
1020
- * @param message - The message
1021
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1022
- * For messages from a remote client, this will be undefined.
1213
+ * @param local - Whether the operation originated from the local client
1214
+ * @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
1215
+ * For ops from a remote client, this will be undefined.
1023
1216
  * @returns True if the operation should be processed, false otherwise
1024
1217
  */
1025
1218
  needProcessStorageOperation(op, local, localOpMetadata) {
1026
- if (this.pendingClearMessageId !== -1) {
1219
+ if (this.pendingClearMessageIds.length > 0) {
1027
1220
  if (local) {
1028
- assert(localOpMetadata !== undefined && localOpMetadata < this.pendingClearMessageId, 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
1221
+ assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata) &&
1222
+ localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0], 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
1029
1223
  }
1030
1224
  // If I have a NACK clear, we can ignore all ops.
1031
1225
  return false;
@@ -1035,9 +1229,11 @@ class SubDirectory extends TypedEventEmitter {
1035
1229
  // Found an NACK op, clear it from the directory if the latest sequence number in the directory
1036
1230
  // match the message's and don't process the op.
1037
1231
  if (local) {
1038
- assert(localOpMetadata !== undefined, 0x011 /* pendingMessageId is missing from the local client's operation */);
1039
- const pendingMessageId = localOpMetadata;
1040
- if (pendingKeyMessageId === pendingMessageId) {
1232
+ assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
1233
+ const pendingMessageIds = this.pendingKeys.get(op.key);
1234
+ assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x331 /* Unexpected pending message received */);
1235
+ pendingMessageIds.shift();
1236
+ if (pendingMessageIds.length === 0) {
1041
1237
  this.pendingKeys.delete(op.key);
1042
1238
  }
1043
1239
  }
@@ -1056,13 +1252,15 @@ class SubDirectory extends TypedEventEmitter {
1056
1252
  * For messages from a remote client, this will be undefined.
1057
1253
  * @returns True if the operation should be processed, false otherwise
1058
1254
  */
1059
- needProcessSubDirectoryOperations(op, local, localOpMetadata) {
1255
+ needProcessSubDirectoryOperation(op, local, localOpMetadata) {
1060
1256
  const pendingSubDirectoryMessageId = this.pendingSubDirectories.get(op.subdirName);
1061
1257
  if (pendingSubDirectoryMessageId !== undefined) {
1062
1258
  if (local) {
1063
- assert(localOpMetadata !== undefined, 0x012 /* pendingMessageId is missing from the local client's operation */);
1064
- const pendingMessageId = localOpMetadata;
1065
- if (pendingSubDirectoryMessageId === pendingMessageId) {
1259
+ assert(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
1260
+ const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1261
+ assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x332 /* Unexpected pending message received */);
1262
+ pendingMessageIds.shift();
1263
+ if (pendingMessageIds.length === 0) {
1066
1264
  this.pendingSubDirectories.delete(op.subdirName);
1067
1265
  }
1068
1266
  }
@@ -1081,9 +1279,9 @@ class SubDirectory extends TypedEventEmitter {
1081
1279
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1082
1280
  temp.set(key, this._storage.get(key));
1083
1281
  });
1084
- this._storage.clear();
1282
+ this.clearCore(false);
1085
1283
  temp.forEach((value, key, map) => {
1086
- this._storage.set(key, value);
1284
+ this.setCore(key, value, true);
1087
1285
  });
1088
1286
  }
1089
1287
  /**
@@ -1099,11 +1297,11 @@ class SubDirectory extends TypedEventEmitter {
1099
1297
  * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
1100
1298
  * @param key - The key being deleted
1101
1299
  * @param local - Whether the message originated from the local client
1102
- * @param op - The message if from a remote delete, or null if from a local delete
1103
- * @returns True if the key existed and was deleted, false if it did not exist
1300
+ * @returns Previous local value of the key if it existed, undefined if it did not exist
1104
1301
  */
1105
1302
  deleteCore(key, local) {
1106
- const previousValue = this.get(key);
1303
+ const previousLocalValue = this._storage.get(key);
1304
+ const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
1107
1305
  const successfullyRemoved = this._storage.delete(key);
1108
1306
  if (successfullyRemoved) {
1109
1307
  const event = { key, path: this.absolutePath, previousValue };
@@ -1111,27 +1309,30 @@ class SubDirectory extends TypedEventEmitter {
1111
1309
  const containedEvent = { key, previousValue };
1112
1310
  this.emit("containedValueChanged", containedEvent, local, this);
1113
1311
  }
1114
- return successfullyRemoved;
1312
+ return previousLocalValue;
1115
1313
  }
1116
1314
  /**
1117
1315
  * Set implementation used for both locally sourced sets as well as incoming remote sets.
1118
1316
  * @param key - The key being set
1119
1317
  * @param value - The value being set
1120
1318
  * @param local - Whether the message originated from the local client
1121
- * @param op - The message if from a remote set, or null if from a local set
1319
+ * @returns Previous local value of the key, if any
1122
1320
  */
1123
1321
  setCore(key, value, local) {
1124
- const previousValue = this.get(key);
1322
+ const previousLocalValue = this._storage.get(key);
1323
+ const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
1125
1324
  this._storage.set(key, value);
1126
1325
  const event = { key, path: this.absolutePath, previousValue };
1127
1326
  this.directory.emit("valueChanged", event, local, this.directory);
1128
1327
  const containedEvent = { key, previousValue };
1129
1328
  this.emit("containedValueChanged", containedEvent, local, this);
1329
+ return previousLocalValue;
1130
1330
  }
1131
1331
  /**
1132
1332
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1133
1333
  * @param subdirName - The name of the subdirectory being created
1134
1334
  * @param local - Whether the message originated from the local client
1335
+ * @returns - True if is newly created, false if it already existed.
1135
1336
  */
1136
1337
  createSubDirectoryCore(subdirName, local) {
1137
1338
  if (!this._subdirectories.has(subdirName)) {
@@ -1140,7 +1341,9 @@ class SubDirectory extends TypedEventEmitter {
1140
1341
  this.registerEventsOnSubDirectory(subDir, subdirName);
1141
1342
  this._subdirectories.set(subdirName, subDir);
1142
1343
  this.emit("subDirectoryCreated", subdirName, local, this);
1344
+ return true;
1143
1345
  }
1346
+ return false;
1144
1347
  }
1145
1348
  registerEventsOnSubDirectory(subDirectory, subDirName) {
1146
1349
  subDirectory.on("subDirectoryCreated", (relativePath, local) => {
@@ -1154,18 +1357,17 @@ class SubDirectory extends TypedEventEmitter {
1154
1357
  * Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1155
1358
  * @param subdirName - The name of the subdirectory being deleted
1156
1359
  * @param local - Whether the message originated from the local client
1157
- * @param op - The message if from a remote delete, or null if from a local delete
1158
1360
  */
1159
1361
  deleteSubDirectoryCore(subdirName, local) {
1160
- const previousValue = this.getSubDirectory(subdirName);
1362
+ const previousValue = this._subdirectories.get(subdirName);
1161
1363
  // This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
1162
- // Might want to consider cleaning out the structure more exhaustively though?
1163
- const successfullyRemoved = this._subdirectories.delete(subdirName);
1364
+ // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
1164
1365
  if (previousValue !== undefined) {
1366
+ this._subdirectories.delete(subdirName);
1165
1367
  this.disposeSubDirectoryTree(previousValue);
1166
1368
  this.emit("subDirectoryDeleted", subdirName, local, this);
1167
1369
  }
1168
- return successfullyRemoved;
1370
+ return previousValue;
1169
1371
  }
1170
1372
  disposeSubDirectoryTree(directory) {
1171
1373
  if (!directory) {
@@ -1180,5 +1382,12 @@ class SubDirectory extends TypedEventEmitter {
1180
1382
  directory.dispose();
1181
1383
  }
1182
1384
  }
1385
+ undeleteSubDirectoryTree(directory) {
1386
+ // Restore deleted subdirectory tree. This will unmark "deleted" from the subdirectories from bottom to top.
1387
+ for (const [_, subDirectory] of this._subdirectories.entries()) {
1388
+ this.undeleteSubDirectoryTree(subDirectory);
1389
+ }
1390
+ directory.undispose();
1391
+ }
1183
1392
  }
1184
1393
  //# sourceMappingURL=directory.js.map