@fluidframework/map 2.0.0-internal.7.2.2 → 2.0.0-internal.7.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/api-extractor-lint.json +13 -0
  3. package/api-extractor.json +9 -1
  4. package/api-report/map.api.md +42 -44
  5. package/dist/{directory.js → directory.cjs} +228 -41
  6. package/dist/directory.cjs.map +1 -0
  7. package/dist/directory.d.ts +499 -38
  8. package/dist/directory.d.ts.map +1 -1
  9. package/dist/{index.js → index.cjs} +4 -4
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/{interfaces.js → interfaces.cjs} +1 -1
  12. package/dist/interfaces.cjs.map +1 -0
  13. package/dist/interfaces.d.ts +10 -20
  14. package/dist/interfaces.d.ts.map +1 -1
  15. package/dist/{internalInterfaces.js → internalInterfaces.cjs} +1 -1
  16. package/dist/internalInterfaces.cjs.map +1 -0
  17. package/dist/{localValues.js → localValues.cjs} +2 -3
  18. package/dist/localValues.cjs.map +1 -0
  19. package/dist/localValues.d.ts +2 -4
  20. package/dist/localValues.d.ts.map +1 -1
  21. package/dist/map-alpha.d.ts +979 -0
  22. package/dist/map-beta.d.ts +119 -0
  23. package/dist/map-public.d.ts +119 -0
  24. package/dist/map-untrimmed.d.ts +993 -0
  25. package/dist/{map.js → map.cjs} +5 -13
  26. package/dist/map.cjs.map +1 -0
  27. package/dist/map.d.ts +2 -10
  28. package/dist/map.d.ts.map +1 -1
  29. package/dist/{mapKernel.js → mapKernel.cjs} +2 -2
  30. package/dist/mapKernel.cjs.map +1 -0
  31. package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
  32. package/dist/packageVersion.cjs.map +1 -0
  33. package/dist/packageVersion.d.ts +1 -1
  34. package/dist/tsdoc-metadata.json +1 -1
  35. package/lib/directory.d.ts +499 -38
  36. package/lib/directory.d.ts.map +1 -1
  37. package/lib/{directory.js → directory.mjs} +228 -41
  38. package/lib/directory.mjs.map +1 -0
  39. package/lib/index.d.ts +4 -15
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.mjs +8 -0
  42. package/lib/index.mjs.map +1 -0
  43. package/lib/interfaces.d.ts +10 -20
  44. package/lib/interfaces.d.ts.map +1 -1
  45. package/lib/{interfaces.js → interfaces.mjs} +1 -1
  46. package/lib/interfaces.mjs.map +1 -0
  47. package/lib/internalInterfaces.d.ts +2 -2
  48. package/lib/internalInterfaces.d.ts.map +1 -1
  49. package/lib/{internalInterfaces.js → internalInterfaces.mjs} +1 -1
  50. package/{dist/internalInterfaces.js.map → lib/internalInterfaces.mjs.map} +1 -1
  51. package/lib/localValues.d.ts +3 -5
  52. package/lib/localValues.d.ts.map +1 -1
  53. package/lib/{localValues.js → localValues.mjs} +2 -3
  54. package/lib/localValues.mjs.map +1 -0
  55. package/lib/map-alpha.d.ts +979 -0
  56. package/lib/map-beta.d.ts +119 -0
  57. package/lib/map-public.d.ts +119 -0
  58. package/lib/map-untrimmed.d.ts +993 -0
  59. package/lib/map.d.ts +3 -11
  60. package/lib/map.d.ts.map +1 -1
  61. package/lib/{map.js → map.mjs} +5 -13
  62. package/lib/map.mjs.map +1 -0
  63. package/lib/mapKernel.d.ts +2 -2
  64. package/lib/mapKernel.d.ts.map +1 -1
  65. package/lib/{mapKernel.js → mapKernel.mjs} +2 -2
  66. package/lib/mapKernel.mjs.map +1 -0
  67. package/lib/packageVersion.d.ts +1 -1
  68. package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
  69. package/lib/packageVersion.mjs.map +1 -0
  70. package/map.test-files.tar +0 -0
  71. package/package.json +54 -33
  72. package/src/directory.ts +280 -62
  73. package/src/interfaces.ts +10 -20
  74. package/src/localValues.ts +2 -4
  75. package/src/map.ts +2 -10
  76. package/src/packageVersion.ts +1 -1
  77. package/tsc-multi.test.json +4 -0
  78. package/tsconfig.json +6 -5
  79. package/dist/directory.js.map +0 -1
  80. package/dist/index.js.map +0 -1
  81. package/dist/interfaces.js.map +0 -1
  82. package/dist/localValues.js.map +0 -1
  83. package/dist/map.js.map +0 -1
  84. package/dist/mapKernel.js.map +0 -1
  85. package/dist/packageVersion.js.map +0 -1
  86. package/lib/directory.js.map +0 -1
  87. package/lib/index.js +0 -19
  88. package/lib/index.js.map +0 -1
  89. package/lib/interfaces.js.map +0 -1
  90. package/lib/internalInterfaces.js.map +0 -1
  91. package/lib/localValues.js.map +0 -1
  92. package/lib/map.js.map +0 -1
  93. package/lib/mapKernel.js.map +0 -1
  94. package/lib/packageVersion.js.map +0 -1
  95. package/tsconfig.esnext.json +0 -7
