@fluidframework/map 2.0.0-dev.3.1.0.125672 → 2.0.0-dev.4.2.0.153917

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,49 @@ 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 any subdir in the relative path.
747
+ * @param relativePath - path of sub directory.
748
+ * @returns - true if there is pending delete.
749
+ */
750
+ private isSubDirectoryDeletePending(relativePath: string): boolean {
751
+ const absolutePath = this.makeAbsolute(relativePath);
752
+ if (absolutePath === posix.sep) {
753
+ return false;
754
+ }
755
+ let currentParent = this.root;
756
+ const nodeList = absolutePath.split(posix.sep);
757
+ let start = 1;
758
+ while (start < nodeList.length) {
759
+ const subDirName = nodeList[start];
760
+ if (currentParent.isSubDirectoryDeletePending(subDirName)) {
761
+ return true;
762
+ }
763
+ currentParent = currentParent.getSubDirectory(subDirName) as SubDirectory;
764
+ if (currentParent === undefined) {
765
+ return true;
766
+ }
767
+ start += 1;
768
+ }
769
+ return false;
770
+ }
771
+
708
772
  /**
709
773
  * Set the message handlers for the directory.
710
774
  */
711
775
  private setMessageHandlers(): void {
712
776
  this.messageHandlers.set("clear", {
713
- process: (op: IDirectoryClearOperation, local, localOpMetadata) => {
777
+ process: (
778
+ msg: ISequencedDocumentMessage,
779
+ op: IDirectoryClearOperation,
780
+ local,
781
+ localOpMetadata,
782
+ ) => {
714
783
  const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
715
- if (subdir) {
716
- subdir.processClearMessage(op, local, localOpMetadata);
784
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
785
+ // as we are going to delete this subDirectory.
786
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
787
+ subdir.processClearMessage(msg, op, local, localOpMetadata);
717
788
  }
718
789
  },
719
790
  submit: (op: IDirectoryClearOperation, localOpMetadata: unknown) => {
@@ -730,10 +801,17 @@ export class SharedDirectory
730
801
  },
731
802
  });
732
803
  this.messageHandlers.set("delete", {
733
- process: (op: IDirectoryDeleteOperation, local, localOpMetadata) => {
804
+ process: (
805
+ msg: ISequencedDocumentMessage,
806
+ op: IDirectoryDeleteOperation,
807
+ local,
808
+ localOpMetadata,
809
+ ) => {
734
810
  const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
735
- if (subdir) {
736
- subdir.processDeleteMessage(op, local, localOpMetadata);
811
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
812
+ // as we are going to delete this subDirectory.
813
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
814
+ subdir.processDeleteMessage(msg, op, local, localOpMetadata);
737
815
  }
738
816
  },
739
817
  submit: (op: IDirectoryDeleteOperation, localOpMetadata: unknown) => {
@@ -752,11 +830,18 @@ export class SharedDirectory
752
830
  },
753
831
  });
754
832
  this.messageHandlers.set("set", {
755
- process: (op: IDirectorySetOperation, local, localOpMetadata) => {
833
+ process: (
834
+ msg: ISequencedDocumentMessage,
835
+ op: IDirectorySetOperation,
836
+ local,
837
+ localOpMetadata,
838
+ ) => {
756
839
  const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
757
- if (subdir) {
840
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
841
+ // as we are going to delete this subDirectory.
842
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
758
843
  const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
759
- subdir.processSetMessage(op, context, local, localOpMetadata);
844
+ subdir.processSetMessage(msg, op, context, local, localOpMetadata);
760
845
  }
761
846
  },
762
847
  submit: (op: IDirectorySetOperation, localOpMetadata: unknown) => {
@@ -775,10 +860,17 @@ export class SharedDirectory
775
860
  });
776
861
 
777
862
  this.messageHandlers.set("createSubDirectory", {
778
- process: (op: IDirectoryCreateSubDirectoryOperation, local, localOpMetadata) => {
863
+ process: (
864
+ msg: ISequencedDocumentMessage,
865
+ op: IDirectoryCreateSubDirectoryOperation,
866
+ local,
867
+ localOpMetadata,
868
+ ) => {
779
869
  const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
780
- if (parentSubdir) {
781
- parentSubdir.processCreateSubDirectoryMessage(op, local, localOpMetadata);
870
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
871
+ // as we are going to delete this subDirectory.
872
+ if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
873
+ parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
782
874
  }
783
875
  },
784
876
  submit: (op: IDirectoryCreateSubDirectoryOperation, localOpMetadata: unknown) => {
@@ -799,10 +891,17 @@ export class SharedDirectory
799
891
  });
800
892
 
801
893
  this.messageHandlers.set("deleteSubDirectory", {
802
- process: (op: IDirectoryDeleteSubDirectoryOperation, local, localOpMetadata) => {
894
+ process: (
895
+ msg: ISequencedDocumentMessage,
896
+ op: IDirectoryDeleteSubDirectoryOperation,
897
+ local,
898
+ localOpMetadata,
899
+ ) => {
803
900
  const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
804
- if (parentSubdir) {
805
- parentSubdir.processDeleteSubDirectoryMessage(op, local, localOpMetadata);
901
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
902
+ // as we are going to delete this subDirectory.
903
+ if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
904
+ parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
806
905
  }
807
906
  },
808
907
  submit: (op: IDirectoryDeleteSubDirectoryOperation, localOpMetadata: unknown) => {
@@ -853,6 +952,7 @@ export class SharedDirectory
853
952
  while (stack.length > 0) {
854
953
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
855
954
  const [currentSubDir, currentSubDirObject] = stack.pop()!;
955
+ currentSubDirObject.ci = currentSubDir.getSerializableCreateInfo();
856
956
  for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
857
957
  if (!currentSubDirObject.storage) {
858
958
  currentSubDirObject.storage = {};
@@ -916,7 +1016,6 @@ interface IClearLocalOpMetadata {
916
1016
  interface ICreateSubDirLocalOpMetadata {
917
1017
  type: "createSubDir";
918
1018
  pendingMessageId: number;
919
- previouslyExisted: boolean;
920
1019
  }
921
1020
 
922
1021
  interface IDeleteSubDirLocalOpMetadata {
@@ -954,8 +1053,7 @@ function isSubDirLocalOpMetadata(metadata: any): metadata is SubDirLocalOpMetada
954
1053
  return (
955
1054
  metadata !== undefined &&
956
1055
  typeof metadata.pendingMessageId === "number" &&
957
- ((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
958
- metadata.type === "deleteSubDir")
1056
+ (metadata.type === "createSubDir" || metadata.type === "deleteSubDir")
959
1057
  );
960
1058
  }
961
1059
 
@@ -966,7 +1064,7 @@ function isDirectoryLocalOpMetadata(metadata: any): metadata is DirectoryLocalOp
966
1064
  (metadata.type === "edit" ||
967
1065
  metadata.type === "deleteSubDir" ||
968
1066
  (metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
969
- (metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"))
1067
+ metadata.type === "createSubDir")
970
1068
  );
971
1069
  }
972
1070
 
@@ -1003,10 +1101,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1003
1101
  private readonly pendingKeys: Map<string, number[]> = new Map();
1004
1102
 
1005
1103
  /**
1006
- * Subdirectories that have been modified locally but not yet ack'd from the server.
1104
+ * Subdirectories that have been created/deleted locally but not yet ack'd from the server.
1007
1105
  */
1008
1106
  private readonly pendingSubDirectories: Map<string, number[]> = new Map();
1009
1107
 
1108
+ /**
1109
+ * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the count
1110
+ * of delete op that are pending or yet to be acked from server.
1111
+ */
1112
+ private readonly pendingDeleteSubDirectoriesCount: Map<string, number> = new Map();
1113
+
1010
1114
  /**
1011
1115
  * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
1012
1116
  */
@@ -1019,12 +1123,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1019
1123
 
1020
1124
  /**
1021
1125
  * Constructor.
1126
+ * @param sequenceNumber - Message seq number at which this was created.
1127
+ * @param clientIds - Ids of client which created this directory.
1022
1128
  * @param directory - Reference back to the SharedDirectory to perform operations
1023
1129
  * @param runtime - The data store runtime this directory is associated with
1024
1130
  * @param serializer - The serializer to serialize / parse handles
1025
1131
  * @param absolutePath - The absolute path of this IDirectory
1026
1132
  */
1027
1133
  public constructor(
1134
+ private sequenceNumber: number,
1135
+ private readonly clientIds: Set<string>,
1028
1136
  private readonly directory: SharedDirectory,
1029
1137
  private readonly runtime: IFluidDataStoreRuntime,
1030
1138
  private readonly serializer: IFluidSerializer,
@@ -1132,22 +1240,29 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1132
1240
  }
1133
1241
 
1134
1242
  // 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)!;
1243
+ const isNew = this.createSubDirectoryCore(
1244
+ subdirName,
1245
+ true,
1246
+ -1,
1247
+ this.runtime.clientId ?? "detached",
1248
+ );
1249
+ const subDir = this._subdirectories.get(subdirName);
1250
+ assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
1139
1251
 
1140
1252
  // If we are not attached, don't submit the op.
1141
1253
  if (!this.directory.isAttached()) {
1142
1254
  return subDir;
1143
1255
  }
1144
1256
 
1145
- const op: IDirectoryCreateSubDirectoryOperation = {
1146
- path: this.absolutePath,
1147
- subdirName,
1148
- type: "createSubDirectory",
1149
- };
1150
- this.submitCreateSubDirectoryMessage(op, !isNew);
1257
+ // Only submit the op, if it is newly created.
1258
+ if (isNew) {
1259
+ const op: IDirectoryCreateSubDirectoryOperation = {
1260
+ path: this.absolutePath,
1261
+ subdirName,
1262
+ type: "createSubDirectory",
1263
+ };
1264
+ this.submitCreateSubDirectoryMessage(op);
1265
+ }
1151
1266
 
1152
1267
  return subDir;
1153
1268
  }
@@ -1181,13 +1296,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1181
1296
  return subDir !== undefined;
1182
1297
  }
1183
1298
 
1184
- const op: IDirectoryDeleteSubDirectoryOperation = {
1185
- path: this.absolutePath,
1186
- subdirName,
1187
- type: "deleteSubDirectory",
1188
- };
1299
+ // Only submit the op, if the directory existed and we deleted it.
1300
+ if (subDir !== undefined) {
1301
+ const op: IDirectoryDeleteSubDirectoryOperation = {
1302
+ path: this.absolutePath,
1303
+ subdirName,
1304
+ type: "deleteSubDirectory",
1305
+ };
1189
1306
 
1190
- this.submitDeleteSubDirectoryMessage(op, subDir);
1307
+ this.submitDeleteSubDirectoryMessage(op, subDir);
1308
+ }
1191
1309
  return subDir !== undefined;
1192
1310
  }
1193
1311
 
@@ -1207,6 +1325,19 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1207
1325
  return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
1208
1326
  }
1209
1327
 
1328
+ /**
1329
+ * This checks if there is pending delete op for local delete for a given child subdirectory.
1330
+ * @param subDirName - directory name.
1331
+ * @returns - true if there is pending delete.
1332
+ */
1333
+ public isSubDirectoryDeletePending(subDirName: string): boolean {
1334
+ const pendingDeleteSubDirectory = this.pendingDeleteSubDirectoriesCount.get(subDirName);
1335
+ if (pendingDeleteSubDirectory !== undefined && pendingDeleteSubDirectory > 0) {
1336
+ return true;
1337
+ }
1338
+ return false;
1339
+ }
1340
+
1210
1341
  /**
1211
1342
  * Deletes the given key from within this IDirectory.
1212
1343
  * @param key - The key to delete
@@ -1337,6 +1468,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1337
1468
 
1338
1469
  /**
1339
1470
  * Process a clear operation.
1471
+ * @param msg - The message from the server to apply.
1340
1472
  * @param op - The op to process
1341
1473
  * @param local - Whether the message originated from the local client
1342
1474
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1344,11 +1476,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1344
1476
  * @internal
1345
1477
  */
1346
1478
  public processClearMessage(
1479
+ msg: ISequencedDocumentMessage,
1347
1480
  op: IDirectoryClearOperation,
1348
1481
  local: boolean,
1349
1482
  localOpMetadata: unknown,
1350
1483
  ): void {
1351
1484
  this.throwIfDisposed();
1485
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1486
+ return;
1487
+ }
1352
1488
  if (local) {
1353
1489
  assert(
1354
1490
  isClearLocalOpMetadata(localOpMetadata),
@@ -1385,6 +1521,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1385
1521
 
1386
1522
  /**
1387
1523
  * Process a delete operation.
1524
+ * @param msg - The message from the server to apply.
1388
1525
  * @param op - The op to process
1389
1526
  * @param local - Whether the message originated from the local client
1390
1527
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1392,12 +1529,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1392
1529
  * @internal
1393
1530
  */
1394
1531
  public processDeleteMessage(
1532
+ msg: ISequencedDocumentMessage,
1395
1533
  op: IDirectoryDeleteOperation,
1396
1534
  local: boolean,
1397
1535
  localOpMetadata: unknown,
1398
1536
  ): void {
1399
1537
  this.throwIfDisposed();
1400
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1538
+ if (
1539
+ !(
1540
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1541
+ this.needProcessStorageOperation(op, local, localOpMetadata)
1542
+ )
1543
+ ) {
1401
1544
  return;
1402
1545
  }
1403
1546
  this.deleteCore(op.key, local);
@@ -1422,6 +1565,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1422
1565
 
1423
1566
  /**
1424
1567
  * Process a set operation.
1568
+ * @param msg - The message from the server to apply.
1425
1569
  * @param op - The op to process
1426
1570
  * @param local - Whether the message originated from the local client
1427
1571
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1429,19 +1573,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1429
1573
  * @internal
1430
1574
  */
1431
1575
  public processSetMessage(
1576
+ msg: ISequencedDocumentMessage,
1432
1577
  op: IDirectorySetOperation,
1433
1578
  context: ILocalValue | undefined,
1434
1579
  local: boolean,
1435
1580
  localOpMetadata: unknown,
1436
1581
  ): void {
1437
1582
  this.throwIfDisposed();
1438
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1583
+ if (
1584
+ !(
1585
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1586
+ this.needProcessStorageOperation(op, local, localOpMetadata)
1587
+ )
1588
+ ) {
1439
1589
  return;
1440
1590
  }
1441
1591
 
1442
1592
  // needProcessStorageOperation should have returned false if local is true
1443
1593
  // so we can assume context is not undefined
1444
-
1445
1594
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1446
1595
  this.setCore(op.key, context!, local);
1447
1596
  }
@@ -1470,6 +1619,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1470
1619
  }
1471
1620
  /**
1472
1621
  * Process a create subdirectory operation.
1622
+ * @param msg - The message from the server to apply.
1473
1623
  * @param op - The op to process
1474
1624
  * @param local - Whether the message originated from the local client
1475
1625
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1477,15 +1627,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1477
1627
  * @internal
1478
1628
  */
1479
1629
  public processCreateSubDirectoryMessage(
1630
+ msg: ISequencedDocumentMessage,
1480
1631
  op: IDirectoryCreateSubDirectoryOperation,
1481
1632
  local: boolean,
1482
1633
  localOpMetadata: unknown,
1483
1634
  ): void {
1484
1635
  this.throwIfDisposed();
1485
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1636
+ if (!this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)) {
1486
1637
  return;
1487
1638
  }
1488
- this.createSubDirectoryCore(op.subdirName, local);
1639
+ this.createSubDirectoryCore(op.subdirName, local, msg.sequenceNumber, msg.clientId);
1489
1640
  }
1490
1641
 
1491
1642
  /**
@@ -1498,19 +1649,19 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1498
1649
  ): ICreateSubDirLocalOpMetadata {
1499
1650
  this.throwIfDisposed();
1500
1651
  // Create the sub directory locally first.
1501
- const isNew = this.createSubDirectoryCore(op.subdirName, true);
1652
+ this.createSubDirectoryCore(op.subdirName, true, -1, this.runtime.clientId ?? "detached");
1502
1653
  const newMessageId = this.getSubDirMessageId(op);
1503
1654
 
1504
1655
  const localOpMetadata: ICreateSubDirLocalOpMetadata = {
1505
1656
  type: "createSubDir",
1506
1657
  pendingMessageId: newMessageId,
1507
- previouslyExisted: !isNew,
1508
1658
  };
1509
1659
  return localOpMetadata;
1510
1660
  }
1511
1661
 
1512
1662
  /**
1513
1663
  * Process a delete subdirectory operation.
1664
+ * @param msg - The message from the server to apply.
1514
1665
  * @param op - The op to process
1515
1666
  * @param local - Whether the message originated from the local client
1516
1667
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
@@ -1518,12 +1669,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1518
1669
  * @internal
1519
1670
  */
1520
1671
  public processDeleteSubDirectoryMessage(
1672
+ msg: ISequencedDocumentMessage,
1521
1673
  op: IDirectoryDeleteSubDirectoryOperation,
1522
1674
  local: boolean,
1523
1675
  localOpMetadata: unknown,
1524
1676
  ): void {
1525
1677
  this.throwIfDisposed();
1526
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1678
+ if (
1679
+ !(
1680
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1681
+ this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)
1682
+ )
1683
+ ) {
1527
1684
  return;
1528
1685
  }
1529
1686
  this.deleteSubDirectoryCore(op.subdirName, local);
@@ -1652,25 +1809,24 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1652
1809
  } else {
1653
1810
  this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
1654
1811
  }
1812
+ if (op.type === "deleteSubDirectory") {
1813
+ const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName) ?? 0;
1814
+ this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count + 1);
1815
+ }
1655
1816
  return newMessageId;
1656
1817
  }
1657
1818
 
1658
1819
  /**
1659
1820
  * Submit a create subdirectory operation.
1660
1821
  * @param op - The operation
1661
- * @param prevExisted - Whether the subdirectory existed before the op
1662
1822
  */
1663
- private submitCreateSubDirectoryMessage(
1664
- op: IDirectorySubDirectoryOperation,
1665
- prevExisted: boolean,
1666
- ): void {
1823
+ private submitCreateSubDirectoryMessage(op: IDirectorySubDirectoryOperation): void {
1667
1824
  this.throwIfDisposed();
1668
1825
  const newMessageId = this.getSubDirMessageId(op);
1669
1826
 
1670
1827
  const localOpMetadata: ICreateSubDirLocalOpMetadata = {
1671
1828
  type: "createSubDir",
1672
1829
  pendingMessageId: newMessageId,
1673
- previouslyExisted: prevExisted,
1674
1830
  };
1675
1831
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1676
1832
  }
@@ -1723,7 +1879,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1723
1879
  }
1724
1880
 
1725
1881
  if (localOpMetadata.type === "createSubDir") {
1726
- this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
1882
+ this.submitCreateSubDirectoryMessage(op);
1727
1883
  } else {
1728
1884
  this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1729
1885
  }
@@ -1746,6 +1902,15 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1746
1902
  }
1747
1903
  }
1748
1904
 
1905
+ public getSerializableCreateInfo() {
1906
+ this.throwIfDisposed();
1907
+ const createInfo: ICreateInfo = {
1908
+ csn: this.sequenceNumber,
1909
+ ccIds: Array.from(this.clientIds),
1910
+ };
1911
+ return createInfo;
1912
+ }
1913
+
1749
1914
  /**
1750
1915
  * Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
1751
1916
  * @param key - The key to populate
@@ -1838,9 +2003,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1838
2003
  localOpMetadata.pendingMessageId,
1839
2004
  );
1840
2005
  } else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1841
- if (!localOpMetadata.previouslyExisted) {
1842
- this.deleteSubDirectoryCore(op.subdirName as string, true);
1843
- }
2006
+ this.deleteSubDirectoryCore(op.subdirName as string, true);
1844
2007
 
1845
2008
  this.rollbackPendingMessageId(
1846
2009
  this.pendingSubDirectories,
@@ -1860,6 +2023,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1860
2023
  op.subdirName as string,
1861
2024
  localOpMetadata.pendingMessageId,
1862
2025
  );
2026
+ const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
2027
+ assert(count !== undefined && count > 0, 0x5ab /* should have record for delete op */);
2028
+ this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
2029
+ if (count === 1) {
2030
+ this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
2031
+ }
1863
2032
  } else {
1864
2033
  throw new Error("Unsupported op for rollback");
1865
2034
  }
@@ -1898,7 +2067,23 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1898
2067
  localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
1899
2068
  0x010 /* "Received out of order storage op when there is an unackd clear message" */,
1900
2069
  );
2070
+ // Remove all pendingMessageIds lower than first pendingClearMessageId.
2071
+ const lowestPendingClearMessageId = this.pendingClearMessageIds[0];
2072
+ const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
2073
+ if (pendingKeyMessageIdArray !== undefined) {
2074
+ let index = 0;
2075
+ while (pendingKeyMessageIdArray[index] < lowestPendingClearMessageId) {
2076
+ index += 1;
2077
+ }
2078
+ const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
2079
+ if (newPendingKeyMessageId.length === 0) {
2080
+ this.pendingKeys.delete(op.key);
2081
+ } else {
2082
+ this.pendingKeys.set(op.key, newPendingKeyMessageId);
2083
+ }
2084
+ }
1901
2085
  }
2086
+
1902
2087
  // If I have a NACK clear, we can ignore all ops.
1903
2088
  return false;
1904
2089
  }
@@ -1930,6 +2115,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1930
2115
  return !local;
1931
2116
  }
