@fluidframework/map 2.0.0-internal.3.3.2 → 2.0.0-internal.3.4.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/src/directory.ts CHANGED
@@ -43,12 +43,18 @@ const snapshotFileName = "header";
43
43
  interface IDirectoryMessageHandler {
44
44
  /**
45
45
  * Apply the given operation.
46
+ * @param msg - The message from the server to apply.
46
47
  * @param op - The directory operation to apply
47
48
  * @param local - Whether the message originated from the local client
48
49
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
49
50
  * For messages from a remote client, this will be undefined.
50
51
  */
51
- process(op: IDirectoryOperation, local: boolean, localOpMetadata: unknown): void;
52
+ process(
53
+ msg: ISequencedDocumentMessage,
54
+ op: IDirectoryOperation,
55
+ local: boolean,
56
+ localOpMetadata: unknown,
57
+ ): void;
52
58
 
53
59
  /**
54
60
  * Communicate the operation to remote clients.
@@ -182,6 +188,21 @@ export type IDirectorySubDirectoryOperation =
182
188
  */
183
189
  export type IDirectoryOperation = IDirectoryStorageOperation | IDirectorySubDirectoryOperation;
184
190
 
191
+ /**
192
+ * Create info for the subdirectory.
193
+ */
194
+ export interface ICreateInfo {
195
+ /**
196
+ * Sequence number at which this subdirectory was created.
197
+ */
198
+ csn: number;
199
+
200
+ /**
201
+ * clientids of the clients which created this sub directory.
202
+ */
203
+ ccIds: string[];
204
+ }
205
+
185
206
  /**
186
207
  * Defines the in-memory object structure to be used for the conversion to/from serialized.
187
208
  *
@@ -200,6 +221,15 @@ export interface IDirectoryDataObject {
200
221
  * Recursive sub-directories {@link IDirectoryDataObject | objects}.
201
222
  */
202
223
  subdirectories?: { [subdirName: string]: IDirectoryDataObject };
224
+
225
+ /**
226
+ * Create info for the sub directory. Since directories with same name can get deleted/created by multiple clients
227
+ * asynchronously, this info helps us to determine whether the ops where for the current instance of sub directory
228
+ * or not and whether to process them or not based on that. Summaries which were not produced which this change
229
+ * will not have this info and in that case we can still run in eventual consistency issues but that is no worse
230
+ * than the state before this change.
231
+ */
232
+ ci?: ICreateInfo;
203
233
  }
204
234
 
205
235
  /**
@@ -336,6 +366,8 @@ export class SharedDirectory
336
366
  * Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
337
367
  */
338
368
  private readonly root: SubDirectory = new SubDirectory(
369
+ 0,
370
+ new Set(),
339
371
  this,
340
372
  this.runtime,
341
373
  this.serializer,
@@ -620,7 +652,12 @@ export class SharedDirectory
620
652
  )) {
621
653
  let newSubDir = currentSubDir.getSubDirectory(subdirName) as SubDirectory;
622
654
  if (!newSubDir) {
655
+ const createInfo = subdirObject.ci;
623
656
  newSubDir = new SubDirectory(
657
+ createInfo !== undefined ? createInfo.csn : 0,
658
+ createInfo !== undefined
659
+ ? new Set<string>(createInfo.ccIds)
660
+ : new Set(),
624
661
  this,
625
662
  this.runtime,
626
663
  this.serializer,
@@ -658,7 +695,7 @@ export class SharedDirectory
658
695
  const op: IDirectoryOperation = message.contents as IDirectoryOperation;
659
696
  const handler = this.messageHandlers.get(op.type);
660
697
  assert(handler !== undefined, 0x00e /* Missing message handler for message type */);
661
- handler.process(op, local, localOpMetadata);
698
+ handler.process(message, op, local, localOpMetadata);
662
699
  }
663
700
  }
664
701
 
@@ -705,15 +742,48 @@ export class SharedDirectory
705
742
  return this.localValueMaker.fromSerializable(serializable);
706
743
  }
