@fluidframework/map 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.224419

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 (118) hide show
  1. package/.eslintrc.js +12 -11
  2. package/.mocharc.js +12 -0
  3. package/CHANGELOG.md +162 -0
  4. package/README.md +24 -8
  5. package/api-extractor-lint.json +4 -0
  6. package/api-extractor.json +2 -2
  7. package/api-report/map.api.md +297 -0
  8. package/dist/{directory.js → directory.cjs} +749 -228
  9. package/dist/directory.cjs.map +1 -0
  10. package/dist/directory.d.ts +567 -34
  11. package/dist/directory.d.ts.map +1 -1
  12. package/dist/index.cjs +27 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.d.ts +5 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/{interfaces.js → interfaces.cjs} +1 -1
  17. package/dist/interfaces.cjs.map +1 -0
  18. package/dist/interfaces.d.ts +167 -184
  19. package/dist/interfaces.d.ts.map +1 -1
  20. package/dist/internalInterfaces.cjs +7 -0
  21. package/dist/internalInterfaces.cjs.map +1 -0
  22. package/dist/internalInterfaces.d.ts +101 -0
  23. package/dist/internalInterfaces.d.ts.map +1 -0
  24. package/dist/{localValues.js → localValues.cjs} +15 -3
  25. package/dist/localValues.cjs.map +1 -0
  26. package/dist/localValues.d.ts +17 -6
  27. package/dist/localValues.d.ts.map +1 -1
  28. package/dist/map-alpha.d.ts +982 -0
  29. package/dist/map-beta.d.ts +275 -0
  30. package/dist/map-public.d.ts +275 -0
  31. package/dist/map-untrimmed.d.ts +996 -0
  32. package/dist/{map.js → map.cjs} +39 -34
  33. package/dist/map.cjs.map +1 -0
  34. package/dist/map.d.ts +10 -17
  35. package/dist/map.d.ts.map +1 -1
  36. package/dist/{mapKernel.js → mapKernel.cjs} +122 -79
  37. package/dist/mapKernel.cjs.map +1 -0
  38. package/dist/mapKernel.d.ts +17 -48
  39. package/dist/mapKernel.d.ts.map +1 -1
  40. package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
  41. package/dist/packageVersion.cjs.map +1 -0
  42. package/dist/packageVersion.d.ts +1 -1
  43. package/dist/packageVersion.d.ts.map +1 -1
  44. package/dist/tsdoc-metadata.json +11 -0
  45. package/lib/directory.d.mts +902 -0
  46. package/lib/directory.d.mts.map +1 -0
  47. package/lib/{directory.js → directory.mjs} +736 -199
  48. package/lib/directory.mjs.map +1 -0
  49. package/lib/index.d.mts +9 -0
  50. package/lib/index.d.mts.map +1 -0
  51. package/lib/index.mjs +8 -0
  52. package/lib/index.mjs.map +1 -0
  53. package/lib/{interfaces.d.ts → interfaces.d.mts} +167 -184
  54. package/lib/interfaces.d.mts.map +1 -0
  55. package/lib/{interfaces.js → interfaces.mjs} +1 -1
  56. package/lib/interfaces.mjs.map +1 -0
  57. package/lib/internalInterfaces.d.mts +101 -0
  58. package/lib/internalInterfaces.d.mts.map +1 -0
  59. package/lib/internalInterfaces.mjs +6 -0
  60. package/lib/internalInterfaces.mjs.map +1 -0
  61. package/lib/{localValues.d.ts → localValues.d.mts} +18 -7
  62. package/lib/localValues.d.mts.map +1 -0
  63. package/lib/{localValues.js → localValues.mjs} +15 -3
  64. package/lib/localValues.mjs.map +1 -0
  65. package/lib/map-alpha.d.mts +982 -0
  66. package/lib/map-beta.d.mts +275 -0
  67. package/lib/map-public.d.mts +275 -0
  68. package/lib/map-untrimmed.d.mts +996 -0
  69. package/lib/{map.d.ts → map.d.mts} +11 -18
  70. package/lib/map.d.mts.map +1 -0
  71. package/lib/{map.js → map.mjs} +40 -35
  72. package/lib/map.mjs.map +1 -0
  73. package/lib/{mapKernel.d.ts → mapKernel.d.mts} +18 -49
  74. package/lib/mapKernel.d.mts.map +1 -0
  75. package/lib/{mapKernel.js → mapKernel.mjs} +116 -73
  76. package/lib/mapKernel.mjs.map +1 -0
  77. package/lib/{packageVersion.d.ts → packageVersion.d.mts} +1 -1
  78. package/lib/{packageVersion.d.ts.map → packageVersion.d.mts.map} +1 -1
  79. package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
  80. package/lib/packageVersion.mjs.map +1 -0
  81. package/map.test-files.tar +0 -0
  82. package/package.json +105 -65
  83. package/prettier.config.cjs +8 -0
  84. package/src/directory.ts +2544 -1727
  85. package/src/index.ts +31 -5
  86. package/src/interfaces.ts +346 -345
  87. package/src/internalInterfaces.ts +119 -0
  88. package/src/localValues.ts +103 -96
  89. package/src/map.ts +362 -351
  90. package/src/mapKernel.ts +755 -722
  91. package/src/packageVersion.ts +1 -1
  92. package/tsc-multi.test.json +4 -0
  93. package/tsconfig.json +10 -15
  94. package/dist/directory.js.map +0 -1
  95. package/dist/index.js +0 -34
  96. package/dist/index.js.map +0 -1
  97. package/dist/interfaces.js.map +0 -1
  98. package/dist/localValues.js.map +0 -1
  99. package/dist/map.js.map +0 -1
  100. package/dist/mapKernel.js.map +0 -1
  101. package/dist/packageVersion.js.map +0 -1
  102. package/lib/directory.d.ts +0 -369
  103. package/lib/directory.d.ts.map +0 -1
  104. package/lib/directory.js.map +0 -1
  105. package/lib/index.d.ts +0 -20
  106. package/lib/index.d.ts.map +0 -1
  107. package/lib/index.js +0 -20
  108. package/lib/index.js.map +0 -1
  109. package/lib/interfaces.d.ts.map +0 -1
  110. package/lib/interfaces.js.map +0 -1
  111. package/lib/localValues.d.ts.map +0 -1
  112. package/lib/localValues.js.map +0 -1
  113. package/lib/map.d.ts.map +0 -1
  114. package/lib/map.js.map +0 -1
  115. package/lib/mapKernel.d.ts.map +0 -1
  116. package/lib/mapKernel.js.map +0 -1
  117. package/lib/packageVersion.js.map +0 -1
  118. package/tsconfig.esnext.json +0 -7
@@ -3,22 +3,26 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
  var _a, _b;
6
- import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
7
- import { UsageError } from "@fluidframework/container-utils";
6
+ import { assert } from "@fluidframework/core-utils";
7
+ import { TypedEventEmitter } from "@fluid-internal/client-utils";
8
+ import { UsageError } from "@fluidframework/telemetry-utils";
8
9
  import { readAndParse } from "@fluidframework/driver-utils";
9
- import { MessageType, } from "@fluidframework/protocol-definitions";
10
+ import { MessageType } from "@fluidframework/protocol-definitions";
10
11
  import { SharedObject, ValueType } from "@fluidframework/shared-object-base";
11
12
  import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
12
- import * as path from "path-browserify";
13
- import { LocalValueMaker, makeSerializable, } from "./localValues";
14
- import { pkgVersion } from "./packageVersion";
13
+ import path from "path-browserify";
14
+ import { RedBlackTree } from "@fluidframework/merge-tree";
15
+ import { LocalValueMaker, makeSerializable } from "./localValues.mjs";
16
+ import { pkgVersion } from "./packageVersion.mjs";
15
17
  // We use path-browserify since this code can run safely on the server or the browser.
16
18
  // We standardize on using posix slashes everywhere.
17
19
  const posix = path.posix;
18
20
  const snapshotFileName = "header";
19
21
  /**
20
- * The factory that defines the directory.
22
+ * {@link @fluidframework/datastore-definitions#IChannelFactory} for {@link SharedDirectory}.
23
+ *
21
24
  * @sealed
25
+ * @alpha
22
26
  */