1932
2117
 
2118
+ /**
2119
+ * This return true if the message is for the current instance of this sub directory. As the sub directory
2120
+ * can be deleted and created again, then this finds if the message is for current instance of directory or not.
2121
+ * @param msg - message for the directory
2122
+ */
2123
+ private isMessageForCurrentInstanceOfSubDirectory(msg: ISequencedDocumentMessage) {
2124
+ // If the message is either from the creator of directory or this directory was created when
2125
+ // container was detached or in case this directory is already live(known to other clients)
2126
+ // and the op was created after the directory was created then apply this op.
2127
+ return (
2128
+ this.clientIds.has(msg.clientId) ||
2129
+ this.clientIds.has("detached") ||
2130
+ (this.sequenceNumber !== -1 && this.sequenceNumber <= msg.referenceSequenceNumber)
2131
+ );
2132
+ }
2133
+
1933
2134
  /**
1934
2135
  * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1935
2136
  * not process the incoming operation.
@@ -1941,6 +2142,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1941
2142
  * @returns True if the operation should be processed, false otherwise
1942
2143
  */
1943
2144
  private needProcessSubDirectoryOperation(
2145
+ msg: ISequencedDocumentMessage,
1944
2146
  op: IDirectorySubDirectoryOperation,
1945
2147
  local: boolean,
1946
2148
  localOpMetadata: unknown,
@@ -1962,6 +2164,44 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1962
2164
  if (pendingMessageIds.length === 0) {
1963
2165
  this.pendingSubDirectories.delete(op.subdirName);
1964
2166
  }
2167
+ if (op.type === "deleteSubDirectory") {
2168
+ const count = this.pendingDeleteSubDirectoriesCount.get(op.subdirName);
2169
+ assert(
2170
+ count !== undefined && count > 0,
2171
+ 0x5ac /* should have record for delete op */,
2172
+ );
2173
+ this.pendingDeleteSubDirectoriesCount.set(op.subdirName, count - 1);
2174
+ if (count === 1) {
2175
+ this.pendingDeleteSubDirectoriesCount.delete(op.subdirName);
2176
+ }
2177
+ }
2178
+ } else if (op.type === "deleteSubDirectory") {
2179
+ // If this is remote delete op and we have keys in this subDirectory, then we need to delete these
2180
+ // keys except the pending ones as they will be sequenced after this delete.
2181
+ const subDirectory = this._subdirectories.get(op.subdirName);
2182
+ if (subDirectory) {
2183
+ subDirectory.clearExceptPendingKeys(local);
2184
+ // In case of remote delete op, we need to reset the creation seq number and client ids of
2185
+ // creators as the previous directory is getting deleted and we will initialize again when
2186
+ // we will receive op for the create again.
2187
+ subDirectory.sequenceNumber = -1;
2188
+ subDirectory.clientIds.clear();
2189
+ }
2190
+ }
2191
+ if (op.type === "createSubDirectory") {
2192
+ const dir = this._subdirectories.get(op.subdirName);
2193
+ if (dir?.sequenceNumber === -1) {
2194
+ // Only set the seq on the first message, could be more
2195
+ dir.sequenceNumber = msg.sequenceNumber;
2196
+ }
2197
+ // The client created the dir at or after the dirs seq, so list its client id as a creator.
2198
+ if (
2199
+ dir !== undefined &&
2200
+ !dir.clientIds.has(msg.clientId) &&
2201
+ dir.sequenceNumber <= msg.sequenceNumber
2202
+ ) {
2203
+ dir.clientIds.add(msg.clientId);
2204
+ }
1965
2205
  }