707
744
 
745
+ /**
746
+ * This checks if there is pending delete op for local delete for a subdirectory.
747
+ * @param relativePath - path of sub directory.
748
+ * @returns - true if there is pending delete.
749
+ */
750
+ private isSubDirectoryDeletePending(relativePath: string): boolean {
751
+ const parentSubDir = this.getParentDirectory(relativePath);
752
+ const index = relativePath.lastIndexOf(posix.sep);
753
+ const dirName = relativePath.substring(index + 1);
754
+ return !!parentSubDir?.isSubDirectoryDeletePending(dirName);
755
+ }
756
+
757
+ /**
758
+ * Gets the parent directory of a sub directory.
759
+ * @param relativePath - path of sub directory of which parent needs to be find out.
760
+ */
761
+ private getParentDirectory(relativePath: string): SubDirectory | undefined {
762
+ const absolutePath = this.makeAbsolute(relativePath);
763
+ if (absolutePath === posix.sep) {
764
+ return undefined;
765
+ }
766
+ const index = absolutePath.lastIndexOf(posix.sep);
767
+ const parentAbsPath = absolutePath.substring(0, index);
768
+ return this.getWorkingDirectory(parentAbsPath) as SubDirectory;
769
+ }
770
+
708
771
  /**
709
772
  * Set the message handlers for the directory.
710
773
  */