23
27
  export class DirectoryFactory {
24
28
  /**
@@ -63,11 +67,104 @@ DirectoryFactory.Attributes = {
63
67
  packageVersion: pkgVersion,
64
68
  };
65
69
  /**
66
- * SharedDirectory provides a hierarchical organization of map-like data structures as SubDirectories.
67
- * The values stored within can be accessed like a map, and the hierarchy can be navigated using path syntax.
68
- * SubDirectories can be retrieved for use as working directories.
70
+ * The comparator essentially performs the following procedure to determine the order of subdirectory creation:
71
+ * 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
72
+ * the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
73
+ *
74
+ * 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
75
+ * - 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
76
+ * should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
77
+ * - 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
78
+ * batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
79
+ *
80
+ * 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
81
+ * - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
82
+ * acknowledged by the server earlier.
83
+ * - 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
84
+ * and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
85
+ *
86
+ * 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
87
+ * purpose of ordering.
88
+ */
89
+ const seqDataComparator = (a, b) => {
90
+ if (isAcknowledgedOrDetached(a)) {
91
+ if (isAcknowledgedOrDetached(b)) {
92
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
93
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
94
+ }
95
+ else {
96
+ return -1;
97
+ }
98
+ }
99
+ else {
100
+ if (!isAcknowledgedOrDetached(b)) {
101
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
102
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq - b.clientSeq;
103
+ }
104
+ else {
105
+ return 1;
106
+ }
107
+ }
108
+ };
109
+ function isAcknowledgedOrDetached(seqData) {
110
+ return seqData.seq >= 0;
111
+ }
112
+ /**
113
+ * A utility class for tracking associations between keys and their creation indices.
114
+ * This is relevant to support map iteration in insertion order, see
115
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
116
+ *
117
+ * TODO: It can be combined with the creation tracker utilized in SharedMap
118
+ */
119
+ class DirectoryCreationTracker {
120
+ constructor() {
121
+ this.indexToKey = new RedBlackTree(seqDataComparator);
122
+ this.keyToIndex = new Map();
123
+ }
124
+ set(key, seqData) {
125
+ this.indexToKey.put(seqData, key);
126
+ this.keyToIndex.set(key, seqData);
127
+ }
128
+ has(keyOrSeqData) {
129
+ return typeof keyOrSeqData === "string"
130
+ ? this.keyToIndex.has(keyOrSeqData)
131
+ : this.indexToKey.get(keyOrSeqData) !== undefined;
132
+ }
133
+ delete(keyOrSeqData) {
134
+ if (this.has(keyOrSeqData)) {
135
+ if (typeof keyOrSeqData === "string") {
136
+ const seqData = this.keyToIndex.get(keyOrSeqData);
137
+ this.keyToIndex.delete(keyOrSeqData);
138
+ this.indexToKey.remove(seqData);
139
+ }
140
+ else {
141
+ const key = this.indexToKey.get(keyOrSeqData)?.data;
142
+ this.indexToKey.remove(keyOrSeqData);
143
+ this.keyToIndex.delete(key);
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * Retrieves all subdirectories with creation order that satisfy an optional constraint function.
149
+ * @param constraint - An optional constraint function that filters keys.
150
+ * @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
151
+ */
152
+ keys(constraint) {
153
+ const keys = [];
154
+ this.indexToKey.mapRange((node) => {
155
+ if (!constraint || constraint(node.data)) {
156
+ keys.push(node.data);
157
+ }
158
+ return true;
159
+ }, keys);
160
+ return keys;
161
+ }
162
+ }
163
+ /**
164
+ * {@inheritDoc ISharedDirectory}
69
165
  *
70
166
  * @example
167
+ *
71
168
  * ```typescript
72
169
  * mySharedDirectory.createSubDirectory("a").createSubDirectory("b").createSubDirectory("c").set("foo", val1);
73
170
  * const mySubDir = mySharedDirectory.getWorkingDirectory("/a/b/c");
@@ -75,8 +172,33 @@ DirectoryFactory.Attributes = {
75
172
  * ```
76
173
  *
77
174
  * @sealed
175
+ * @alpha
78
176
  */
79
177
  export class SharedDirectory extends SharedObject {
178
+ /**
179
+ * Create a new shared directory
180
+ *
181
+ * @param runtime - Data store runtime the new shared directory belongs to
182
+ * @param id - Optional name of the shared directory
183
+ * @returns Newly create shared directory (but not attached yet)
184
+ */
185
+ static create(runtime, id) {
186
+ return runtime.createChannel(id, DirectoryFactory.Type);
187
+ }
188
+ /**
189
+ * Get a factory for SharedDirectory to register with the data store.
190
+ *
191
+ * @returns A factory that creates and load SharedDirectory
192
+ */
193
+ static getFactory() {
194
+ return new DirectoryFactory();
195
+ }
196
+ /**
197
+ * {@inheritDoc IDirectory.absolutePath}
198
+ */
199
+ get absolutePath() {
200
+ return this.root.absolutePath;
201
+ }
80
202
  /**
81
203
  * Constructs a new shared directory. If the object is non-local an id and service interfaces will
82
204
  * be provided.
@@ -93,7 +215,7 @@ export class SharedDirectory extends SharedObject {
93
215
  /**
94
216
  * Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
95
217
  */
96
- this.root = new SubDirectory(this, this.runtime, this.serializer, posix.sep);
218
+ this.root = new SubDirectory({ seq: 0, clientSeq: 0 }, new Set(), this, this.runtime, this.serializer, posix.sep);
97
219
  /**
98
220
  * Mapping of op types to message handlers.
99
221
  */
@@ -111,33 +233,11 @@ export class SharedDirectory extends SharedObject {
111
233
  this.emit("subDirectoryDeleted", relativePath, local, this);
112
234
  });
113
235
  }
114
- /**
115
- * Create a new shared directory
116
- *
117
- * @param runtime - Data store runtime the new shared directory belongs to
118
- * @param id - Optional name of the shared directory
119
- * @returns Newly create shared directory (but not attached yet)
120
- */
121
- static create(runtime, id) {
122
- return runtime.createChannel(id, DirectoryFactory.Type);
123
- }
124
- /**
125
- * Get a factory for SharedDirectory to register with the data store.
126
- *
127
- * @returns A factory that creates and load SharedDirectory
128
- */
129
- static getFactory() {
130
- return new DirectoryFactory();
131
- }
132
- /**
133
- * {@inheritDoc IDirectory.absolutePath}
134
- */
135
- get absolutePath() {
136
- return this.root.absolutePath;
137
- }
138
236
  /**
139
237
  * {@inheritDoc IDirectory.get}
140
238
  */
239
+ // TODO: Use `unknown` instead (breaking change).
240
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
241
  get(key) {
142
242
  return this.root.get(key);
143
243
  }
@@ -186,13 +286,18 @@ export class SharedDirectory extends SharedObject {
186
286
  * Issue a callback on each entry under this IDirectory.
187
287
  * @param callback - Callback to issue
188
288
  */
289
+ // TODO: Use `unknown` instead (breaking change).
290
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
291
  forEach(callback) {
292
+ // eslint-disable-next-line unicorn/no-array-for-each, unicorn/no-array-callback-reference
190
293
  this.root.forEach(callback);
191
294
  }
192
295
  /**
193
296
  * Get an iterator over the entries under this IDirectory.
194
297
  * @returns The iterator
195
298
  */
299
+ // TODO: Use `unknown` instead (breaking change).
300
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
301
  [(_a = Symbol.toStringTag, Symbol.iterator)]() {
197
302
  return this.root[Symbol.iterator]();
198
303
  }
@@ -200,6 +305,8 @@ export class SharedDirectory extends SharedObject {
200
305
  * Get an iterator over the entries under this IDirectory.
201
306
  * @returns The iterator
202
307
  */
308
+ // TODO: Use `unknown` instead (breaking change).
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
203
310
  entries() {
204
311
  return this.root.entries();
205
312
  }
@@ -220,6 +327,8 @@ export class SharedDirectory extends SharedObject {
220
327
  * Get an iterator over the values under this IDirectory.
221
328
  * @returns The iterator
222
329
  */
330
+ // TODO: Use `unknown` instead (breaking change).
331
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
332
  values() {
224
333
  return this.root.values();
225
334
  }
@@ -262,7 +371,7 @@ export class SharedDirectory extends SharedObject {
262
371
  return this.root;
263
372
  }
264
373
  let currentSubDir = this.root;
265
- const subdirs = absolutePath.substr(1).split(posix.sep);
374
+ const subdirs = absolutePath.slice(1).split(posix.sep);
266
375
  for (const subdir of subdirs) {
267
376
  currentSubDir = currentSubDir.getSubDirectory(subdir);
268
377
  if (!currentSubDir) {
@@ -273,7 +382,6 @@ export class SharedDirectory extends SharedObject {
273
382
  }
274
383
  /**
275
384
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
276
- * @internal
277
385
  */
278
386
  summarizeCore(serializer, telemetryContext) {
279
387
  return this.serializeDirectory(this.root, serializer);
@@ -283,19 +391,16 @@ export class SharedDirectory extends SharedObject {
283
391
  * @param op - Op to submit
284
392
  * @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
285
393
  * this op while it has not been ack'd. This will be sent when we receive this op back from the server.
286
- * @internal
287
394
  */
288
395
  submitDirectoryMessage(op, localOpMetadata) {
289
396
  this.submitLocalMessage(op, localOpMetadata);
290
397
  }
291
398
  /**
292
399
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
293
- * @internal
294
400
  */
295
401
  onDisconnect() { }
296
402
  /**
297
403
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
298
- * @internal
299
404
  */
300
405
  reSubmitCore(content, localOpMetadata) {
301
406
  const message = content;
@@ -305,7 +410,6 @@ export class SharedDirectory extends SharedObject {
305
410
  }
306
411
  /**
307
412
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
308
- * @internal
309
413
  */
310
414
  async loadCore(storage) {
311
415
  const data = await readAndParse(storage, snapshotFileName);
@@ -326,7 +430,6 @@ export class SharedDirectory extends SharedObject {
326
430
  /**
327
431
  * Populate the directory with the given directory data.
328
432
  * @param data - A JSON string containing serialized directory data
329
- * @internal
330
433
  */
331
434
  populate(data) {
332
435
  const stack = [];
@@ -335,11 +438,42 @@ export class SharedDirectory extends SharedObject {
335
438
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
336
439
  const [currentSubDir, currentSubDirObject] = stack.pop();
337
440
  if (currentSubDirObject.subdirectories) {
441
+ // Utilize a map to store the seq -> clientSeq for the newly created subdirectory
442
+ const tempSeqNums = new Map();
338
443
  for (const [subdirName, subdirObject] of Object.entries(currentSubDirObject.subdirectories)) {
339
444
  let newSubDir = currentSubDir.getSubDirectory(subdirName);
445
+ let seqData;
340
446
  if (!newSubDir) {
341
- newSubDir = new SubDirectory(this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName));
447
+ const createInfo = subdirObject.ci;
448
+ // We do not store the client sequence number in the storage because the order has already been
449
+ // guaranteed during the serialization process. As a result, it is only essential to utilize the
450
+ // "fake" client sequence number to signify the loading order, and there is no need to retain
451
+ // the actual client sequence number at this point.
452
+ if (createInfo !== undefined && createInfo.csn > -1) {
453
+ // If csn is -1, then initialize it with 0, otherwise we will never process ops for this
454
+ // sub directory. This could be done at serialization time too, but we need to maintain
455
+ // back compat too and also we will actually know the state when it was serialized.
456
+ if (!tempSeqNums.has(createInfo.csn)) {
457
+ tempSeqNums.set(createInfo.csn, 0);
458
+ }
459
+ let fakeClientSeq = tempSeqNums.get(createInfo.csn);
460
+ seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
461
+ tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
462
+ }
463
+ else {
464
+ seqData = {
465
+ seq: 0,
466
+ clientSeq: ++currentSubDir.localCreationSeq,
467
+ };
468
+ }
469
+ newSubDir = new SubDirectory(seqData, createInfo !== undefined
470
+ ? new Set(createInfo.ccIds)
471
+ : new Set(), this, this.runtime, this.serializer, posix.join(currentSubDir.absolutePath, subdirName));
342
472
  currentSubDir.populateSubDirectory(subdirName, newSubDir);
473
+ // Record the newly inserted subdirectory to the creation tracker
474
+ currentSubDir.ackedCreationSeqTracker.set(subdirName, {
475
+ ...seqData,
476
+ });
343
477
  }
344
478
  stack.push([newSubDir, subdirObject]);
345
479
  }
@@ -354,20 +488,18 @@ export class SharedDirectory extends SharedObject {
354
488
  }
355
489
  /**
356
490
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
357
- * @internal
358
491
  */
359
492
  processCore(message, local, localOpMetadata) {
360
493
  if (message.type === MessageType.Operation) {
361
494
  const op = message.contents;
362
495
  const handler = this.messageHandlers.get(op.type);
363
496
  assert(handler !== undefined, 0x00e /* Missing message handler for message type */);
364
- handler.process(op, local, localOpMetadata);
497
+ handler.process(message, op, local, localOpMetadata);
365
498
  }
366
499
  }
367
500
  /**
368
501
  * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
369
- * @internal
370
- */
502
+ */
371
503
  rollback(content, localOpMetadata) {
372
504
  const op = content;
373
505
  const subdir = this.getWorkingDirectory(op.path);
@@ -392,19 +524,50 @@ export class SharedDirectory extends SharedObject {
392
524
  * @param serializable - The remote information that we can convert into a real object
393
525
  * @returns The local value that was produced
394
526
  */
395
- makeLocal(key, absolutePath, serializable) {
396
- assert(serializable.type === ValueType[ValueType.Plain] || serializable.type === ValueType[ValueType.Shared], 0x1e4 /* "Unexpected serializable type" */);
527
+ makeLocal(key, absolutePath,
528
+ // eslint-disable-next-line import/no-deprecated
529
+ serializable) {
530
+ assert(serializable.type === ValueType[ValueType.Plain] ||
531
+ serializable.type === ValueType[ValueType.Shared], 0x1e4 /* "Unexpected serializable type" */);
397
532
  return this.localValueMaker.fromSerializable(serializable);
398
533
  }
534
+ /**
535
+ * This checks if there is pending delete op for local delete for a any subdir in the relative path.
536
+ * @param relativePath - path of sub directory.
537
+ * @returns `true` if there is pending delete, `false` otherwise.
538
+ */
539
+ isSubDirectoryDeletePending(relativePath) {
540
+ const absolutePath = this.makeAbsolute(relativePath);
541
+ if (absolutePath === posix.sep) {
542
+ return false;
543
+ }
544
+ let currentParent = this.root;
545
+ const nodeList = absolutePath.split(posix.sep);
546
+ let start = 1;
547
+ while (start < nodeList.length) {
548
+ const subDirName = nodeList[start];
549
+ if (currentParent.isSubDirectoryDeletePending(subDirName)) {
550
+ return true;
551
+ }
552
+ currentParent = currentParent.getSubDirectory(subDirName);
553
+ if (currentParent === undefined) {
554
+ return true;
555
+ }
556
+ start += 1;
557
+ }
558
+ return false;
559
+ }
399
560
  /**
400
561
  * Set the message handlers for the directory.
401
562
  */
402
563
  setMessageHandlers() {
403
564
  this.messageHandlers.set("clear", {
404
- process: (op, local, localOpMetadata) => {
565
+ process: (msg, op, local, localOpMetadata) => {
405
566
  const subdir = this.getWorkingDirectory(op.path);
406
- if (subdir) {
407
- subdir.processClearMessage(op, local, localOpMetadata);
567
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
568
+ // as we are going to delete this subDirectory.
569
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
570
+ subdir.processClearMessage(msg, op, local, localOpMetadata);
408
571
  }
409
572
  },
410
573
  submit: (op, localOpMetadata) => {
@@ -413,12 +576,20 @@ export class SharedDirectory extends SharedObject {
413
576
  subdir.resubmitClearMessage(op, localOpMetadata);
414
577
  }
415
578
  },
579
+ applyStashedOp: (op) => {
580
+ const subdir = this.getWorkingDirectory(op.path);
581
+ if (subdir) {
582
+ return subdir.applyStashedClearMessage(op);
583
+ }
584
+ },
416
585
  });
417
586
  this.messageHandlers.set("delete", {
418
- process: (op, local, localOpMetadata) => {
587
+ process: (msg, op, local, localOpMetadata) => {
419
588
  const subdir = this.getWorkingDirectory(op.path);
420
- if (subdir) {
421
- subdir.processDeleteMessage(op, local, localOpMetadata);
589
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
590
+ // as we are going to delete this subDirectory.
591
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
592
+ subdir.processDeleteMessage(msg, op, local, localOpMetadata);
422
593
  }
423
594
  },
424
595
  submit: (op, localOpMetadata) => {
@@ -427,13 +598,21 @@ export class SharedDirectory extends SharedObject {
427
598
  subdir.resubmitKeyMessage(op, localOpMetadata);
428
599
  }
429
600
  },
601
+ applyStashedOp: (op) => {
602
+ const subdir = this.getWorkingDirectory(op.path);
603
+ if (subdir) {
604
+ return subdir.applyStashedDeleteMessage(op);
605
+ }
606
+ },
430
607
  });
431
608
  this.messageHandlers.set("set", {
432
- process: (op, local, localOpMetadata) => {
609
+ process: (msg, op, local, localOpMetadata) => {
433
610
  const subdir = this.getWorkingDirectory(op.path);
434
- if (subdir) {
611
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
612
+ // as we are going to delete this subDirectory.
613
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
435
614
  const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
436
- subdir.processSetMessage(op, context, local, localOpMetadata);
615
+ subdir.processSetMessage(msg, op, context, local, localOpMetadata);
437
616
  }
438
617
  },
439
618
  submit: (op, localOpMetadata) => {
@@ -442,12 +621,21 @@ export class SharedDirectory extends SharedObject {
442
621
  subdir.resubmitKeyMessage(op, localOpMetadata);
443
622
  }
444
623
  },
624
+ applyStashedOp: (op) => {
625
+ const subdir = this.getWorkingDirectory(op.path);
626
+ if (subdir) {
627
+ const context = this.makeLocal(op.key, op.path, op.value);
628
+ return subdir.applyStashedSetMessage(op, context);
629
+ }
630
+ },
445
631
  });
446
632
  this.messageHandlers.set("createSubDirectory", {
447
- process: (op, local, localOpMetadata) => {
633
+ process: (msg, op, local, localOpMetadata) => {
448
634
  const parentSubdir = this.getWorkingDirectory(op.path);
449
- if (parentSubdir) {
450
- parentSubdir.processCreateSubDirectoryMessage(op, local, localOpMetadata);
635
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
636
+ // as we are going to delete this subDirectory.
637
+ if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
638
+ parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
451
639
  }
452
640
  },
453
641
  submit: (op, localOpMetadata) => {
@@ -457,12 +645,20 @@ export class SharedDirectory extends SharedObject {
457
645
  parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
458
646
  }
459
647
  },
648
+ applyStashedOp: (op) => {
649
+ const parentSubdir = this.getWorkingDirectory(op.path);
650
+ if (parentSubdir) {
651
+ return parentSubdir.applyStashedCreateSubDirMessage(op);
652
+ }
653
+ },
460
654
  });
461
655
  this.messageHandlers.set("deleteSubDirectory", {
462
- process: (op, local, localOpMetadata) => {
656
+ process: (msg, op, local, localOpMetadata) => {
463
657
  const parentSubdir = this.getWorkingDirectory(op.path);
464
- if (parentSubdir) {
465
- parentSubdir.processDeleteSubDirectoryMessage(op, local, localOpMetadata);
658
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
659
+ // as we are going to delete this subDirectory.
660
+ if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
661
+ parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
466
662
  }
467
663
  },
468
664
  submit: (op, localOpMetadata) => {
@@ -472,13 +668,23 @@ export class SharedDirectory extends SharedObject {
472
668
  parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
473
669
  }
474
670
  },
671
+ applyStashedOp: (op) => {
672
+ const parentSubdir = this.getWorkingDirectory(op.path);
673
+ if (parentSubdir) {
674
+ return parentSubdir.applyStashedDeleteSubDirMessage(op);
675
+ }
676
+ },
475
677
  });
476
678
  }
477
679
  /**
478
- * @internal
680
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
479
681
  */
480
- applyStashedOp() {
481
- throw new Error("not implemented");
682
+ applyStashedOp(op) {
683
+ const handler = this.messageHandlers.get(op.type);
684
+ if (handler === undefined) {
685
+ throw new Error("no apply stashed op handler");
686
+ }
687
+ return handler.applyStashedOp(op);
482
688
  }
483
689
  serializeDirectory(root, serializer, telemetryContext) {
484
690
  const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
@@ -491,20 +697,21 @@ export class SharedDirectory extends SharedObject {
491
697
  while (stack.length > 0) {
492
698
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
493
699
  const [currentSubDir, currentSubDirObject] = stack.pop();
700
+ currentSubDirObject.ci = currentSubDir.getSerializableCreateInfo();
494
701
  for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
495
702
  if (!currentSubDirObject.storage) {
496
703
  currentSubDirObject.storage = {};
497
704
  }
705
+ // eslint-disable-next-line import/no-deprecated
498
706
  const result = {
499
707
  type: value.type,
500
- // eslint-disable-next-line @typescript-eslint/ban-types
501
708
  value: value.value && JSON.parse(value.value),
502
709
  };
503
710
  if (value.value && value.value.length >= MinValueSizeSeparateSnapshotBlob) {
504
711
  const extraContent = {};
505
712
  let largeContent = extraContent;
506
713
  if (currentSubDir.absolutePath !== posix.sep) {
507
- for (const dir of currentSubDir.absolutePath.substr(1).split(posix.sep)) {
714
+ for (const dir of currentSubDir.absolutePath.slice(1).split(posix.sep)) {
508
715
  const subDataObject = {};
509
716
  largeContent.subdirectories = { [dir]: subDataObject };
510
717
  largeContent = subDataObject;
@@ -537,23 +744,30 @@ export class SharedDirectory extends SharedObject {
537
744
  return builder.getSummaryTree();
538
745
  }
539
746
  }
747
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
540
748
  function isKeyEditLocalOpMetadata(metadata) {
541
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" && metadata.type === "edit";
749
+ return (metadata !== undefined &&
750
+ typeof metadata.pendingMessageId === "number" &&
751
+ metadata.type === "edit");
542
752
  }
543
753
  function isClearLocalOpMetadata(metadata) {
544
- return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number" &&
545
- typeof metadata.previousStorage === "object";
754
+ return (metadata !== undefined &&
755
+ metadata.type === "clear" &&
756
+ typeof metadata.pendingMessageId === "number" &&
757
+ typeof metadata.previousStorage === "object");
546
758
  }
547
759
  function isSubDirLocalOpMetadata(metadata) {
548
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
549
- ((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
550
- metadata.type === "deleteSubDir");
760
+ return (metadata !== undefined &&
761
+ (metadata.type === "createSubDir" || metadata.type === "deleteSubDir"));
551
762
  }
552
763
  function isDirectoryLocalOpMetadata(metadata) {
553
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
554
- (metadata.type === "edit" || metadata.type === "deleteSubDir" ||
555
- (metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
556
- (metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"));
764
+ return (isKeyEditLocalOpMetadata(metadata) ||
765
+ isClearLocalOpMetadata(metadata) ||
766
+ isSubDirLocalOpMetadata(metadata));
767
+ }
768
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
769
+ function assertNonNullClientId(clientId) {
770
+ assert(clientId !== null, 0x6af /* client id should never be null */);
557
771
  }
558
772
  /**
559
773
  * Node of the directory tree.
@@ -562,13 +776,17 @@ function isDirectoryLocalOpMetadata(metadata) {
562
776
  class SubDirectory extends TypedEventEmitter {
563
777
  /**
564
778
  * Constructor.
779
+ * @param sequenceNumber - Message seq number at which this was created.
780
+ * @param clientIds - Ids of client which created this directory.
565
781
  * @param directory - Reference back to the SharedDirectory to perform operations
566
782
  * @param runtime - The data store runtime this directory is associated with
567
783
  * @param serializer - The serializer to serialize / parse handles
568
784
  * @param absolutePath - The absolute path of this IDirectory
569
785
  */
570
- constructor(directory, runtime, serializer, absolutePath) {
786
+ constructor(seqData, clientIds, directory, runtime, serializer, absolutePath) {
571
787
  super();
788
+ this.seqData = seqData;
789
+ this.clientIds = clientIds;
572
790
  this.directory = directory;
573
791
  this.runtime = runtime;
574
792
  this.serializer = serializer;
@@ -590,13 +808,24 @@ class SubDirectory extends TypedEventEmitter {
590
808
  */
591
809
  this._subdirectories = new Map();
592
810
  /**
593
- * Keys that have been modified locally but not yet ack'd from the server.
811
+ * Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
812
+ * set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
813
+ * was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
814
+ * directory ops, so we have separate map from subDirectories tracker.
594
815
  */
595
816
  this.pendingKeys = new Map();
596
817
  /**
597
- * Subdirectories that have been modified locally but not yet ack'd from the server.
818
+ * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
819
+ * of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
820
+ * deleted sub directory.
821
+ */
822
+ this.pendingDeleteSubDirectoriesTracker = new Map();
823
+ /**
824
+ * Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
825
+ * of create op that are pending or yet to be acked from server. This is maintained just to track the locally
826
+ * created sub directory.
598
827
  */
599
- this.pendingSubDirectories = new Map();
828
+ this.pendingCreateSubDirectoriesTracker = new Map();
600
829
  /**
601
830
  * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
602
831
  */
@@ -605,16 +834,24 @@ class SubDirectory extends TypedEventEmitter {
605
834
  * The pending ids of any clears that have been performed locally but not yet ack'd from the server
606
835
  */
607
836
  this.pendingClearMessageIds = [];
837
+ /**
838
+ * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
839
+ * of the creation order.
840
+ */
841
+ this.localCreationSeq = 0;
842
+ this.localCreationSeqTracker = new DirectoryCreationTracker();
843
+ this.ackedCreationSeqTracker = new DirectoryCreationTracker();
608
844
  }
609
845
  dispose(error) {
610
846
  this._deleted = true;
611
847
  this.emit("disposed", this);
612
848
  }
613
849
  /**
614
- * Unmark the deleted property when rolling back delete.
850
+ * Unmark the deleted property only when rolling back delete.
615
851
  */
616
852
  undispose() {
617
853
  this._deleted = false;
854
+ this.emit("undisposed", this);
618
855
  }
619
856
  get disposed() {
620
857
  return this._deleted;
@@ -637,9 +874,8 @@ class SubDirectory extends TypedEventEmitter {
637
874
  * {@inheritDoc IDirectory.get}
638
875
  */
639
876
  get(key) {
640
- var _c;
641
877
  this.throwIfDisposed();
642
- return (_c = this._storage.get(key)) === null || _c === void 0 ? void 0 : _c.value;
878
+ return this._storage.get(key)?.value;
643
879
  }
644
880
  /**
645
881
  * {@inheritDoc IDirectory.set}
@@ -687,21 +923,38 @@ class SubDirectory extends TypedEventEmitter {
687
923
  throw new Error(`SubDirectory name may not contain ${posix.sep}`);
688
924
  }
689
925
  // Create the sub directory locally first.
690
- const isNew = this.createSubDirectoryCore(subdirName, true);
691
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
926
+ const isNew = this.createSubDirectoryCore(subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
692
927
  const subDir = this._subdirectories.get(subdirName);
928
+ assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
693
929
  // If we are not attached, don't submit the op.
694
930
  if (!this.directory.isAttached()) {
695
931
  return subDir;
696
932
  }
697
- const op = {
698
- path: this.absolutePath,
699
- subdirName,
700
- type: "createSubDirectory",
701
- };
702
- this.submitCreateSubDirectoryMessage(op, !isNew);
933
+ // Only submit the op, if it is newly created.
934
+ if (isNew) {
935
+ const op = {
936
+ path: this.absolutePath,
937
+ subdirName,
938
+ type: "createSubDirectory",
939
+ };
940
+ this.submitCreateSubDirectoryMessage(op);
941
+ }
703
942
  return subDir;
704
943
  }
944
+ /**
945
+ * @returns The Sequence Data which should be used for local changes.
946
+ * @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
947
+ * only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
948
+ * last set while detached will now be known to any new client, until they are changed).
949
+ *
950
+ * The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
951
+ * TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
952
+ */
953
+ getLocalSeq() {
954
+ return this.directory.isAttached()
955
+ ? { seq: -1, clientSeq: ++this.localCreationSeq }
956
+ : { seq: 0, clientSeq: ++this.localCreationSeq };
957
+ }
705
958
  /**
706
959
  * {@inheritDoc IDirectory.getSubDirectory}
707
960
  */
@@ -727,12 +980,15 @@ class SubDirectory extends TypedEventEmitter {
727
980
  if (!this.directory.isAttached()) {
728
981
  return subDir !== undefined;
729
982
  }
730
- const op = {
731
- path: this.absolutePath,
732
- subdirName,
733
- type: "deleteSubDirectory",
734
- };
735
- this.submitDeleteSubDirectoryMessage(op, subDir);
983
+ // Only submit the op, if the directory existed and we deleted it.
984
+ if (subDir !== undefined) {
985
+ const op = {
986
+ path: this.absolutePath,
987
+ subdirName,
988
+ type: "deleteSubDirectory",
989
+ };
990
+ this.submitDeleteSubDirectoryMessage(op, subDir);
991
+ }
736
992
  return subDir !== undefined;
737
993
  }
738
994
  /**
@@ -740,7 +996,26 @@ class SubDirectory extends TypedEventEmitter {
740
996
  */
741
997
  subdirectories() {
742
998
  this.throwIfDisposed();
743
- return this._subdirectories.entries();
999
+ const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
1000
+ const localSubdirsInOrder = this.localCreationSeqTracker.keys((key) => !this.ackedCreationSeqTracker.has(key));
1001
+ const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1002
+ assert(subdirNames.length === this._subdirectories.size, 0x85c /* The count of keys for iteration should be consistent with the size of actual data */);
1003
+ const entriesIterator = {
1004
+ index: 0,
1005
+ dirs: this._subdirectories,
1006
+ next() {
1007
+ if (this.index < subdirNames.length) {
1008
+ const subdirName = subdirNames[this.index++];
1009
+ const subdir = this.dirs.get(subdirName);
1010
+ return { value: [subdirName, subdir], done: false };
1011
+ }
1012
+ return { value: undefined, done: true };
1013
+ },
1014
+ [Symbol.iterator]() {
1015
+ return this;
1016
+ },
1017
+ };
1018
+ return entriesIterator;
744
1019
  }
745
1020
  /**
746
1021
  * {@inheritDoc IDirectory.getWorkingDirectory}
@@ -749,6 +1024,17 @@ class SubDirectory extends TypedEventEmitter {
749
1024
  this.throwIfDisposed();
750
1025
  return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
751
1026
  }
1027
+ /**
1028
+ * This checks if there is pending delete op for local delete for a given child subdirectory.
1029
+ * @param subDirName - directory name.
1030
+ * @returns true if there is pending delete.
1031
+ */
1032
+ isSubDirectoryDeletePending(subDirName) {
1033
+ if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
1034
+ return true;
1035
+ }
1036
+ return false;
1037
+ }
752
1038
  /**
753
1039
  * Deletes the given key from within this IDirectory.
754
1040
  * @param key - The key to delete
@@ -794,6 +1080,7 @@ class SubDirectory extends TypedEventEmitter {
794
1080
  */
795
1081
  forEach(callback) {
796
1082
  this.throwIfDisposed();
1083
+ // eslint-disable-next-line unicorn/no-array-for-each
797
1084
  this._storage.forEach((localValue, key, map) => {
798
1085
  callback(localValue.value, key, map);
799
1086
  });
@@ -815,13 +1102,9 @@ class SubDirectory extends TypedEventEmitter {
815
1102
  const iterator = {
816
1103
  next() {
817
1104
  const nextVal = localEntriesIterator.next();
818
- if (nextVal.done) {
819
- return { value: undefined, done: true };
820
- }
821
- else {
822
- // Unpack the stored value
823
- return { value: [nextVal.value[0], nextVal.value[1].value], done: false };
824
- }
1105
+ return nextVal.done
1106
+ ? { value: undefined, done: true }
1107
+ : { value: [nextVal.value[0], nextVal.value[1].value], done: false };
825
1108
  },
826
1109
  [Symbol.iterator]() {
827
1110
  return this;
@@ -847,13 +1130,9 @@ class SubDirectory extends TypedEventEmitter {
847
1130
  const iterator = {
848
1131
  next() {
849
1132
  const nextVal = localValuesIterator.next();
850
- if (nextVal.done) {
851
- return { value: undefined, done: true };
852
- }
853
- else {
854
- // Unpack the stored value
855
- return { value: nextVal.value.value, done: false };
856
- }
1133
+ return nextVal.done
1134
+ ? { value: undefined, done: true }
1135
+ : { value: nextVal.value.value, done: false };
857
1136
  },
858
1137
  [Symbol.iterator]() {
859
1138
  return this;
@@ -871,48 +1150,90 @@ class SubDirectory extends TypedEventEmitter {
871
1150
  }
872
1151
  /**
873
1152
  * Process a clear operation.
1153
+ * @param msg - The message from the server to apply.
874
1154
  * @param op - The op to process
875
1155
  * @param local - Whether the message originated from the local client
876
1156
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
877
1157
  * For messages from a remote client, this will be undefined.
878
1158
  * @internal
879
1159
  */
880
- processClearMessage(op, local, localOpMetadata) {
1160
+ processClearMessage(msg, op, local, localOpMetadata) {
881
1161
  this.throwIfDisposed();
1162
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1163
+ return;
1164
+ }
882
1165
  if (local) {
883
1166
  assert(isClearLocalOpMetadata(localOpMetadata), 0x00f /* pendingMessageId is missing from the local client's operation */);
884
1167
  const pendingClearMessageId = this.pendingClearMessageIds.shift();
885
1168
  assert(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32a /* pendingMessageId does not match */);
886
1169
  return;
887
1170
  }
888
- this.clearExceptPendingKeys();
1171
+ this.clearExceptPendingKeys(false);
1172
+ }
1173
+ /**
1174
+ * Apply clear operation locally and generate metadata
1175
+ * @param op - Op to apply
1176
+ * @returns metadata generated for stahed op
1177
+ */
1178
+ applyStashedClearMessage(op) {
1179
+ this.throwIfDisposed();
1180
+ const previousValue = new Map(this._storage);
1181
+ this.clearExceptPendingKeys(true);
1182
+ const pendingMsgId = ++this.pendingMessageId;
1183
+ this.pendingClearMessageIds.push(pendingMsgId);
1184
+ const metadata = {
1185
+ type: "clear",
1186
+ pendingMessageId: pendingMsgId,
1187
+ previousStorage: previousValue,
1188
+ };
1189
+ return metadata;
889
1190
  }
890
1191
  /**
891
1192
  * Process a delete operation.
1193
+ * @param msg - The message from the server to apply.
892
1194
  * @param op - The op to process
893
1195
  * @param local - Whether the message originated from the local client
894
1196
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
895
1197
  * For messages from a remote client, this will be undefined.
896
1198
  * @internal
897
1199
  */
898
- processDeleteMessage(op, local, localOpMetadata) {
1200
+ processDeleteMessage(msg, op, local, localOpMetadata) {
899
1201
  this.throwIfDisposed();
900
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1202
+ if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1203
+ this.needProcessStorageOperation(op, local, localOpMetadata))) {
901
1204
  return;
902
1205
  }
903
1206
  this.deleteCore(op.key, local);
904
1207
  }
1208
+ /**
1209
+ * Apply delete operation locally and generate metadata
1210
+ * @param op - Op to apply
1211
+ * @returns metadata generated for stahed op
1212
+ */
1213
+ applyStashedDeleteMessage(op) {
1214
+ this.throwIfDisposed();
1215
+ const previousValue = this.deleteCore(op.key, true);
1216
+ const pendingMessageId = this.getKeyMessageId(op);
1217
+ const localMetadata = {
1218
+ type: "edit",
1219
+ pendingMessageId,
1220
+ previousValue,
1221
+ };
1222
+ return localMetadata;
1223
+ }
905
1224
  /**
906
1225
  * Process a set operation.
1226
+ * @param msg - The message from the server to apply.
907
1227
  * @param op - The op to process
908
1228
  * @param local - Whether the message originated from the local client
909
1229
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
910
1230
  * For messages from a remote client, this will be undefined.
911
1231
  * @internal
912
1232
  */
913
- processSetMessage(op, context, local, localOpMetadata) {
1233
+ processSetMessage(msg, op, context, local, localOpMetadata) {
914
1234
  this.throwIfDisposed();
915
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1235
+ if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1236
+ this.needProcessStorageOperation(op, local, localOpMetadata))) {
916
1237
  return;
917
1238
  }
918
1239
  // needProcessStorageOperation should have returned false if local is true
@@ -920,36 +1241,89 @@ class SubDirectory extends TypedEventEmitter {
920
1241
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
921
1242
  this.setCore(op.key, context, local);
922
1243
  }
1244
+ /**
1245
+ * Apply set operation locally and generate metadata
1246
+ * @param op - Op to apply
1247
+ * @returns metadata generated for stahed op
1248
+ */
1249
+ applyStashedSetMessage(op, context) {
1250
+ this.throwIfDisposed();
1251
+ // Set the value locally.
1252
+ const previousValue = this.setCore(op.key, context, true);
1253
+ // Create metadata
1254
+ const pendingMessageId = this.getKeyMessageId(op);
1255
+ const localMetadata = {
1256
+ type: "edit",
1257
+ pendingMessageId,
1258
+ previousValue,
1259
+ };
1260
+ return localMetadata;
1261
+ }
923
1262
  /**
924
1263
  * Process a create subdirectory operation.
1264
+ * @param msg - The message from the server to apply.
925
1265
  * @param op - The op to process
926
1266
  * @param local - Whether the message originated from the local client
927
1267
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
928
1268
  * For messages from a remote client, this will be undefined.
929
1269
  * @internal
930
1270
  */
931
- processCreateSubDirectoryMessage(op, local, localOpMetadata) {
1271
+ processCreateSubDirectoryMessage(msg, op, local, localOpMetadata) {
932
1272
  this.throwIfDisposed();
933
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1273
+ if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1274
+ this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
934
1275
  return;
935
1276
  }
936
- this.createSubDirectoryCore(op.subdirName, local);
1277
+ assertNonNullClientId(msg.clientId);
1278
+ this.createSubDirectoryCore(op.subdirName, local, { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber }, msg.clientId);
1279
+ }
1280
+ /**
1281
+ * Apply createSubDirectory operation locally and generate metadata
1282
+ * @param op - Op to apply
1283
+ * @returns metadata generated for stahed op
1284
+ */
1285
+ applyStashedCreateSubDirMessage(op) {
1286
+ this.throwIfDisposed();
1287
+ // Create the sub directory locally first.
1288
+ this.createSubDirectoryCore(op.subdirName, true, this.getLocalSeq(), this.runtime.clientId ?? "detached");
1289
+ this.updatePendingSubDirMessageCount(op);
1290
+ const localOpMetadata = {
1291
+ type: "createSubDir",
1292
+ };
1293
+ return localOpMetadata;
937
1294
  }
938
1295
  /**
939
1296
  * Process a delete subdirectory operation.
1297
+ * @param msg - The message from the server to apply.
940
1298
  * @param op - The op to process
941
1299
  * @param local - Whether the message originated from the local client
942
1300
  * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
943
1301
  * For messages from a remote client, this will be undefined.
944
1302
  * @internal
945
1303
  */
946
- processDeleteSubDirectoryMessage(op, local, localOpMetadata) {
1304
+ processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata) {
947
1305
  this.throwIfDisposed();
948
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1306
+ if (!(this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1307
+ this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata))) {
949
1308
  return;
950
1309
  }
951
1310
  this.deleteSubDirectoryCore(op.subdirName, local);
952
1311
  }
1312
+ /**
1313
+ * Apply deleteSubDirectory operation locally and generate metadata
1314
+ * @param op - Op to apply
1315
+ * @returns metadata generated for stahed op
1316
+ */
1317
+ applyStashedDeleteSubDirMessage(op) {
1318
+ this.throwIfDisposed();
1319
+ const subDir = this.deleteSubDirectoryCore(op.subdirName, true);
1320
+ this.updatePendingSubDirMessageCount(op);
1321
+ const metadata = {
1322
+ type: "deleteSubDir",
1323
+ subDirectory: subDir,
1324
+ };
1325
+ return metadata;
1326
+ }
953
1327
  /**
954
1328
  * Submit a clear operation.
955
1329
  * @param op - The operation
@@ -974,8 +1348,11 @@ class SubDirectory extends TypedEventEmitter {
974
1348
  assert(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
975
1349
  // We don't reuse the metadata pendingMessageId but send a new one on each submit.
976
1350
  const pendingClearMessageId = this.pendingClearMessageIds.shift();
977
- assert(pendingClearMessageId === localOpMetadata.pendingMessageId, 0x32c /* pendingMessageId does not match */);
978
- this.submitClearMessage(op, localOpMetadata.previousStorage);
1351
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1352
+ // is already deleted, in which case we don't need to submit the op.
1353
+ if (pendingClearMessageId === localOpMetadata.pendingMessageId) {
1354
+ this.submitClearMessage(op, localOpMetadata.previousStorage);
1355
+ }
979
1356
  }
980
1357
  /**
981
1358
  * Get a new pending message id for the op and cache it to track the pending op
@@ -1013,40 +1390,52 @@ class SubDirectory extends TypedEventEmitter {
1013
1390
  assert(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
1014
1391
  // clear the old pending message id
1015
1392
  const pendingMessageIds = this.pendingKeys.get(op.key);
1016
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x32e /* Unexpected pending message received */);
1017
- pendingMessageIds.shift();
1018
- if (pendingMessageIds.length === 0) {
1019
- this.pendingKeys.delete(op.key);
1393
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1394
+ // is already deleted, in which case we don't need to submit the op.
1395
+ if (pendingMessageIds !== undefined) {
1396
+ const index = pendingMessageIds.findIndex((id) => id === localOpMetadata.pendingMessageId);
1397
+ if (index === -1) {
1398
+ return;
1399
+ }
1400
+ pendingMessageIds.splice(index, 1);
1401
+ if (pendingMessageIds.length === 0) {
1402
+ this.pendingKeys.delete(op.key);
1403
+ }
1404
+ this.submitKeyMessage(op, localOpMetadata.previousValue);
1405
+ }
1406
+ }
1407
+ incrementPendingSubDirCount(map, subDirName) {
1408
+ const count = map.get(subDirName) ?? 0;
1409
+ map.set(subDirName, count + 1);
1410
+ }
1411
+ decrementPendingSubDirCount(map, subDirName) {
1412
+ const count = map.get(subDirName) ?? 0;
1413
+ map.set(subDirName, count - 1);
1414
+ if (count <= 1) {
1415
+ map.delete(subDirName);
1020
1416
  }
1021
- this.submitKeyMessage(op, localOpMetadata.previousValue);
1022
1417
  }
1023
1418
  /**
1024
- * Get a new pending message id for the op and cache it to track the pending op
1419
+ * Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
1420
+ * or while resubmitting the op.
1025
1421
  */
1026
- getSubDirMessageId(op) {
1027
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1028
- const newMessageId = ++this.pendingMessageId;
1029
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1030
- if (pendingMessageIds !== undefined) {
1031
- pendingMessageIds.push(newMessageId);
1422
+ updatePendingSubDirMessageCount(op) {
1423
+ if (op.type === "deleteSubDirectory") {
1424
+ this.incrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1032
1425
  }
1033
- else {
1034
- this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
1426
+ else if (op.type === "createSubDirectory") {
1427
+ this.incrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1035
1428
  }
1036
- return newMessageId;
1037
1429
  }
1038
1430
  /**
1039
1431
  * Submit a create subdirectory operation.
1040
1432
  * @param op - The operation
1041
- * @param prevExisted - Whether the subdirectory existed before the op
1042
1433
  */
1043
- submitCreateSubDirectoryMessage(op, prevExisted) {
1434
+ submitCreateSubDirectoryMessage(op) {
1044
1435
  this.throwIfDisposed();
1045
- const newMessageId = this.getSubDirMessageId(op);
1436
+ this.updatePendingSubDirMessageCount(op);
1046
1437
  const localOpMetadata = {
1047
1438
  type: "createSubDir",
1048
- pendingMessageId: newMessageId,
1049
- previouslyExisted: prevExisted,
1050
1439
  };
1051
1440
  this.directory.submitDirectoryMessage(op, localOpMetadata);
1052
1441
  }
@@ -1057,10 +1446,9 @@ class SubDirectory extends TypedEventEmitter {
1057
1446
  */
1058
1447
  submitDeleteSubDirectoryMessage(op, subDir) {
1059
1448
  this.throwIfDisposed();
1060
- const newMessageId = this.getSubDirMessageId(op);
1449
+ this.updatePendingSubDirMessageCount(op);
1061
1450
  const localOpMetadata = {
1062
1451
  type: "deleteSubDir",
1063
- pendingMessageId: newMessageId,
1064
1452
  subDirectory: subDir,
1065
1453
  };
1066
1454
  this.directory.submitDirectoryMessage(op, localOpMetadata);
@@ -1073,17 +1461,22 @@ class SubDirectory extends TypedEventEmitter {
1073
1461
  */
1074
1462
  resubmitSubDirectoryMessage(op, localOpMetadata) {
1075
1463
  assert(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
1076
- // clear the old pending message id
1077
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1078
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x330 /* Unexpected pending message received */);
1079
- pendingMessageIds.shift();
1080
- if (pendingMessageIds.length === 0) {
1081
- this.pendingSubDirectories.delete(op.subdirName);
1464
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1465
+ // is already deleted, in which case we don't need to submit the op.
1466
+ if (localOpMetadata.type === "createSubDir" &&
1467
+ !this.pendingCreateSubDirectoriesTracker.has(op.subdirName)) {
1468
+ return;
1469
+ }
1470
+ else if (localOpMetadata.type === "deleteSubDir" &&
1471
+ !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1472
+ return;
1082
1473
  }
1083
1474
  if (localOpMetadata.type === "createSubDir") {
1084
- this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
1475
+ this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1476
+ this.submitCreateSubDirectoryMessage(op);
1085
1477
  }
1086
1478
  else {
1479
+ this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1087
1480
  this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1088
1481
  }
1089
1482
  }
@@ -1101,6 +1494,14 @@ class SubDirectory extends TypedEventEmitter {
1101
1494
  yield res;
1102
1495
  }
1103
1496
  }
1497
+ getSerializableCreateInfo() {
1498
+ this.throwIfDisposed();
1499
+ const createInfo = {
1500
+ csn: this.seqData.seq,
1501
+ ccIds: Array.from(this.clientIds),
1502
+ };
1503
+ return createInfo;
1504
+ }
1104
1505
  /**
1105
1506
  * Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
1106
1507
  * @param key - The key to populate
@@ -1139,7 +1540,7 @@ class SubDirectory extends TypedEventEmitter {
1139
1540
  */
1140
1541
  rollbackPendingMessageId(map, key, pendingMessageId) {
1141
1542
  const pendingMessageIds = map.get(key);
1142
- const lastPendingMessageId = pendingMessageIds === null || pendingMessageIds === void 0 ? void 0 : pendingMessageIds.pop();
1543
+ const lastPendingMessageId = pendingMessageIds?.pop();
1143
1544
  if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
1144
1545
  throw new Error("Rollback op does not match last pending");
1145
1546
  }
@@ -1147,21 +1548,24 @@ class SubDirectory extends TypedEventEmitter {
1147
1548
  map.delete(key);
1148
1549
  }
1149
1550
  }
1551
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
1150
1552
  /**
1151
1553
  * Rollback a local op
1152
1554
  * @param op - The operation to rollback
1153
1555
  * @param localOpMetadata - The local metadata associated with the op.
1154
1556
  */
1557
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1155
1558
  rollback(op, localOpMetadata) {
1156
1559
  if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
1157
1560
  throw new Error("Invalid localOpMetadata");
1158
1561
  }
1159
1562
  if (op.type === "clear" && localOpMetadata.type === "clear") {
1160
- localOpMetadata.previousStorage.forEach((localValue, key) => {
1563
+ for (const [key, localValue] of localOpMetadata.previousStorage.entries()) {
1161
1564
  this.setCore(key, localValue, true);
1162
- });
1565
+ }
1163
1566
  const lastPendingClearId = this.pendingClearMessageIds.pop();
1164
- if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
1567
+ if (lastPendingClearId === undefined ||
1568
+ lastPendingClearId !== localOpMetadata.pendingMessageId) {
1165
1569
  throw new Error("Rollback op does match last clear");
1166
1570
  }
1167
1571
  }
@@ -1175,24 +1579,34 @@ class SubDirectory extends TypedEventEmitter {
1175
1579
  this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
1176
1580
  }
1177
1581
  else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1178
- if (!localOpMetadata.previouslyExisted) {
1179
- this.deleteSubDirectoryCore(op.subdirName, true);
1180
- }
1181
- this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1582
+ this.deleteSubDirectoryCore(op.subdirName, true);
1583
+ this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1182
1584
  }
1183
1585
  else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
1184
1586
  if (localOpMetadata.subDirectory !== undefined) {
1185
1587
  this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
1186
1588
  // don't need to register events because deleting never unregistered
1187
1589
  this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
1590
+ // Restore the record in creation tracker
1591
+ if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
1592
+ this.ackedCreationSeqTracker.set(op.subdirName, {
1593
+ ...localOpMetadata.subDirectory.seqData,
1594
+ });
1595
+ }
1596
+ else {
1597
+ this.localCreationSeqTracker.set(op.subdirName, {
1598
+ ...localOpMetadata.subDirectory.seqData,
1599
+ });
1600
+ }
1188
1601
  this.emit("subDirectoryCreated", op.subdirName, true, this);
1189
1602
  }
1190
- this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1603
+ this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subDirName);
1191
1604
  }
1192
1605
  else {
1193
1606
  throw new Error("Unsupported op for rollback");
1194
1607
  }
1195
1608
  }
1609
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
1196
1610
  /**
1197
1611
  * Converts the given relative path into an absolute path.
1198
1612
  * @param path - Relative path to convert
@@ -1213,22 +1627,38 @@ class SubDirectory extends TypedEventEmitter {
1213
1627
  needProcessStorageOperation(op, local, localOpMetadata) {
1214
1628
  if (this.pendingClearMessageIds.length > 0) {
1215
1629
  if (local) {
1216
- assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata) &&
1630
+ assert(localOpMetadata !== undefined &&
1631
+ isKeyEditLocalOpMetadata(localOpMetadata) &&
1217
1632
  localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0], 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
1633
+ // Remove all pendingMessageIds lower than first pendingClearMessageId.
1634
+ const lowestPendingClearMessageId = this.pendingClearMessageIds[0];
1635
+ const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
1636
+ if (pendingKeyMessageIdArray !== undefined) {
1637
+ let index = 0;
1638
+ while (pendingKeyMessageIdArray[index] < lowestPendingClearMessageId) {
1639
+ index += 1;
1640
+ }
1641
+ const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
1642
+ if (newPendingKeyMessageId.length === 0) {
1643
+ this.pendingKeys.delete(op.key);
1644
+ }
1645
+ else {
1646
+ this.pendingKeys.set(op.key, newPendingKeyMessageId);
1647
+ }
1648
+ }
1218
1649
  }
1219
1650
  // If I have a NACK clear, we can ignore all ops.
1220
1651
  return false;
1221
1652
  }
1222
- const pendingKeyMessageId = this.pendingKeys.get(op.key);
1223
- if (pendingKeyMessageId !== undefined) {
1653
+ const pendingKeyMessageIds = this.pendingKeys.get(op.key);
1654
+ if (pendingKeyMessageIds !== undefined) {
1224
1655
  // Found an NACK op, clear it from the directory if the latest sequence number in the directory
1225
1656
  // match the message's and don't process the op.
1226
1657
  if (local) {
1227
1658
  assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata), 0x011 /* pendingMessageId is missing from the local client's operation */);
1228
- const pendingMessageIds = this.pendingKeys.get(op.key);
1229
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x331 /* Unexpected pending message received */);
1230
- pendingMessageIds.shift();
1231
- if (pendingMessageIds.length === 0) {
1659
+ assert(pendingKeyMessageIds[0] === localOpMetadata.pendingMessageId, 0x331 /* Unexpected pending message received */);
1660
+ pendingKeyMessageIds.shift();
1661
+ if (pendingKeyMessageIds.length === 0) {
1232
1662
  this.pendingKeys.delete(op.key);
1233
1663
  }
1234
1664
  }
@@ -1237,6 +1667,19 @@ class SubDirectory extends TypedEventEmitter {
1237
1667
  // If we don't have a NACK op on the key, we need to process the remote ops.
1238
1668
  return !local;
1239
1669
  }
1670
+ /**
1671
+ * This return true if the message is for the current instance of this sub directory. As the sub directory
1672
+ * can be deleted and created again, then this finds if the message is for current instance of directory or not.
1673
+ * @param msg - message for the directory
1674
+ */
1675
+ isMessageForCurrentInstanceOfSubDirectory(msg) {
1676
+ // If the message is either from the creator of directory or this directory was created when
1677
+ // container was detached or in case this directory is already live(known to other clients)
1678
+ // and the op was created after the directory was created then apply this op.
1679
+ return ((msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
1680
+ this.clientIds.has("detached") ||
1681
+ (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber));
1682
+ }
1240
1683
  /**
1241
1684
  * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1242
1685
  * not process the incoming operation.
@@ -1247,16 +1690,80 @@ class SubDirectory extends TypedEventEmitter {
1247
1690
  * For messages from a remote client, this will be undefined.
1248
1691
  * @returns True if the operation should be processed, false otherwise
1249
1692
  */
1250
- needProcessSubDirectoryOperation(op, local, localOpMetadata) {
1251
- const pendingSubDirectoryMessageId = this.pendingSubDirectories.get(op.subdirName);
1252
- if (pendingSubDirectoryMessageId !== undefined) {
1693
+ needProcessSubDirectoryOperation(msg, op, local, localOpMetadata) {
1694
+ assertNonNullClientId(msg.clientId);
1695
+ const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
1696
+ const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
1697
+ if ((pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
1698
+ (pendingCreateCount !== undefined && pendingCreateCount > 0)) {
1253
1699
  if (local) {
1254
1700
  assert(isSubDirLocalOpMetadata(localOpMetadata), 0x012 /* pendingMessageId is missing from the local client's operation */);
1255
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1256
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId, 0x332 /* Unexpected pending message received */);
1257
- pendingMessageIds.shift();
1258
- if (pendingMessageIds.length === 0) {
1259
- this.pendingSubDirectories.delete(op.subdirName);
1701
+ if (localOpMetadata.type === "deleteSubDir") {
1702
+ assert(pendingDeleteCount !== undefined && pendingDeleteCount > 0, 0x6c2 /* pendingDeleteCount should exist */);
1703
+ this.decrementPendingSubDirCount(this.pendingDeleteSubDirectoriesTracker, op.subdirName);
1704
+ }
1705
+ else if (localOpMetadata.type === "createSubDir") {
1706
+ assert(pendingCreateCount !== undefined && pendingCreateCount > 0, 0x6c3 /* pendingCreateCount should exist */);
1707
+ this.decrementPendingSubDirCount(this.pendingCreateSubDirectoriesTracker, op.subdirName);
1708
+ }
1709
+ }
1710
+ if (op.type === "deleteSubDirectory") {
1711
+ const resetSubDirectoryTree = (directory) => {
1712
+ if (!directory) {
1713
+ return;
1714
+ }
1715
+ // If this is delete op and we have keys in this subDirectory, then we need to delete these
1716
+ // keys except the pending ones as they will be sequenced after this delete.
1717
+ directory.clearExceptPendingKeys(local);
1718
+ // In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
1719
+ // creators as the previous directory is getting deleted and we will initialize again when
1720
+ // we will receive op for the create again.
1721
+ directory.seqData.seq = -1;
1722
+ directory.seqData.clientSeq = -1;
1723
+ directory.clientIds.clear();
1724
+ // Do the same thing for the subtree of the directory. If create is not pending for a child, then just
1725
+ // delete it.
1726
+ const subDirectories = directory.subdirectories();
1727
+ for (const [subDirName, subDir] of subDirectories) {
1728
+ if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
1729
+ resetSubDirectoryTree(subDir);
1730
+ continue;
1731
+ }
1732
+ directory.deleteSubDirectoryCore(subDirName, false);
1733
+ }
1734
+ };
1735
+ const subDirectory = this._subdirectories.get(op.subdirName);
1736
+ // Clear the creation tracker record
1737
+ this.ackedCreationSeqTracker.delete(op.subdirName);
1738
+ resetSubDirectoryTree(subDirectory);
1739
+ }
1740
+ if (op.type === "createSubDirectory") {
1741
+ const dir = this._subdirectories.get(op.subdirName);
1742
+ // Child sub directory create seq number can't be lower than the parent subdirectory.
1743
+ // The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
1744
+ if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
1745
+ if (dir?.seqData.seq === -1) {
1746
+ // Only set the sequence data based on the first message
1747
+ dir.seqData.seq = msg.sequenceNumber;
1748
+ dir.seqData.clientSeq = msg.clientSequenceNumber;
1749
+ // set the creation seq in tracker
1750
+ if (!this.ackedCreationSeqTracker.has(op.subdirName) &&
1751
+ !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)) {
1752
+ this.ackedCreationSeqTracker.set(op.subdirName, {
1753
+ seq: msg.sequenceNumber,
1754
+ clientSeq: msg.clientSequenceNumber,
1755
+ });
1756
+ if (local) {
1757
+ this.localCreationSeqTracker.delete(op.subdirName);
1758
+ }
1759
+ }
1760
+ }
1761
+ // The client created the dir at or after the dirs seq, so list its client id as a creator.
1762
+ if (dir !== undefined &&
1763
+ !dir.clientIds.has(msg.clientId) &&
1764
+ dir.seqData.seq <= msg.sequenceNumber) {
1765
+ dir.clientIds.add(msg.clientId);
1766
+ }
1260
1767
  }
1261
1768
  }
1262
1769
  return false;
@@ -1266,18 +1773,21 @@ class SubDirectory extends TypedEventEmitter {
1266
1773
  /**
1267
1774
  * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
1268
1775
  */
1269
- clearExceptPendingKeys() {
1776
+ clearExceptPendingKeys(local) {
1270
1777
  // Assuming the pendingKeys is small and the map is large
1271
1778
  // we will get the value for the pendingKeys and clear the map
1272
1779
  const temp = new Map();
1273
- this.pendingKeys.forEach((value, key, map) => {
1274
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1275
- temp.set(key, this._storage.get(key));
1276
- });
1277
- this.clearCore(false);
1278
- temp.forEach((value, key, map) => {
1780
+ for (const [key] of this.pendingKeys) {
1781
+ const value = this._storage.get(key);
1782
+ // If this key is already deleted, then we don't need to add it again.
1783
+ if (value !== undefined) {
1784
+ temp.set(key, value);
1785
+ }
1786
+ }
1787
+ this.clearCore(local);
1788
+ for (const [key, value] of temp.entries()) {
1279
1789
  this.setCore(key, value, true);
1280
- });
1790
+ }
1281
1791
  }
1282
1792
  /**
1283
1793
  * Clear implementation used for both locally sourced clears as well as incoming remote clears.
@@ -1295,7 +1805,7 @@ class SubDirectory extends TypedEventEmitter {
1295
1805
  */
1296
1806
  deleteCore(key, local) {
1297
1807
  const previousLocalValue = this._storage.get(key);
1298
- const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
1808
+ const previousValue = previousLocalValue?.value;
1299
1809
  const successfullyRemoved = this._storage.delete(key);
1300
1810
  if (successfullyRemoved) {
1301
1811
  const event = { key, path: this.absolutePath, previousValue };
@@ -1314,7 +1824,7 @@ class SubDirectory extends TypedEventEmitter {
1314
1824
  */
1315
1825
  setCore(key, value, local) {
1316
1826
  const previousLocalValue = this._storage.get(key);
1317
- const previousValue = previousLocalValue === null || previousLocalValue === void 0 ? void 0 : previousLocalValue.value;
1827
+ const previousValue = previousLocalValue?.value;
1318
1828
  this._storage.set(key, value);
1319
1829
  const event = { key, path: this.absolutePath, previousValue };
1320
1830
  this.directory.emit("valueChanged", event, local, this.directory);
@@ -1326,17 +1836,33 @@ class SubDirectory extends TypedEventEmitter {
1326
1836
  * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1327
1837
  * @param subdirName - The name of the subdirectory being created
1328
1838
  * @param local - Whether the message originated from the local client
1329
- * @returns - True if is newly created, false if it already existed.
1839
+ * @param seqData - Sequence number and client sequence number at which this directory is created
1840
+ * @param clientId - Id of client which created this directory.
1841
+ * @returns True if is newly created, false if it already existed.
1330
1842
  */
1331
- createSubDirectoryCore(subdirName, local) {
1332
- if (!this._subdirectories.has(subdirName)) {
1843
+ createSubDirectoryCore(subdirName, local, seqData, clientId) {
1844
+ const subdir = this._subdirectories.get(subdirName);
1845
+ if (subdir === undefined) {
1333
1846
  const absolutePath = posix.join(this.absolutePath, subdirName);
1334
- const subDir = new SubDirectory(this.directory, this.runtime, this.serializer, absolutePath);
1847
+ const subDir = new SubDirectory({ ...seqData }, new Set([clientId]), this.directory, this.runtime, this.serializer, absolutePath);
1848
+ /**
1849
+ * Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
1850
+ * on whether the creation behavior has been ack'd or not
1851
+ */
1852
+ if (!isAcknowledgedOrDetached(seqData)) {
1853
+ this.localCreationSeqTracker.set(subdirName, { ...seqData });
1854
+ }
1855
+ else {
1856
+ this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
1857
+ }
1335
1858
  this.registerEventsOnSubDirectory(subDir, subdirName);
1336
1859
  this._subdirectories.set(subdirName, subDir);
1337
1860
  this.emit("subDirectoryCreated", subdirName, local, this);
1338
1861
  return true;
1339
1862
  }
1863
+ else {
1864
+ subdir.clientIds.add(clientId);
1865
+ }
1340
1866
  return false;
1341
1867
  }
1342
1868
  registerEventsOnSubDirectory(subDirectory, subDirName) {
@@ -1358,6 +1884,16 @@ class SubDirectory extends TypedEventEmitter {
1358
1884
  // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
1359
1885
  if (previousValue !== undefined) {
1360
1886
  this._subdirectories.delete(subdirName);
1887
+ /**
1888
+ * Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
1889
+ * ack'd already or still not committed yet (could be both).
1890
+ */
1891
+ if (this.ackedCreationSeqTracker.has(subdirName)) {
1892
+ this.ackedCreationSeqTracker.delete(subdirName);
1893
+ }
1894
+ if (this.localCreationSeqTracker.has(subdirName)) {
1895
+ this.localCreationSeqTracker.delete(subdirName);
1896
+ }
1361
1897
  this.disposeSubDirectoryTree(previousValue);
1362
1898
  this.emit("subDirectoryDeleted", subdirName, local, this);
1363
1899
  }
@@ -1377,11 +1913,12 @@ class SubDirectory extends TypedEventEmitter {
1377
1913
  }
1378
1914
  }
1379
1915
  undeleteSubDirectoryTree(directory) {
1380
- // Restore deleted subdirectory tree. This will unmark "deleted" from the subdirectories from bottom to top.
1381
- for (const [_, subDirectory] of this._subdirectories.entries()) {
1916
+ // Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
1917
+ // This will unmark "deleted" from the subdirectories from top to bottom.
1918
+ directory.undispose();
1919
+ for (const [_, subDirectory] of directory.subdirectories()) {
1382
1920
  this.undeleteSubDirectoryTree(subDirectory);
1383
1921
  }
1384
- directory.undispose();
1385
1922
  }
1386
1923
  }
1387
- //# sourceMappingURL=directory.js.map
1924
+ //# sourceMappingURL=directory.mjs.map