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

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