711
774
  private setMessageHandlers(): void {
712
775
  this.messageHandlers.set("clear", {
713
- process: (op: IDirectoryClearOperation, local, localOpMetadata) => {
776
+ process: (
777
+ msg: ISequencedDocumentMessage,
778
+ op: IDirectoryClearOperation,
779
+ local,
780
+ localOpMetadata,
781
+ ) => {
714
782
  const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
715
- if (subdir) {
716
- subdir.processClearMessage(op, local, localOpMetadata);
783
+ // If there is pending delete op for this subDirectory, then don't apply the this op as we are going
784
+ // to delete this subDirectory.
785
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
786
+ subdir.processClearMessage(msg, op, local, localOpMetadata);
717
787
  }
718
788
  },
719
789
  submit: (op: IDirectoryClearOperation, localOpMetadata: unknown) => {
@@ -730,10 +800,17 @@ export class SharedDirectory
730
800
  },
731
801
  });
732
802
  this.messageHandlers.set("delete", {
733
- process: (op: IDirectoryDeleteOperation, local, localOpMetadata) => {
803
+ process: (
804
+ msg: ISequencedDocumentMessage,
805
+ op: IDirectoryDeleteOperation,
806
+ local,
807
+ localOpMetadata,
808
+ ) => {
734
809
  const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
735
- if (subdir) {
736
- subdir.processDeleteMessage(op, local, localOpMetadata);
810
+ // If there is pending delete op for this subDirectory, then don't apply the this op as we are going
811
+ // to delete this subDirectory.
812
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
813
+ subdir.processDeleteMessage(msg, op, local, localOpMetadata);
737
814
  }
738
815
  },
739
816
  submit: (op: IDirectoryDeleteOperation, localOpMetadata: unknown) => {
@@ -752,11 +829,18 @@ export class SharedDirectory
752
829
  },
753
830
  });
754
831
  this.messageHandlers.set("set", {
755
- process: (op: IDirectorySetOperation, local, localOpMetadata) => {
832
+ process: (
833
+ msg: ISequencedDocumentMessage,
834
+ op: IDirectorySetOperation,
835
+ local,
836
+ localOpMetadata,
837
+ ) => {
756
838
  const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
757
- if (subdir) {
839
+ // If there is pending delete op for this subDirectory, then don't apply the this op as we are going
840
+ // to delete this subDirectory.
841
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
758
842
  const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
759
- subdir.processSetMessage(op, context, local, localOpMetadata);
843
+ subdir.processSetMessage(msg, op, context, local, localOpMetadata);
760
844
  }
761
845
  },
762
846
  submit: (op: IDirectorySetOperation, localOpMetadata: unknown) => {
@@ -775,10 +859,15 @@ export class SharedDirectory
775
859
  });
776
860
 
777
861
  this.messageHandlers.set("createSubDirectory", {
778
- process: (op: IDirectoryCreateSubDirectoryOperation, local, localOpMetadata) => {
862
+ process: (
863
+ msg: ISequencedDocumentMessage,
864
+ op: IDirectoryCreateSubDirectoryOperation,
865
+ local,
866
+ localOpMetadata,
867
+ ) => {
779
868
  const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
780
869
  if (parentSubdir) {
781
- parentSubdir.processCreateSubDirectoryMessage(op, local, localOpMetadata);
870
+ parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
782
871
  }
783
872
  },
784
873
  submit: (op: IDirectoryCreateSubDirectoryOperation, localOpMetadata: unknown) => {
@@ -799,10 +888,15 @@ export class SharedDirectory
799
888
  });
800
889
 
801
890
  this.messageHandlers.set("deleteSubDirectory", {
802
- process: (op: IDirectoryDeleteSubDirectoryOperation, local, localOpMetadata) => {
891
+ process: (
892
+ msg: ISequencedDocumentMessage,
893
+ op: IDirectoryDeleteSubDirectoryOperation,
894
+ local,
895
+ localOpMetadata,
896
+ ) => {
803
897
  const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
804
898
  if (parentSubdir) {
805
- parentSubdir.processDeleteSubDirectoryMessage(op, local, localOpMetadata);
899
+ parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
806
900
  }
807
901
  },
808
902
  submit: (op: IDirectoryDeleteSubDirectoryOperation, localOpMetadata: unknown) => {
@@ -853,6 +947,7 @@ export class SharedDirectory
853
947
  while (stack.length > 0) {
854
948
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
855
949
  const [currentSubDir, currentSubDirObject] = stack.pop()!;
950
+ currentSubDirObject.ci = currentSubDir.getSerializableCreateInfo();
856
951
  for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
857
952
  if (!currentSubDirObject.storage) {
858
953
  currentSubDirObject.storage = {};
@@ -916,7 +1011,6 @@ interface IClearLocalOpMetadata {
916
1011
  interface ICreateSubDirLocalOpMetadata {
917
1012
  type: "createSubDir";
918
1013
  pendingMessageId: number;
919
- previouslyExisted: boolean;
920
1014
  }
921
1015
 
922
1016
  interface IDeleteSubDirLocalOpMetadata {
@@ -954,8 +1048,7 @@ function isSubDirLocalOpMetadata(metadata: any): metadata is SubDirLocalOpMetada
954
1048
  return (
955
1049
  metadata !== undefined &&
956
1050
  typeof metadata.pendingMessageId === "number" &&
957
- ((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
958
- metadata.type === "deleteSubDir")
1051
+ (metadata.type === "createSubDir" || metadata.type === "deleteSubDir")
959
1052
  );
960
1053
  }
961
1054
 
@@ -966,7 +1059,7 @@ function isDirectoryLocalOpMetadata(metadata: any): metadata is DirectoryLocalOp
966
1059
  (metadata.type === "edit" ||
967
1060
  metadata.type === "deleteSubDir" ||
968
1061
  (metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
969
- (metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"))
1062
+ metadata.type === "createSubDir")
970
1063
  );
971
1064
  }
972
1065
 
@@ -1003,10 +1096,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1003
1096
  private readonly pendingKeys: Map<string, number[]> = new Map();
1004
1097
 
1005
1098
  /**
1006
- * Subdirectories that have been modified locally but not yet ack'd from the server.
1099
+ * Subdirectories that have been created/deleted locally but not yet ack'd from the server.
1007
1100
  */
1008
1101
  private readonly pendingSubDirectories: Map<string, number[]> = new Map();
1009
1102
 
1103
+ /**
1104
+ * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the count
1105
+ * of delete op that are pending or yet to be acked from server.
1106
+ */
1107
+ private readonly pendingDeleteSubDirectoriesCount: Map<string, number> = new Map();
1108
+
1010
1109
  /**
1011
1110
  * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
1012
1111
  */
@@ -1019,12 +1118,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1019
1118
 
1020
1119
  /**
1021
1120
  * Constructor.
1121
+ * @param sequenceNumber - Message seq number at which this was created.
1122
+ * @param clientIds - Ids of client which created this directory.
1022
1123
  * @param directory - Reference back to the SharedDirectory to perform operations
1023
1124
  * @param runtime - The data store runtime this directory is associated with
1024
1125
  * @param serializer - The serializer to serialize / parse handles
1025
1126
  * @param absolutePath - The absolute path of this IDirectory
1026
1127
  */
1027
1128
  public constructor(
1129
+ private sequenceNumber: number,
1130
+ private readonly clientIds: Set<string>,
1028
1131
  private readonly directory: SharedDirectory,
1029
1132
  private readonly runtime: IFluidDataStoreRuntime,
1030
1133
  private readonly serializer: IFluidSerializer,
@@ -1132,22 +1235,29 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1132
1235
  }
1133
1236
 
1134
1237
  // Create the sub directory locally first.
1135
- const isNew = this.createSubDirectoryCore(subdirName, true);
1136
-
1137
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1138
- const subDir: IDirectory = this._subdirectories.get(subdirName)!;
1238
+ const isNew = this.createSubDirectoryCore(
1239
+ subdirName,
1240
+ true,
1241
+ -1,
1242
+ this.runtime.clientId ?? "detached",
1243
+ );
1244
+ const subDir = this._subdirectories.get(subdirName);
1245
+ assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
1139
1246
 
1140
1247
  // If we are not attached, don't submit the op.
1141
1248
  if (!this.directory.isAttached()) {
1142
1249
  return subDir;
1143
1250
  }
1144
1251
 
1145
- const op: IDirectoryCreateSubDirectoryOperation = {
1146
- path: this.absolutePath,
1147
- subdirName,
1148
- type: "createSubDirectory",
1149
- };
1150
- this.submitCreateSubDirectoryMessage(op, !isNew);
1252
+ // Only submit the op, if it is newly created.
1253
+ if (isNew) {
1254
+ const op: IDirectoryCreateSubDirectoryOperation = {
1255
+ path: this.absolutePath,
1256
+ subdirName,
1257
+ type: "createSubDirectory",
1258
+ };
1259
+ this.submitCreateSubDirectoryMessage(op);
1260
+ }
1151
1261
 
1152
1262
  return subDir;
1153
1263
  }
@@ -1181,13 +1291,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1181
1291
  return subDir !== undefined;
1182
1292
  }
1183
1293
 
1184
- const op: IDirectoryDeleteSubDirectoryOperation = {
1185
- path: this.absolutePath,
1186
- subdirName,
1187
- type: "deleteSubDirectory",
1188
- };
1294
+ // Only submit the op, if the directory existed and we deleted it.
1295
+ if (subDir !== undefined) {
1296
+ const op: IDirectoryDeleteSubDirectoryOperation = {
1297
+ path: this.absolutePath,
1298
+ subdirName,
1299
+ type: "deleteSubDirectory",
1300
+ };
1189
1301
 
1190
- this.submitDeleteSubDirectoryMessage(op, subDir);
1302
+ this.submitDeleteSubDirectoryMessage(op, subDir);
1303
+ }
1191
1304
  return subDir !== undefined;
1192
1305
  }
1193
1306
 
@@ -1207,6 +1320,19 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1207
1320
  return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
1208
1321
  }
1209
1322
 
1323
+ /**
1324
+ * This checks if there is pending delete op for local delete for a given child subdirectory.
1325
+ * @param subDirName - directory name.
1326
+ * @returns - true if there is pending delete.
1327
+ */
1328
+ public isSubDirectoryDeletePending(subDirName: string): boolean {
1329
+ const pendingDeleteSubDirectory = this.pendingDeleteSubDirectoriesCount.get(subDirName);
1330
+ if (pendingDeleteSubDirectory !== undefined && pendingDeleteSubDirectory > 0) {
1331
+ return true;
1332
+ }
1333
+ return false;
1334
+ }
1335
+
1210
1336
  /**
1211
1337
  * Deletes the given key from within this IDirectory.
1212
1338
  * @param key - The key to delete
@@ -1337,6 +1463,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1337
1463
 
1338
1464
  /**
1339
1465
  * Process a clear operation.
1466
+ * @param msg - The message from the server to apply.
1340
1467
  * @param op - The op to process
1341
1468
  * @param local - Whether the message originated from the local client
1342
1469
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1344,11 +1471,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1344
1471
  * @internal
1345
1472
  */
1346
1473
  public processClearMessage(
1474
+ msg: ISequencedDocumentMessage,
1347
1475
  op: IDirectoryClearOperation,
1348
1476
  local: boolean,
1349
1477
  localOpMetadata: unknown,
1350
1478
  ): void {
1351
1479
  this.throwIfDisposed();
1480
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1481
+ return;
1482
+ }
1352
1483
  if (local) {
1353
1484
  assert(
1354
1485
  isClearLocalOpMetadata(localOpMetadata),
@@ -1385,6 +1516,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1385
1516
 
1386
1517
  /**
1387
1518
  * Process a delete operation.
1519
+ * @param msg - The message from the server to apply.
1388
1520
  * @param op - The op to process
1389
1521
  * @param local - Whether the message originated from the local client
1390
1522
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1392,12 +1524,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1392
1524
  * @internal
1393
1525
  */
1394
1526
  public processDeleteMessage(
1527
+ msg: ISequencedDocumentMessage,
1395
1528
  op: IDirectoryDeleteOperation,
1396
1529
  local: boolean,
1397
1530
  localOpMetadata: unknown,
1398
1531
  ): void {
1399
1532
  this.throwIfDisposed();
1400
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1533
+ if (
1534
+ !(
1535
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1536
+ this.needProcessStorageOperation(op, local, localOpMetadata)
1537
+ )
1538
+ ) {
1401
1539
  return;
1402
1540
  }
1403
1541
  this.deleteCore(op.key, local);
@@ -1422,6 +1560,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1422
1560
 
1423
1561
  /**
1424
1562
  * Process a set operation.
1563
+ * @param msg - The message from the server to apply.
1425
1564
  * @param op - The op to process
1426
1565
  * @param local - Whether the message originated from the local client
1427
1566
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1429,19 +1568,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1429
1568
  * @internal
1430
1569
  */
1431
1570
  public processSetMessage(
1571
+ msg: ISequencedDocumentMessage,
1432
1572
  op: IDirectorySetOperation,
1433
1573
  context: ILocalValue | undefined,
1434
1574
  local: boolean,
1435
1575
  localOpMetadata: unknown,
1436
1576
  ): void {
1437
1577
  this.throwIfDisposed();
1438
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1578
+ if (
1579
+ !(
1580
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1581
+ this.needProcessStorageOperation(op, local, localOpMetadata)
1582
+ )
1583
+ ) {
1439
1584
  return;
1440
1585
  }
1441
1586
 
1442
1587
  // needProcessStorageOperation should have returned false if local is true
1443
1588
  // so we can assume context is not undefined
1444
-
1445
1589
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1446
1590
  this.setCore(op.key, context!, local);
1447
1591
  }
@@ -1470,6 +1614,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1470
1614
  }
1471
1615
  /**
1472
1616
  * Process a create subdirectory operation.
1617
+ * @param msg - The message from the server to apply.
1473
1618
  * @param op - The op to process
1474
1619
  * @param local - Whether the message originated from the local client
1475
1620
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1477,15 +1622,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1477
1622
  * @internal
1478
1623
  */
1479
1624
  public processCreateSubDirectoryMessage(
1625
+ msg: ISequencedDocumentMessage,
1480
1626
  op: IDirectoryCreateSubDirectoryOperation,
1481
1627
  local: boolean,
1482
1628
  localOpMetadata: unknown,
1483
1629
  ): void {
1484
1630
  this.throwIfDisposed();
1485
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1631
+ if (!this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)) {
1486
1632
  return;
1487
1633
  }
1488
- this.createSubDirectoryCore(op.subdirName, local);
1634
+ this.createSubDirectoryCore(op.subdirName, local, msg.sequenceNumber, msg.clientId);
1489
1635
  }
1490
1636
 
1491
1637
  /**
@@ -1498,19 +1644,19 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1498
1644
  ): ICreateSubDirLocalOpMetadata {
1499
1645
  this.throwIfDisposed();
1500
1646
  // Create the sub directory locally first.
1501
- const isNew = this.createSubDirectoryCore(op.subdirName, true);
1647
+ this.createSubDirectoryCore(op.subdirName, true, -1, this.runtime.clientId ?? "detached");
1502
1648
  const newMessageId = this.getSubDirMessageId(op);
1503
1649
 
1504
1650
  const localOpMetadata: ICreateSubDirLocalOpMetadata = {
1505
1651
  type: "createSubDir",
1506
1652
  pendingMessageId: newMessageId,
1507
- previouslyExisted: !isNew,
1508
1653
  };
1509
1654
  return localOpMetadata;
1510
1655
  }
1511
1656
 
1512
1657
  /**
1513
1658
  * Process a delete subdirectory operation.
1659
+ * @param msg - The message from the server to apply.
1514
1660
  * @param op - The op to process
1515
1661
  * @param local - Whether the message originated from the local client
1516
1662
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1518,12 +1664,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1518
1664
  * @internal
1519
1665
  */
1520
1666
  public processDeleteSubDirectoryMessage(
1667
+ msg: ISequencedDocumentMessage,
1521
1668
  op: IDirectoryDeleteSubDirectoryOperation,
1522
1669
  local: boolean,
1523
1670
  localOpMetadata: unknown,
1524
1671
  ): void {
1525
1672
  this.throwIfDisposed();
1526
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1673
+ if (
1674
+ !(
1675
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1676
+ this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)
1677
+ )
1678
+ ) {
1527
1679
  return;
1528
1680
  }
1529
1681
  this.deleteSubDirectoryCore(op.subdirName, local);
@@ -1652,25 +1804,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1652
1804
  } else {
1653
1805
  this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
1654
1806
  }
1807
+ if (op.type === "deleteSubDirectory") {
1808
+ const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName) ?? 0;
1809
+ this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count + 1);
1810
+ }
1655
1811
  return newMessageId;
1656
1812
  }
1657
1813
 
1658
1814
  /**
1659
1815
  * Submit a create subdirectory operation.
1660
1816
  * @param op - The operation
1661
- * @param prevExisted - Whether the subdirectory existed before the op
1662
1817
  */
1663
- private submitCreateSubDirectoryMessage(
1664
- op: IDirectorySubDirectoryOperation,
1665
- prevExisted: boolean,
1666
- ): void {
1818
+ private submitCreateSubDirectoryMessage(op: IDirectorySubDirectoryOperation): void {
1667
1819
  this.throwIfDisposed();
1668
1820
  const newMessageId = this.getSubDirMessageId(op);
1669
1821
 
1670
1822
  const localOpMetadata: ICreateSubDirLocalOpMetadata = {
1671
1823
  type: "createSubDir",
1672
1824
  pendingMessageId: newMessageId,
1673
- previouslyExisted: prevExisted,
1674
1825
  };
1675
1826
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1676
1827
  }
@@ -1723,7 +1874,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1723
1874
  }
1724
1875
 
1725
1876
  if (localOpMetadata.type === "createSubDir") {
1726
- this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
1877
+ this.submitCreateSubDirectoryMessage(op);
1727
1878
  } else {
1728
1879
  this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1729
1880
  }
@@ -1746,6 +1897,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1746
1897
  }
1747
1898
  }
1748
1899
 
1900
+ public getSerializableCreateInfo() {
1901
+ this.throwIfDisposed();
1902
+ const createInfo: ICreateInfo = {
1903
+ csn: this.sequenceNumber,
1904
+ ccIds: Array.from(this.clientIds),
1905
+ };
1906
+ return createInfo;
1907
+ }
1908
+
1749
1909
  /**
1750
1910
  * Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
1751
1911
  * @param key - The key to populate
@@ -1838,9 +1998,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1838
1998
  localOpMetadata.pendingMessageId,
1839
1999
  );
1840
2000
  } else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1841
- if (!localOpMetadata.previouslyExisted) {
1842
- this.deleteSubDirectoryCore(op.subdirName as string, true);
1843
- }
2001
+ this.deleteSubDirectoryCore(op.subdirName as string, true);
1844
2002
 
1845
2003
  this.rollbackPendingMessageId(
1846
2004
  this.pendingSubDirectories,
@@ -1860,6 +2018,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1860
2018
  op.subdirName as string,
1861
2019
  localOpMetadata.pendingMessageId,
1862
2020
  );
2021
+ const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
2022
+ assert(count !== undefined && count > 0, 0x5ab /* should have record for delete op */);
2023
+ this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
2024
+ if (count === 1) {
2025
+ this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
2026
+ }
1863
2027
  } else {
1864
2028
  throw new Error("Unsupported op for rollback");
1865
2029
  }
@@ -1898,7 +2062,23 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1898
2062
  localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
1899
2063
  0x010 /* "Received out of order storage op when there is an unackd clear message" */,
1900
2064
  );
2065
+ // Remove all pendingMessageIds lower than first pendingClearMessageId.
2066
+ const lowestPendingClearMessageId = this.pendingClearMessageIds[0];
2067
+ const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
2068
+ if (pendingKeyMessageIdArray !== undefined) {
2069
+ let index = 0;
2070
+ while (pendingKeyMessageIdArray[index] < lowestPendingClearMessageId) {
2071
+ index += 1;
2072
+ }
2073
+ const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
2074
+ if (newPendingKeyMessageId.length === 0) {
2075
+ this.pendingKeys.delete(op.key);
2076
+ } else {
2077
+ this.pendingKeys.set(op.key, newPendingKeyMessageId);
2078
+ }
2079
+ }
1901
2080
  }
2081
+
1902
2082
  // If I have a NACK clear, we can ignore all ops.
1903
2083
  return false;
1904
2084
  }
@@ -1930,6 +2110,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1930
2110
  return !local;
1931
2111
  }