1966
2206
  return false;
1967
2207
  }
@@ -1978,8 +2218,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1978
2218
  const temp = new Map<string, ILocalValue>();
1979
2219
 
1980
2220
  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)!);
2221
+ const value = this._storage.get(key);
2222
+ // If this key is already deleted, then we don't need to add it again.
2223
+ if (value !== undefined) {
2224
+ temp.set(key, value);
2225
+ }
1983
2226
  }
1984
2227
 
1985
2228
  this.clearCore(local);
@@ -2039,12 +2282,22 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2039
2282
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
2040
2283
  * @param subdirName - The name of the subdirectory being created
2041
2284
  * @param local - Whether the message originated from the local client
2285
+ * @param seq - Sequence number at which this directory is created
2286
+ * @param clientId - Id of client which created this directory.
2042
2287
  * @returns - True if is newly created, false if it already existed.
2043
2288
  */
2044
- private createSubDirectoryCore(subdirName: string, local: boolean): boolean {
2045
- if (!this._subdirectories.has(subdirName)) {
2289
+ private createSubDirectoryCore(
2290
+ subdirName: string,
2291
+ local: boolean,
2292
+ seq: number,
2293
+ clientId: string,
2294
+ ): boolean {
2295
+ const subdir = this._subdirectories.get(subdirName);
2296
+ if (subdir === undefined) {
2046
2297
  const absolutePath = posix.join(this.absolutePath, subdirName);
2047
2298
  const subDir = new SubDirectory(
2299
+ seq,
2300
+ new Set([clientId]),
2048
2301
  this.directory,
2049
2302
  this.runtime,
2050
2303
  this.serializer,
@@ -2054,6 +2307,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2054
2307
  this._subdirectories.set(subdirName, subDir);
2055
2308
  this.emit("subDirectoryCreated", subdirName, local, this);
2056
2309
  return true;
2310
+ } else {
2311
+ subdir.clientIds.add(clientId);
2057
2312
  }
2058
2313
  return false;
2059
2314
  }