package/src/directory.ts CHANGED
@@ -19,6 +19,7 @@ import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtim
19
19
  import { IFluidSerializer, SharedObject, ValueType } from "@fluidframework/shared-object-base";
20
20
  import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
21
21
  import * as path from "path-browserify";
22
+ import { RedBlackTree } from "@fluidframework/merge-tree";
22
23
  import {
23
24
  IDirectory,
24
25
  IDirectoryEvents,
@@ -70,8 +71,7 @@ interface IDirectoryMessageHandler {
70
71
 
71
72
  /**
72
73
  * Operation indicating a value should be set for a key.
73
- *
74
- * @public
74
+ * @alpha
75
75
  */
76
76
  export interface IDirectorySetOperation {
77
77
  /**
@@ -98,8 +98,7 @@ export interface IDirectorySetOperation {
98
98
 
99
99
  /**
100
100
  * Operation indicating a key should be deleted from the directory.
101
- *
102
- * @public
101
+ * @alpha
103
102
  */
104
103
  export interface IDirectoryDeleteOperation {
105
104
  /**
@@ -120,15 +119,13 @@ export interface IDirectoryDeleteOperation {
120
119
 
121
120
  /**
122
121
  * An operation on a specific key within a directory.
123
- *
124
- * @public
122
+ * @alpha
125
123
  */
126
124
  export type IDirectoryKeyOperation = IDirectorySetOperation | IDirectoryDeleteOperation;
127
125
 
128
126
  /**
129
127
  * Operation indicating the directory should be cleared.
130
- *
131
- * @public
128
+ * @alpha
132
129
  */
133
130
  export interface IDirectoryClearOperation {
134
131
  /**
@@ -144,15 +141,13 @@ export interface IDirectoryClearOperation {
144
141
 
145
142
  /**
146
143
  * An operation on one or more of the keys within a directory.
147
- *
148
- * @public
144
+ * @alpha
149
145
  */
150
146
  export type IDirectoryStorageOperation = IDirectoryKeyOperation | IDirectoryClearOperation;
151
147
 
152
148
  /**
153
149
  * Operation indicating a subdirectory should be created.
154
- *
155
- * @public
150
+ * @alpha
156
151
  */
157
152
  export interface IDirectoryCreateSubDirectoryOperation {
158
153
  /**
@@ -173,8 +168,7 @@ export interface IDirectoryCreateSubDirectoryOperation {
173
168
 
174
169
  /**
175
170
  * Operation indicating a subdirectory should be deleted.
176
- *
177
- * @public
171
+ * @alpha
178
172
  */
179
173
  export interface IDirectoryDeleteSubDirectoryOperation {
180
174
  /**
@@ -195,8 +189,7 @@ export interface IDirectoryDeleteSubDirectoryOperation {
195
189
 
196
190
  /**
197
191
  * An operation on the subdirectories within a directory.
198
- *
199
- * @public
192
+ * @alpha
200
193
  */
201
194
  export type IDirectorySubDirectoryOperation =
202
195
  | IDirectoryCreateSubDirectoryOperation
@@ -204,15 +197,13 @@ export type IDirectorySubDirectoryOperation =
204
197
 
205
198
  /**
206
199
  * Any operation on a directory.
207
- *
208
- * @public
200
+ * @alpha
209
201
  */
210
202
  export type IDirectoryOperation = IDirectoryStorageOperation | IDirectorySubDirectoryOperation;
211
203
 
212
204
  /**
213
205
  * Create info for the subdirectory.
214
- *
215
- * @public
206
+ * @alpha
216
207
  */
217
208
  export interface ICreateInfo {
218
209
  /**
@@ -233,8 +224,7 @@ export interface ICreateInfo {
233
224
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
234
225
  * | JSON.stringify}, direct result from
235
226
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse}.
236
- *
237
- * @public
227
+ * @alpha
238
228
  */
239
229
  export interface IDirectoryDataObject {
240
230
  /**
@@ -279,7 +269,7 @@ export interface IDirectoryNewStorageFormat {
279
269
  * {@link @fluidframework/datastore-definitions#IChannelFactory} for {@link SharedDirectory}.
280
270
  *
281
271
  * @sealed
282
- * @public
272
+ * @alpha
283
273
  */
284
274
  export class DirectoryFactory implements IChannelFactory {
285
275
  /**
@@ -336,6 +326,115 @@ export class DirectoryFactory implements IChannelFactory {
336
326
  }
337
327
  }
338
328
 
329
+ /**
330
+ * The comparator essentially performs the following procedure to determine the order of subdirectory creation:
331
+ * 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
332
+ * the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
333
+ *
334
+ * 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
335
+ * - If A and B have different 'seq', they are ordered based on 'seq', and the one with the lower 'seq' will be positioned ahead. Notably this rule
336
+ * should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
337
+ * - In the case where A and B have equal 'seq', the one with the lower 'clientSeq' will be positioned ahead. This scenario occurs when grouped
338
+ * batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
339
+ *
340
+ * 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
341
+ * - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
342
+ * acknowledged by the server earlier.
343
+ * - If A and B have equal 'seq', the one with lower 'clientSeq' will be placed at the front. This scenario suggests that both subdirectories A
344
+ * and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
345
+ *
346
+ * 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
347
+ * purpose of ordering.
348
+ */
349
+ const seqDataComparator = (a: SequenceData, b: SequenceData) => {
350
+ if (isAcknowledgedOrDetached(a)) {
351
+ if (isAcknowledgedOrDetached(b)) {
352
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
354
+ } else {
355
+ return -1;
356
+ }
357
+ } else {
358
+ if (!isAcknowledgedOrDetached(b)) {
359
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
360
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
361
+ } else {
362
+ return 1;
363
+ }
364
+ }
365
+ };
366
+
367
+ function isAcknowledgedOrDetached(seqData: SequenceData) {
368
+ return seqData.seq >= 0;
369
+ }
370
+
371
+ /**
372
+ * The combination of sequence numebr and client sequence number of a subdirectory
373
+ */
374
+ interface SequenceData {
375
+ seq: number;
376
+ clientSeq?: number;
377
+ }
378
+
379
+ /**
380
+ * A utility class for tracking associations between keys and their creation indices.
381
+ * This is relevant to support map iteration in insertion order, see
382
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
383
+ *
384
+ * TODO: It can be combined with the creation tracker utilized in SharedMap
385
+ */
386
+ class DirectoryCreationTracker {
387
+ readonly indexToKey: RedBlackTree<SequenceData, string>;
388
+
389
+ readonly keyToIndex: Map<string, SequenceData>;
390
+
391
+ constructor() {
392
+ this.indexToKey = new RedBlackTree<SequenceData, string>(seqDataComparator);
393
+ this.keyToIndex = new Map<string, SequenceData>();
394
+ }
395
+
396
+ set(key: string, seqData: SequenceData): void {
397
+ this.indexToKey.put(seqData, key);
398
+ this.keyToIndex.set(key, seqData);
399
+ }
400
+
401
+ has(keyOrSeqData: string | SequenceData): boolean {
402
+ return typeof keyOrSeqData === "string"
403
+ ? this.keyToIndex.has(keyOrSeqData)
404
+ : this.indexToKey.get(keyOrSeqData) !== undefined;
405
+ }
406
+
407
+ delete(keyOrSeqData: string | SequenceData): void {
408
+ if (this.has(keyOrSeqData)) {
409
+ if (typeof keyOrSeqData === "string") {
410
+ const seqData = this.keyToIndex.get(keyOrSeqData) as SequenceData;
411
+ this.keyToIndex.delete(keyOrSeqData);
412
+ this.indexToKey.remove(seqData);
413
+ } else {
414
+ const key = this.indexToKey.get(keyOrSeqData)?.data as string;
415
+ this.indexToKey.remove(keyOrSeqData);
416
+ this.keyToIndex.delete(key);
417
+ }
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Retrieves all subdirectories with creation order that satisfy an optional constraint function.
423
+ * @param constraint - An optional constraint function that filters keys.
424
+ * @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
425
+ */
426
+ keys(constraint?: (key: string) => boolean): string[] {
427
+ const keys: string[] = [];
428
+ this.indexToKey.mapRange((node) => {
429
+ if (!constraint || constraint(node.data)) {
430
+ keys.push(node.data);
431
+ }
432
+ return true;
433
+ }, keys);
434
+ return keys;
435
+ }
436
+ }
437
+
339
438
  /**
340
439
  * {@inheritDoc ISharedDirectory}
341
440
  *
@@ -348,7 +447,7 @@ export class DirectoryFactory implements IChannelFactory {
348
447
  * ```
349
448
  *
350
449
  * @sealed
351
- * @public
450
+ * @alpha
352
451
  */
353
452
  export class SharedDirectory
354
453
  extends SharedObject<ISharedDirectoryEvents>
@@ -386,16 +485,14 @@ export class SharedDirectory
386
485
  return this.root.absolutePath;
387
486
  }
388
487
 
389
- /**
390
- * @internal
391
- */
488
+ /***/
392
489
  public readonly localValueMaker: LocalValueMaker;
393
490
 
394
491
  /**
395
492
  * Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
396
493
  */
397
494
  private readonly root: SubDirectory = new SubDirectory(
398
- 0,
495
+ { seq: 0, clientSeq: 0 },
399
496
  new Set(),
400
497
  this,
401
498
  this.runtime,
@@ -605,7 +702,6 @@ export class SharedDirectory
605
702
 
606
703
  /**
607
704
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
608
- * @internal
609
705
  */
610
706
  protected summarizeCore(
611
707
  serializer: IFluidSerializer,
@@ -619,7 +715,6 @@ export class SharedDirectory
619
715
  * @param op - Op to submit
620
716
  * @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
621
717
  * this op while it has not been ack'd. This will be sent when we receive this op back from the server.
622
- * @internal
623
718
  */
624
719
  public submitDirectoryMessage(op: IDirectoryOperation, localOpMetadata: unknown): void {
625
720
  this.submitLocalMessage(op, localOpMetadata);
@@ -627,13 +722,11 @@ export class SharedDirectory
627
722
 
628
723
  /**
629
724
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
630
- * @internal
631
725
  */
632
726
  protected onDisconnect(): void {}
633
727
 
634
728
  /**
635
729
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
636
- * @internal
637
730
  */
638
731
  protected reSubmitCore(content: unknown, localOpMetadata: unknown): void {
639
732
  const message = content as IDirectoryOperation;
@@ -644,7 +737,6 @@ export class SharedDirectory
644
737
 
645
738
  /**
646
739
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
647
- * @internal
648
740
  */
649
741
  protected async loadCore(storage: IChannelStorageService): Promise<void> {
650
742
  const data = await readAndParse(storage, snapshotFileName);
@@ -667,26 +759,46 @@ export class SharedDirectory
667
759
  /**
668
760
  * Populate the directory with the given directory data.
669
761
  * @param data - A JSON string containing serialized directory data
670
- * @internal
671
762
  */
672
763
  protected populate(data: IDirectoryDataObject): void {
673
764
  const stack: [SubDirectory, IDirectoryDataObject][] = [];
674
765
  stack.push([this.root, data]);
766
+
675
767
  while (stack.length > 0) {
676
768
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
677
769
  const [currentSubDir, currentSubDirObject] = stack.pop()!;
678
770
  if (currentSubDirObject.subdirectories) {
771
+ // Utilize a map to store the seq -> clientSeq for the newly created subdirectory
772
+ const tempSeqNums = new Map<number, number>();
679
773
  for (const [subdirName, subdirObject] of Object.entries(
680
774
  currentSubDirObject.subdirectories,
681
775
  )) {
682
776
  let newSubDir = currentSubDir.getSubDirectory(subdirName) as SubDirectory;
777
+ let seqData: SequenceData;
683
778
  if (!newSubDir) {
684
779
  const createInfo = subdirObject.ci;
685
- newSubDir = new SubDirectory(
780
+ // We do not store the client sequence number in the storage because the order has already been
781
+ // guaranteed during the serialization process. As a result, it is only essential to utilize the
782
+ // "fake" client sequence number to signify the loading order, and there is no need to retain
783
+ // the actual client sequence number at this point.
784
+ if (createInfo !== undefined && createInfo.csn > -1) {
686
785
  // If csn is -1, then initialize it with 0, otherwise we will never process ops for this
687
786
  // sub directory. This could be done at serialization time too, but we need to maintain
688
787
  // back compat too and also we will actually know the state when it was serialized.
689
- createInfo !== undefined && createInfo.csn > -1 ? createInfo.csn : 0,
788
+ if (!tempSeqNums.has(createInfo.csn)) {
789
+ tempSeqNums.set(createInfo.csn, 0);
790
+ }
791
+ let fakeClientSeq = tempSeqNums.get(createInfo.csn) as number;
792
+ seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
793
+ tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
794
+ } else {
795
+ seqData = {
796
+ seq: 0,
797
+ clientSeq: ++currentSubDir.localCreationSeq,
798
+ };
799
+ }
800
+ newSubDir = new SubDirectory(
801
+ seqData,
690
802
  createInfo !== undefined
691
803
  ? new Set<string>(createInfo.ccIds)
692
804
  : new Set(),
@@ -696,6 +808,10 @@ export class SharedDirectory
696
808
  posix.join(currentSubDir.absolutePath, subdirName),
697
809
  );
698
810
  currentSubDir.populateSubDirectory(subdirName, newSubDir);
811
+ // Record the newly inserted subdirectory to the creation tracker
812
+ currentSubDir.ackedCreationSeqTracker.set(subdirName, {
813
+ ...seqData,
814
+ });
699
815
  }
700
816
  stack.push([newSubDir, subdirObject]);
701
817
  }
@@ -716,7 +832,6 @@ export class SharedDirectory
716
832
 
717
833
  /**
718
834
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
719
- * @internal
720
835
  */
721
836
  protected processCore(
722
837
  message: ISequencedDocumentMessage,
@@ -733,7 +848,6 @@ export class SharedDirectory
733
848
 
734
849
  /**
735
850
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
736
- * @internal
737
851
  */
738
852
  protected rollback(content: unknown, localOpMetadata: unknown): void {
739
853
  const op: IDirectoryOperation = content as IDirectoryOperation;
@@ -957,7 +1071,6 @@ export class SharedDirectory
957
1071
 
958
1072
  /**
959
1073
  * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
960
- * @internal
961
1074
  */
962
1075
  protected applyStashedOp(op: unknown): unknown {
963
1076
  const handler = this.messageHandlers.get((op as IDirectoryOperation).type);
@@ -1057,7 +1170,7 @@ interface IDeleteSubDirLocalOpMetadata {
1057
1170
  }
1058
1171
 
1059
1172
  type SubDirLocalOpMetadata = ICreateSubDirLocalOpMetadata | IDeleteSubDirLocalOpMetadata;
1060
- type DirectoryLocalOpMetadata =
1173
+ export type DirectoryLocalOpMetadata =
1061
1174
  | IClearLocalOpMetadata
1062
1175
  | IKeyEditLocalOpMetadata
1063
1176
  | SubDirLocalOpMetadata;
@@ -1159,6 +1272,23 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1159
1272
  */
1160
1273
  private readonly pendingClearMessageIds: number[] = [];
1161
1274
 
1275
+ /**
1276
+ * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
1277
+ * of the creation order.
1278
+ */
1279
+ public localCreationSeq: number = 0;
1280
+
1281
+ /**
1282
+ * Maintains a bidirectional association between ack'd subdirectories and their seqData.
1283
+ * This helps to ensure iteration order which is consistent with the JS map spec.
1284
+ */
1285
+ public readonly ackedCreationSeqTracker: DirectoryCreationTracker;
1286
+
1287
+ /**
1288
+ * Similar to {@link ackedCreationSeqTracker}, but for local (unacked) entries.
1289
+ */
1290
+ public readonly localCreationSeqTracker: DirectoryCreationTracker;
1291
+
1162
1292
  /**
1163
1293
  * Constructor.
1164
1294
  * @param sequenceNumber - Message seq number at which this was created.
@@ -1169,7 +1299,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1169
1299
  * @param absolutePath - The absolute path of this IDirectory
1170
1300
  */
1171
1301
  public constructor(
1172
- private sequenceNumber: number,
1302
+ private readonly seqData: SequenceData,
1173
1303
  private readonly clientIds: Set<string>,
1174
1304
  private readonly directory: SharedDirectory,
1175
1305
  private readonly runtime: IFluidDataStoreRuntime,
@@ -1177,6 +1307,8 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1177
1307
  public readonly absolutePath: string,
1178
1308
  ) {
1179
1309
  super();
1310
+ this.localCreationSeqTracker = new DirectoryCreationTracker();
1311
+ this.ackedCreationSeqTracker = new DirectoryCreationTracker();
1180
1312
  }
1181
1313
 
1182
1314
  public dispose(error?: Error): void {
@@ -1306,14 +1438,18 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1306
1438
  }
1307
1439
 
1308
1440
  /**
1309
- * @returns A sequenceNumber which should be used for local changes.
1441
+ * @returns The Sequence Data which should be used for local changes.
1310
1442
  * @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
1311
1443
  * only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
1312
1444
  * last set while detached will now be known to any new client, until they are changed).
1445
+ *
1446
+ * The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
1313
1447
  * TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
1314
1448
  */
1315
- private getLocalSeq(): number {
1316
- return this.directory.isAttached() ? -1 : 0;
1449
+ private getLocalSeq(): SequenceData {
1450
+ return this.directory.isAttached()
1451
+ ? { seq: -1, clientSeq: ++this.localCreationSeq }
1452
+ : { seq: 0, clientSeq: ++this.localCreationSeq };
1317
1453
  }
1318
1454
 
1319
1455
  /**
@@ -1363,7 +1499,35 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1363
1499
  */
1364
1500
  public subdirectories(): IterableIterator<[string, IDirectory]> {
1365
1501
  this.throwIfDisposed();
1366
- return this._subdirectories.entries();
1502
+ const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
1503
+ const localSubdirsInOrder = this.localCreationSeqTracker.keys(
1504
+ (key) => !this.ackedCreationSeqTracker.has(key),
1505
+ );
1506
+
1507
+ const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1508
+
1509
+ assert(
1510
+ subdirNames.length === this._subdirectories.size,
1511
+ 0x85c /* The count of keys for iteration should be consistent with the size of actual data */,
1512
+ );
1513
+
1514
+ const entriesIterator = {
1515
+ index: 0,
1516
+ dirs: this._subdirectories,
1517
+ next(): IteratorResult<[string, any]> {
1518
+ if (this.index < subdirNames.length) {
1519
+ const subdirName = subdirNames[this.index++];
1520
+ const subdir = this.dirs.get(subdirName);
1521
+ return { value: [subdirName, subdir], done: false };
1522
+ }
1523
+ return { value: undefined, done: true };
1524
+ },
1525
+ [Symbol.iterator](): IterableIterator<[string, any]> {
1526
+ return this;
1527
+ },
1528
+ };
1529
+
1530
+ return entriesIterator;
1367
1531
  }
1368
1532
 
1369
1533
  /**
@@ -1690,7 +1854,12 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1690
1854
  return;
1691
1855
  }
1692
1856
  assertNonNullClientId(msg.clientId);
1693
- this.createSubDirectoryCore(op.subdirName, local, msg.sequenceNumber, msg.clientId);
1857
+ this.createSubDirectoryCore(
1858
+ op.subdirName,
1859
+ local,
1860
+ { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber },
1861
+ msg.clientId,
1862
+ );
1694
1863
  }
1695
1864
 
1696
1865
  /**
@@ -1986,7 +2155,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
1986
2155
  public getSerializableCreateInfo() {
1987
2156
  this.throwIfDisposed();
1988
2157
  const createInfo: ICreateInfo = {
1989
- csn: this.sequenceNumber,
2158
+ csn: this.seqData.seq,
1990
2159
  ccIds: Array.from(this.clientIds),
1991
2160
  };
1992
2161
  return createInfo;
@@ -2095,6 +2264,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2095
2264
  this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
2096
2265
  // don't need to register events because deleting never unregistered
2097
2266
  this._subdirectories.set(op.subdirName as string, localOpMetadata.subDirectory);
2267
+ // Restore the record in creation tracker
2268
+ if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
2269
+ this.ackedCreationSeqTracker.set(op.subdirName, {
2270
+ ...localOpMetadata.subDirectory.seqData,
2271
+ });
2272
+ } else {
2273
+ this.localCreationSeqTracker.set(op.subdirName, {
2274
+ ...localOpMetadata.subDirectory.seqData,
2275
+ });
2276
+ }
2098
2277
  this.emit("subDirectoryCreated", op.subdirName, true, this);
2099
2278
  }
2100
2279
 
@@ -2198,7 +2377,7 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2198
2377
  return (
2199
2378
  (msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
2200
2379
  this.clientIds.has("detached") ||
2201
- (this.sequenceNumber !== -1 && this.sequenceNumber <= msg.referenceSequenceNumber)
2380
+ (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)
2202
2381
  );
2203
2382
  }
2204
2383
 
@@ -2258,10 +2437,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2258
2437
  // If this is delete op and we have keys in this subDirectory, then we need to delete these
2259
2438
  // keys except the pending ones as they will be sequenced after this delete.
2260
2439
  directory.clearExceptPendingKeys(local);
2261
- // In case of delete op, we need to reset the creation seq number and client ids of
2440
+ // In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
2262
2441
  // creators as the previous directory is getting deleted and we will initialize again when
2263
2442
  // we will receive op for the create again.
2264
- directory.sequenceNumber = -1;
2443
+ directory.seqData.seq = -1;
2444
+ directory.seqData.clientSeq = -1;
2265
2445
  directory.clientIds.clear();
2266
2446
  // Do the same thing for the subtree of the directory. If create is not pending for a child, then just
2267
2447
  // delete it.
@@ -2275,22 +2455,39 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2275
2455
  }
2276
2456
  };
2277
2457
  const subDirectory = this._subdirectories.get(op.subdirName);
2458
+ // Clear the creation tracker record
2459
+ this.ackedCreationSeqTracker.delete(op.subdirName);
2278
2460
  resetSubDirectoryTree(subDirectory);
2279
2461
  }
2280
2462
  if (op.type === "createSubDirectory") {
2281
2463
  const dir = this._subdirectories.get(op.subdirName);
2282
2464
  // Child sub directory create seq number can't be lower than the parent subdirectory.
2283
2465
  // The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
2284
- if (this.sequenceNumber !== -1 && this.sequenceNumber <= msg.sequenceNumber) {
2285
- if (dir?.sequenceNumber === -1) {
2286
- // Only set the seq on the first message, could be more
2287
- dir.sequenceNumber = msg.sequenceNumber;
2466
+ if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
2467
+ if (dir?.seqData.seq === -1) {
2468
+ // Only set the sequence data based on the first message
2469
+ dir.seqData.seq = msg.sequenceNumber;
2470
+ dir.seqData.clientSeq = msg.clientSequenceNumber;
2471
+
2472
+ // set the creation seq in tracker
2473
+ if (
2474
+ !this.ackedCreationSeqTracker.has(op.subdirName) &&
2475
+ !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)
2476
+ ) {
2477
+ this.ackedCreationSeqTracker.set(op.subdirName, {
2478
+ seq: msg.sequenceNumber,
2479
+ clientSeq: msg.clientSequenceNumber,
2480
+ });
2481
+ if (local) {
2482
+ this.localCreationSeqTracker.delete(op.subdirName);
2483
+ }
2484
+ }
2288
2485
  }
2289
2486
  // The client created the dir at or after the dirs seq, so list its client id as a creator.
2290
2487
  if (
2291
2488
  dir !== undefined &&
2292
2489
  !dir.clientIds.has(msg.clientId) &&
2293
- dir.sequenceNumber <= msg.sequenceNumber
2490
+ dir.seqData.seq <= msg.sequenceNumber
2294
2491
  ) {
2295
2492
  dir.clientIds.add(msg.clientId);
2296
2493
  }
@@ -2375,27 +2572,37 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2375
2572
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
2376
2573
  * @param subdirName - The name of the subdirectory being created
2377
2574
  * @param local - Whether the message originated from the local client
2378
- * @param seq - Sequence number at which this directory is created
2575
+ * @param seqData - Sequence number and client sequence number at which this directory is created
2379
2576
  * @param clientId - Id of client which created this directory.
2380
2577
  * @returns True if is newly created, false if it already existed.
2381
2578
  */
2382
2579
  private createSubDirectoryCore(
2383
2580
  subdirName: string,
2384
2581
  local: boolean,
2385
- seq: number,
2582
+ seqData: SequenceData,
2386
2583
  clientId: string,
2387
2584
  ): boolean {
2388
2585
  const subdir = this._subdirectories.get(subdirName);
2389
2586
  if (subdir === undefined) {
2390
2587
  const absolutePath = posix.join(this.absolutePath, subdirName);
2391
2588
  const subDir = new SubDirectory(
2392
- seq,
2589
+ { ...seqData },
2393
2590
  new Set([clientId]),
2394
2591
  this.directory,
2395
2592
  this.runtime,
2396
2593
  this.serializer,
2397
2594
  absolutePath,
2398
2595
  );
2596
+ /**
2597
+ * Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
2598
+ * on whether the creation behavior has been ack'd or not
2599
+ */
2600
+ if (!isAcknowledgedOrDetached(seqData)) {
2601
+ this.localCreationSeqTracker.set(subdirName, { ...seqData });
2602
+ } else {
2603
+ this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
2604
+ }
2605
+
2399
2606
  this.registerEventsOnSubDirectory(subDir, subdirName);
2400
2607
  this._subdirectories.set(subdirName, subDir);
2401
2608
  this.emit("subDirectoryCreated", subdirName, local, this);
@@ -2426,6 +2633,16 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2426
2633
  // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
2427
2634
  if (previousValue !== undefined) {
2428
2635
  this._subdirectories.delete(subdirName);
2636
+ /**
2637
+ * Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
2638
+ * ack'd already or still not committed yet (could be both).
2639
+ */
2640
+ if (this.ackedCreationSeqTracker.has(subdirName)) {
2641
+ this.ackedCreationSeqTracker.delete(subdirName);
2642
+ }
2643
+ if (this.localCreationSeqTracker.has(subdirName)) {
2644
+ this.localCreationSeqTracker.delete(subdirName);
2645
+ }
2429
2646
  this.disposeSubDirectoryTree(previousValue);
2430
2647
  this.emit("subDirectoryDeleted", subdirName, local, this);
2431
2648
  }
@@ -2447,10 +2664,11 @@ class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirec
2447
2664
  }
2448
2665
 
2449
2666
  private undeleteSubDirectoryTree(directory: SubDirectory): void {
2450
- // Restore deleted subdirectory tree. This will unmark "deleted" from the subdirectories from bottom to top.
2451
- for (const [_, subDirectory] of this._subdirectories.entries()) {
2452
- this.undeleteSubDirectoryTree(subDirectory);
2453
- }
2667
+ // Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
2668
+ // This will unmark "deleted" from the subdirectories from top to bottom.
2454
2669
  directory.undispose();
2670
+ for (const [_, subDirectory] of directory.subdirectories()) {
2671
+ this.undeleteSubDirectoryTree(subDirectory as SubDirectory);
2672
+ }
2455
2673
  }
2456
2674
  }