1932
2112
 
2113
+ /**
2114
+ * This return true if the message is for the current instance of this sub directory. As the sub directory
2115
+ * can be deleted and created again, then this finds if the message is for current instance of directory or not.
2116
+ * @param msg - message for the directory
2117
+ */
2118
+ private isMessageForCurrentInstanceOfSubDirectory(msg: ISequencedDocumentMessage) {
2119
+ // If the message is either from the creator of directory or this directory was created when
2120
+ // container was detached or in case this directory is already live(known to other clients)
2121
+ // and the op was created after the directory was created then apply this op.
2122
+ return (
2123
+ this.clientIds.has(msg.clientId) ||
2124
+ this.clientIds.has("detached") ||
2125
+ (this.sequenceNumber !== -1 && this.sequenceNumber <= msg.referenceSequenceNumber)
2126
+ );
2127
+ }
2128
+
1933
2129
  /**
1934
2130
  * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1935
2131
  * not process the incoming operation.
@@ -1941,6 +2137,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1941
2137
  * @returns True if the operation should be processed, false otherwise
1942
2138
  */
1943
2139
  private needProcessSubDirectoryOperation(
2140
+ msg: ISequencedDocumentMessage,
1944
2141
  op: IDirectorySubDirectoryOperation,
1945
2142
  local: boolean,
1946
2143
  localOpMetadata: unknown,
@@ -1962,6 +2159,44 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1962
2159
  if (pendingMessageIds.length === 0) {
1963
2160
  this.pendingSubDirectories.delete(op.subdirName);
1964
2161
  }
2162
+ if (op.type === "deleteSubDirectory") {
2163
+ const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
2164
+ assert(
2165
+ count !== undefined && count > 0,
2166
+ 0x5ac /* should have record for delete op */,
2167
+ );
2168
+ this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
2169
+ if (count === 1) {
2170
+ this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
2171
+ }
2172
+ }
2173
+ } else if (op.type === "deleteSubDirectory") {
2174
+ // If this is remote delete op and we have keys in this subDirectory, then we need to delete these
2175
+ // keys except the pending ones as they will be sequenced after this delete.
2176
+ const subDirectory = this._subdirectories.get(op.subdirName);
2177
+ if (subDirectory) {
2178
+ subDirectory.clearExceptPendingKeys(local);
2179
+ // In case of remote delete op, we need to reset the creation seq number and client ids of
2180
+ // creators as the previous directory is getting deleted and we will initialize again when
2181
+ // we will receive op for the create again.
2182
+ subDirectory.sequenceNumber = -1;
2183
+ subDirectory.clientIds.clear();
2184
+ }
2185
+ }
2186
+ if (op.type === "createSubDirectory") {
2187
+ const dir = this._subdirectories.get(op.subdirName);
2188
+ if (dir?.sequenceNumber === -1) {
2189
+ // Only set the seq on the first message, could be more
2190
+ dir.sequenceNumber = msg.sequenceNumber;
2191
+ }
2192
+ // The client created the dir at or after the dirs seq, so list its client id as a creator.
2193
+ if (
2194
+ dir !== undefined &&
2195
+ !dir.clientIds.has(msg.clientId) &&
2196
+ dir.sequenceNumber <= msg.sequenceNumber
2197
+ ) {
2198
+ dir.clientIds.add(msg.clientId);
2199
+ }
1965
2200
  }
1966
2201
  return false;
1967
2202
  }
@@ -1978,8 +2213,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1978
2213
  const temp = new Map<string, ILocalValue>();
1979
2214
 
1980
2215
  for (const [key] of this.pendingKeys) {
1981
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1982
- temp.set(key, this._storage.get(key)!);
2216
+ const value = this._storage.get(key);
2217
+ // If this key is already deleted, then we don't need to add it again.
2218
+ if (value !== undefined) {
2219
+ temp.set(key, value);
2220
+ }
1983
2221
  }
1984
2222
 
1985
2223
  this.clearCore(local);
@@ -2039,12 +2277,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2039
2277
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
2040
2278
  * @param subdirName - The name of the subdirectory being created
2041
2279
  * @param local - Whether the message originated from the local client
2280
+ * @param seq - Sequence number at which this directory is created
2281
+ * @param clientId - Id of client which created this directory.
2042
2282
  * @returns - True if is newly created, false if it already existed.
2043
2283
  */
2044
- private createSubDirectoryCore(subdirName: string, local: boolean): boolean {
2045
- if (!this._subdirectories.has(subdirName)) {
2284
+ private createSubDirectoryCore(
2285
+ subdirName: string,
2286
+ local: boolean,
2287
+ seq: number,
2288
+ clientId: string,
2289
+ ): boolean {
2290
+ const subdir = this._subdirectories.get(subdirName);
2291
+ if (subdir === undefined) {
2046
2292
  const absolutePath = posix.join(this.absolutePath, subdirName);
2047
2293
  const subDir = new SubDirectory(
2294
+ seq,
2295
+ new Set([clientId]),
2048
2296
  this.directory,
2049
2297
  this.runtime,
2050
2298
  this.serializer,
@@ -2054,6 +2302,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2054
2302
  this._subdirectories.set(subdirName, subDir);
2055
2303
  this.emit("subDirectoryCreated", subdirName, local, this);
2056
2304
  return true;
2305
+ } else {
2306
+ subdir.clientIds.add(clientId);
2057
2307
  }
2058
2308
  return false;
2059
2309
  }