@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
package/src/directory.ts CHANGED
@@ -3,44 +3,40 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
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";
10
+ import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
9
11
  import {
10
- ISequencedDocumentMessage,
11
- MessageType,
12
- } from "@fluidframework/protocol-definitions";
13
- import {
14
- IChannelAttributes,
15
- IFluidDataStoreRuntime,
16
- IChannelStorageService,
17
- IChannelServices,
18
- IChannelFactory,
12
+ IChannelAttributes,
13
+ IFluidDataStoreRuntime,
14
+ IChannelStorageService,
15
+ IChannelServices,
16
+ IChannelFactory,
19
17
  } from "@fluidframework/datastore-definitions";
20
18
  import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtime-definitions";
21
19
  import { IFluidSerializer, SharedObject, ValueType } from "@fluidframework/shared-object-base";
22
20
  import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
23
- import * as path from "path-browserify";
21
+ import path from "path-browserify";
22
+ import { RedBlackTree } from "@fluidframework/merge-tree";
24
23
  import {
25
- IDirectory,
26
- IDirectoryEvents,
27
- IDirectoryValueChanged,
28
- ISerializableValue,
29
- ISerializedValue,
30
- ISharedDirectory,
31
- ISharedDirectoryEvents,
32
- IValueChanged,
24
+ IDirectory,
25
+ IDirectoryEvents,
26
+ IDirectoryValueChanged,
27
+ // eslint-disable-next-line import/no-deprecated
28
+ ISerializableValue,
29
+ ISerializedValue,
30
+ ISharedDirectory,
31
+ ISharedDirectoryEvents,
32
+ IValueChanged,
33
33
  } from "./interfaces";
34
- import {
35
- ILocalValue,
36
- LocalValueMaker,
37
- makeSerializable,
38
- } from "./localValues";
34
+ import { ILocalValue, LocalValueMaker, makeSerializable } from "./localValues";
39
35
  import { pkgVersion } from "./packageVersion";
40
36
 
41
37
  // We use path-browserify since this code can run safely on the server or the browser.
42
38
  // We standardize on using posix slashes everywhere.
43
- const posix: typeof import("path").posix = path.posix;
39
+ const posix = path.posix;
44
40
 
45
41
  const snapshotFileName = "header";
46
42
 
@@ -48,227 +44,402 @@ const snapshotFileName = "header";
48
44
  * Defines the means to process and submit a given op on a directory.
49
45
  */
50
46
  interface IDirectoryMessageHandler {
51
- /**
52
- * Apply the given operation.
53
- * @param op - The directory operation to apply
54
- * @param local - Whether the message originated from the local client
55
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
56
- * For messages from a remote client, this will be undefined.
57
- */
58
- process(
59
- op: IDirectoryOperation,
60
- local: boolean,
61
- localOpMetadata: unknown,
62
- ): void;
63
-
64
- /**
65
- * Communicate the operation to remote clients.
66
- * @param op - The directory operation to submit
67
- * @param localOpMetadata - The metadata to be submitted with the message.
68
- */
69
- submit(op: IDirectoryOperation, localOpMetadata: unknown): void;
47
+ /**
48
+ * Apply the given operation.
49
+ * @param msg - The message from the server to apply.
50
+ * @param op - The directory operation to apply
51
+ * @param local - Whether the message originated from the local client
52
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
53
+ * For messages from a remote client, this will be undefined.
54
+ */
55
+ process(
56
+ msg: ISequencedDocumentMessage,
57
+ op: IDirectoryOperation,
58
+ local: boolean,
59
+ localOpMetadata: unknown,
60
+ ): void;
61
+
62
+ /**
63
+ * Communicate the operation to remote clients.
64
+ * @param op - The directory operation to submit
65
+ * @param localOpMetadata - The metadata to be submitted with the message.
66
+ */
67
+ submit(op: IDirectoryOperation, localOpMetadata: unknown): void;
68
+
69
+ applyStashedOp(op: IDirectoryOperation): unknown;
70
70
  }
71
71
 
72
72
  /**
73
73
  * Operation indicating a value should be set for a key.
74
+ * @alpha
74
75
  */
75
76
  export interface IDirectorySetOperation {
76
- /**
77
- * String identifier of the operation type.
78
- */
79
- type: "set";
80
-
81
- /**
82
- * Directory key being modified.
83
- */
84
- key: string;
85
-
86
- /**
87
- * Absolute path of the directory where the modified key is located.
88
- */
89
- path: string;
90
-
91
- /**
92
- * Value to be set on the key.
93
- */
94
- value: ISerializableValue;
77
+ /**
78
+ * String identifier of the operation type.
79
+ */
80
+ type: "set";
81
+
82
+ /**
83
+ * Directory key being modified.
84
+ */
85
+ key: string;
86
+
87
+ /**
88
+ * Absolute path of the directory where the modified key is located.
89
+ */
90
+ path: string;
91
+
92
+ /**
93
+ * Value to be set on the key.
94
+ */
95
+ // eslint-disable-next-line import/no-deprecated
96
+ value: ISerializableValue;
95
97
  }
96
98
 
97
99
  /**
98
100
  * Operation indicating a key should be deleted from the directory.
101
+ * @alpha
99
102
  */
100
103
  export interface IDirectoryDeleteOperation {
101
- /**
102
- * String identifier of the operation type.
103
- */
104
- type: "delete";
105
-
106
- /**
107
- * Directory key being modified.
108
- */
109
- key: string;
110
-
111
- /**
112
- * Absolute path of the directory where the modified key is located.
113
- */
114
- path: string;
104
+ /**
105
+ * String identifier of the operation type.
106
+ */
107
+ type: "delete";
108
+
109
+ /**
110
+ * Directory key being modified.
111
+ */
112
+ key: string;
113
+
114
+ /**
115
+ * Absolute path of the directory where the modified key is located.
116
+ */
117
+ path: string;
115
118
  }
116
119
 
117
120
  /**
118
- * An operation on a specific key within a directory
121
+ * An operation on a specific key within a directory.
122
+ * @alpha
119
123
  */
120
124
  export type IDirectoryKeyOperation = IDirectorySetOperation | IDirectoryDeleteOperation;
121
125
 
122
126
  /**
123
127
  * Operation indicating the directory should be cleared.
128
+ * @alpha
124
129
  */
125
130
  export interface IDirectoryClearOperation {
126
- /**
127
- * String identifier of the operation type.
128
- */
129
- type: "clear";
130
-
131
- /**
132
- * Absolute path of the directory being cleared.
133
- */
134
- path: string;
131
+ /**
132
+ * String identifier of the operation type.
133
+ */
134
+ type: "clear";
135
+
136
+ /**
137
+ * Absolute path of the directory being cleared.
138
+ */
139
+ path: string;
135
140
  }
136
141
 
137
142
  /**
138
- * An operation on one or more of the keys within a directory
143
+ * An operation on one or more of the keys within a directory.
144
+ * @alpha
139
145
  */
140
146
  export type IDirectoryStorageOperation = IDirectoryKeyOperation | IDirectoryClearOperation;
141
147
 
142
148
  /**
143
149
  * Operation indicating a subdirectory should be created.
150
+ * @alpha
144
151
  */
145
152
  export interface IDirectoryCreateSubDirectoryOperation {
146
- /**
147
- * String identifier of the operation type.
148
- */
149
- type: "createSubDirectory";
150
-
151
- /**
152
- * Absolute path of the directory that will contain the new subdirectory.
153
- */
154
- path: string;
155
-
156
- /**
157
- * Name of the new subdirectory.
158
- */
159
- subdirName: string;
153
+ /**
154
+ * String identifier of the operation type.
155
+ */
156
+ type: "createSubDirectory";
157
+
158
+ /**
159
+ * Absolute path of the directory that will contain the new subdirectory.
160
+ */
161
+ path: string;
162
+
163
+ /**
164
+ * Name of the new subdirectory.
165
+ */
166
+ subdirName: string;
160
167
  }
161
168
 
162
169
  /**
163
170
  * Operation indicating a subdirectory should be deleted.
171
+ * @alpha
164
172
  */
165
173
  export interface IDirectoryDeleteSubDirectoryOperation {
166
- /**
167
- * String identifier of the operation type.
168
- */
169
- type: "deleteSubDirectory";
170
-
171
- /**
172
- * Absolute path of the directory that contains the directory to be deleted.
173
- */
174
- path: string;
175
-
176
- /**
177
- * Name of the subdirectory to be deleted.
178
- */
179
- subdirName: string;
174
+ /**
175
+ * String identifier of the operation type.
176
+ */
177
+ type: "deleteSubDirectory";
178
+
179
+ /**
180
+ * Absolute path of the directory that contains the directory to be deleted.
181
+ */
182
+ path: string;
183
+
184
+ /**
185
+ * Name of the subdirectory to be deleted.
186
+ */
187
+ subdirName: string;
180
188
  }
181
189
 
182
190
  /**
183
- * An operation on the subdirectories within a directory
191
+ * An operation on the subdirectories within a directory.
192
+ * @alpha
184
193
  */
185
- export type IDirectorySubDirectoryOperation = IDirectoryCreateSubDirectoryOperation
186
- | IDirectoryDeleteSubDirectoryOperation;
194
+ export type IDirectorySubDirectoryOperation =
195
+ | IDirectoryCreateSubDirectoryOperation
196
+ | IDirectoryDeleteSubDirectoryOperation;
187
197
 
188
198
  /**
189
- * Any operation on a directory
199
+ * Any operation on a directory.
200
+ * @alpha
190
201
  */
191
202
  export type IDirectoryOperation = IDirectoryStorageOperation | IDirectorySubDirectoryOperation;
192
203
 
204
+ /**
205
+ * Create info for the subdirectory.
206
+ * @alpha
207
+ */
208
+ export interface ICreateInfo {
209
+ /**
210
+ * Sequence number at which this subdirectory was created.
211
+ */
212
+ csn: number;
213
+
214
+ /**
215
+ * clientids of the clients which created this sub directory.
216
+ */
217
+ ccIds: string[];
218
+ }
219
+
193
220
  /**
194
221
  * Defines the in-memory object structure to be used for the conversion to/from serialized.
195
- * @privateRemarks
196
- * Directly used in JSON.stringify, direct result from JSON.parse.
222
+ *
223
+ * @remarks Directly used in
224
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
225
+ * | JSON.stringify}, direct result from
226
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse}.
227
+ * @alpha
197
228
  */
198
229
  export interface IDirectoryDataObject {
199
- storage?: { [key: string]: ISerializableValue; };
200
- subdirectories?: { [subdirName: string]: IDirectoryDataObject; };
230
+ /**
231
+ * Key/value date set by the user.
232
+ */
233
+ // eslint-disable-next-line import/no-deprecated
234
+ storage?: { [key: string]: ISerializableValue };
235
+
236
+ /**
237
+ * Recursive sub-directories {@link IDirectoryDataObject | objects}.
238
+ */
239
+ subdirectories?: { [subdirName: string]: IDirectoryDataObject };
240
+
241
+ /**
242
+ * Create info for the sub directory. Since directories with same name can get deleted/created by multiple clients
243
+ * asynchronously, this info helps us to determine whether the ops where for the current instance of sub directory
244
+ * or not and whether to process them or not based on that. Summaries which were not produced which this change
245
+ * will not have this info and in that case we can still run in eventual consistency issues but that is no worse
246
+ * than the state before this change.
247
+ */
248
+ ci?: ICreateInfo;
201
249
  }
202
250
 
251
+ /**
252
+ * {@link IDirectory} storage format.
253
+ *
254
+ * @internal
255
+ */
203
256
  export interface IDirectoryNewStorageFormat {
204
- blobs: string[];
205
- content: IDirectoryDataObject;
257
+ /**
258
+ * Blob IDs representing larger directory data that was serialized.
259
+ */
260
+ blobs: string[];
261
+
262
+ /**
263
+ * Storage content representing directory data that was not serialized.
264
+ */
265
+ content: IDirectoryDataObject;
206
266
  }
207
267
 
208
268
  /**
209
- * The factory that defines the directory.
269
+ * {@link @fluidframework/datastore-definitions#IChannelFactory} for {@link SharedDirectory}.
270
+ *
210
271
  * @sealed
272
+ * @alpha
273
+ */
274
+ export class DirectoryFactory implements IChannelFactory {
275
+ /**
276
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
277
+ */
278
+ public static readonly Type = "https://graph.microsoft.com/types/directory";
279
+
280
+ /**
281
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
282
+ */
283
+ public static readonly Attributes: IChannelAttributes = {
284
+ type: DirectoryFactory.Type,
285
+ snapshotFormatVersion: "0.1",
286
+ packageVersion: pkgVersion,
287
+ };
288
+
289
+ /**
290
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
291
+ */
292
+ public get type(): string {
293
+ return DirectoryFactory.Type;
294
+ }
295
+
296
+ /**
297
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
298
+ */
299
+ public get attributes(): IChannelAttributes {
300
+ return DirectoryFactory.Attributes;
301
+ }
302
+
303
+ /**
304
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.load}
305
+ */
306
+ public async load(
307
+ runtime: IFluidDataStoreRuntime,
308
+ id: string,
309
+ services: IChannelServices,
310
+ attributes: IChannelAttributes,
311
+ ): Promise<ISharedDirectory> {
312
+ const directory = new SharedDirectory(id, runtime, attributes);
313
+ await directory.load(services);
314
+
315
+ return directory;
316
+ }
317
+
318
+ /**
319
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.create}
320
+ */
321
+ public create(runtime: IFluidDataStoreRuntime, id: string): ISharedDirectory {
322
+ const directory = new SharedDirectory(id, runtime, DirectoryFactory.Attributes);
323
+ directory.initializeLocal();
324
+
325
+ return directory;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * The comparator essentially performs the following procedure to determine the order of subdirectory creation:
331
+ * 1. If subdirectory A has a non-negative 'seq' and subdirectory B has a negative 'seq', subdirectory A is always placed first due to
332
+ * the policy that acknowledged subdirectories precede locally created ones that have not been committed yet.
333
+ *
334
+ * 2. When both subdirectories A and B have a non-negative 'seq', they are compared as follows:
335
+ * - If A and B have different 'seq', they are ordered based on 'seq', and the one with the lower 'seq' will be positioned ahead. Notably this rule
336
+ * should not be applied in the directory ordering, since the lowest 'seq' is -1, when the directory is created locally but not acknowledged yet.
337
+ * - In the case where A and B have equal 'seq', the one with the lower 'clientSeq' will be positioned ahead. This scenario occurs when grouped
338
+ * batching is enabled, and a lower 'clientSeq' indicates that it was processed earlier after the batch was ungrouped.
339
+ *
340
+ * 3. When both subdirectories A and B have a negative 'seq', they are compared as follows:
341
+ * - If A and B have different 'seq', the one with lower 'seq' will be positioned ahead, which indicates the corresponding creation message was
342
+ * acknowledged by the server earlier.
343
+ * - If A and B have equal 'seq', the one with lower 'clientSeq' will be placed at the front. This scenario suggests that both subdirectories A
344
+ * and B were created locally and not acknowledged yet, with the one possessing the lower 'clientSeq' being created earlier.
345
+ *
346
+ * 4. A 'seq' value of zero indicates that the subdirectory was created in detached state, and it is considered acknowledged for the
347
+ * purpose of ordering.
348
+ */
349
+ const seqDataComparator = (a: SequenceData, b: SequenceData) => {
350
+ if (isAcknowledgedOrDetached(a)) {
351
+ if (isAcknowledgedOrDetached(b)) {
352
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
354
+ } else {
355
+ return -1;
356
+ }
357
+ } else {
358
+ if (!isAcknowledgedOrDetached(b)) {
359
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
360
+ return a.seq !== b.seq ? a.seq - b.seq : a.clientSeq! - b.clientSeq!;
361
+ } else {
362
+ return 1;
363
+ }
364
+ }
365
+ };
366
+
367
+ function isAcknowledgedOrDetached(seqData: SequenceData) {
368
+ return seqData.seq >= 0;
369
+ }
370
+
371
+ /**
372
+ * The combination of sequence numebr and client sequence number of a subdirectory
373
+ */
374
+ interface SequenceData {
375
+ seq: number;
376
+ clientSeq?: number;
377
+ }
378
+
379
+ /**
380
+ * A utility class for tracking associations between keys and their creation indices.
381
+ * This is relevant to support map iteration in insertion order, see
382
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/%40%40iterator
383
+ *
384
+ * TODO: It can be combined with the creation tracker utilized in SharedMap
211
385
  */
212
- export class DirectoryFactory {
213
- /**
214
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
215
- */
216
- public static readonly Type = "https://graph.microsoft.com/types/directory";
217
-
218
- /**
219
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
220
- */
221
- public static readonly Attributes: IChannelAttributes = {
222
- type: DirectoryFactory.Type,
223
- snapshotFormatVersion: "0.1",
224
- packageVersion: pkgVersion,
225
- };
226
-
227
- /**
228
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
229
- */
230
- public get type() {
231
- return DirectoryFactory.Type;
232
- }
233
-
234
- /**
235
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
236
- */
237
- public get attributes() {
238
- return DirectoryFactory.Attributes;
239
- }
240
-
241
- /**
242
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.load}
243
- */
244
- public async load(
245
- runtime: IFluidDataStoreRuntime,
246
- id: string,
247
- services: IChannelServices,
248
- attributes: IChannelAttributes): Promise<ISharedDirectory> {
249
- const directory = new SharedDirectory(id, runtime, attributes);
250
- await directory.load(services);
251
-
252
- return directory;
253
- }
254
-
255
- /**
256
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.create}
257
- */
258
- public create(runtime: IFluidDataStoreRuntime, id: string): ISharedDirectory {
259
- const directory = new SharedDirectory(id, runtime, DirectoryFactory.Attributes);
260
- directory.initializeLocal();
261
-
262
- return directory;
263
- }
386
+ class DirectoryCreationTracker {
387
+ readonly indexToKey: RedBlackTree<SequenceData, string>;
388
+
389
+ readonly keyToIndex: Map<string, SequenceData>;
390
+
391
+ constructor() {
392
+ this.indexToKey = new RedBlackTree<SequenceData, string>(seqDataComparator);
393
+ this.keyToIndex = new Map<string, SequenceData>();
394
+ }
395
+
396
+ set(key: string, seqData: SequenceData): void {
397
+ this.indexToKey.put(seqData, key);
398
+ this.keyToIndex.set(key, seqData);
399
+ }
400
+
401
+ has(keyOrSeqData: string | SequenceData): boolean {
402
+ return typeof keyOrSeqData === "string"
403
+ ? this.keyToIndex.has(keyOrSeqData)
404
+ : this.indexToKey.get(keyOrSeqData) !== undefined;
405
+ }
406
+
407
+ delete(keyOrSeqData: string | SequenceData): void {
408
+ if (this.has(keyOrSeqData)) {
409
+ if (typeof keyOrSeqData === "string") {
410
+ const seqData = this.keyToIndex.get(keyOrSeqData) as SequenceData;
411
+ this.keyToIndex.delete(keyOrSeqData);
412
+ this.indexToKey.remove(seqData);
413
+ } else {
414
+ const key = this.indexToKey.get(keyOrSeqData)?.data as string;
415
+ this.indexToKey.remove(keyOrSeqData);
416
+ this.keyToIndex.delete(key);
417
+ }
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Retrieves all subdirectories with creation order that satisfy an optional constraint function.
423
+ * @param constraint - An optional constraint function that filters keys.
424
+ * @returns An array of keys that satisfy the constraint (or all keys if no constraint is provided).
425
+ */
426
+ keys(constraint?: (key: string) => boolean): string[] {
427
+ const keys: string[] = [];
428
+ this.indexToKey.mapRange((node) => {
429
+ if (!constraint || constraint(node.data)) {
430
+ keys.push(node.data);
431
+ }
432
+ return true;
433
+ }, keys);
434
+ return keys;
435
+ }
264
436
  }
265
437
 
266
438
  /**
267
- * SharedDirectory provides a hierarchical organization of map-like data structures as SubDirectories.
268
- * The values stored within can be accessed like a map, and the hierarchy can be navigated using path syntax.
269
- * SubDirectories can be retrieved for use as working directories.
439
+ * {@inheritDoc ISharedDirectory}
270
440
  *
271
441
  * @example
442
+ *
272
443
  * ```typescript
273
444
  * mySharedDirectory.createSubDirectory("a").createSubDirectory("b").createSubDirectory("c").set("foo", val1);
274
445
  * const mySubDir = mySharedDirectory.getWorkingDirectory("/a/b/c");
@@ -276,619 +447,772 @@ export class DirectoryFactory {
276
447
  * ```
277
448
  *
278
449
  * @sealed
450
+ * @alpha
279
451
  */
280
- export class SharedDirectory extends SharedObject<ISharedDirectoryEvents> implements ISharedDirectory {
281
- /**
282
- * Create a new shared directory
283
- *
284
- * @param runtime - Data store runtime the new shared directory belongs to
285
- * @param id - Optional name of the shared directory
286
- * @returns Newly create shared directory (but not attached yet)
287
- */
288
- public static create(runtime: IFluidDataStoreRuntime, id?: string): SharedDirectory {
289
- return runtime.createChannel(id, DirectoryFactory.Type) as SharedDirectory;
290
- }
291
-
292
- /**
293
- * Get a factory for SharedDirectory to register with the data store.
294
- *
295
- * @returns A factory that creates and load SharedDirectory
296
- */
297
- public static getFactory(): IChannelFactory {
298
- return new DirectoryFactory();
299
- }
300
-
301
- /**
302
- * String representation for the class.
303
- */
304
- public [Symbol.toStringTag]: string = "SharedDirectory";
305
-
306
- /**
307
- * {@inheritDoc IDirectory.absolutePath}
308
- */
309
- public get absolutePath(): string {
310
- return this.root.absolutePath;
311
- }
312
-
313
- /**
314
- * @internal
315
- */
316
- public readonly localValueMaker: LocalValueMaker;
317
-
318
- /**
319
- * Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
320
- */
321
- private readonly root: SubDirectory = new SubDirectory(this, this.runtime, this.serializer, posix.sep);
322
-
323
- /**
324
- * Mapping of op types to message handlers.
325
- */
326
- private readonly messageHandlers: Map<string, IDirectoryMessageHandler> = new Map();
327
-
328
- /**
329
- * Constructs a new shared directory. If the object is non-local an id and service interfaces will
330
- * be provided.
331
- * @param id - String identifier for the SharedDirectory
332
- * @param runtime - Data store runtime
333
- * @param type - Type identifier
334
- */
335
- constructor(
336
- id: string,
337
- runtime: IFluidDataStoreRuntime,
338
- attributes: IChannelAttributes,
339
- ) {
340
- super(id, runtime, attributes, "fluid_directory_");
341
- this.localValueMaker = new LocalValueMaker(this.serializer);
342
- this.setMessageHandlers();
343
- // Mirror the containedValueChanged op on the SharedDirectory
344
- this.root.on(
345
- "containedValueChanged",
346
- (changed: IValueChanged, local: boolean) => {
347
- this.emit("containedValueChanged", changed, local, this);
348
- },
349
- );
350
- this.root.on(
351
- "subDirectoryCreated",
352
- (relativePath: string, local: boolean) => {
353
- this.emit("subDirectoryCreated", relativePath, local, this);
354
- },
355
- );
356
- this.root.on(
357
- "subDirectoryDeleted",
358
- (relativePath: string, local: boolean) => {
359
- this.emit("subDirectoryDeleted", relativePath, local, this);
360
- },
361
- );
362
- }
363
-
364
- /**
365
- * {@inheritDoc IDirectory.get}
366
- */
367
- public get<T = any>(key: string): T | undefined {
368
- return this.root.get<T>(key);
369
- }
370
-
371
- /**
372
- * {@inheritDoc IDirectory.set}
373
- */
374
- public set<T = any>(key: string, value: T): this {
375
- this.root.set(key, value);
376
- return this;
377
- }
378
-
379
- public dispose(error?: Error): void {
380
- this.root.dispose(error);
381
- }
382
-
383
- public get disposed(): boolean {
384
- return this.root.disposed;
385
- }
386
-
387
- /**
388
- * Deletes the given key from within this IDirectory.
389
- * @param key - The key to delete
390
- * @returns True if the key existed and was deleted, false if it did not exist
391
- */
392
- public delete(key: string): boolean {
393
- return this.root.delete(key);
394
- }
395
-
396
- /**
397
- * Deletes all keys from within this IDirectory.
398
- */
399
- public clear(): void {
400
- this.root.clear();
401
- }
402
-
403
- /**
404
- * Checks whether the given key exists in this IDirectory.
405
- * @param key - The key to check
406
- * @returns True if the key exists, false otherwise
407
- */
408
- public has(key: string): boolean {
409
- return this.root.has(key);
410
- }
411
-
412
- /**
413
- * The number of entries under this IDirectory.
414
- */
415
- public get size(): number {
416
- return this.root.size;
417
- }
418
-
419
- /**
420
- * Issue a callback on each entry under this IDirectory.
421
- * @param callback - Callback to issue
422
- */
423
- public forEach(callback: (value: any, key: string, map: Map<string, any>) => void): void {
424
- this.root.forEach(callback);
425
- }
426
-
427
- /**
428
- * Get an iterator over the entries under this IDirectory.
429
- * @returns The iterator
430
- */
431
- public [Symbol.iterator](): IterableIterator<[string, any]> {
432
- return this.root[Symbol.iterator]();
433
- }
434
-
435
- /**
436
- * Get an iterator over the entries under this IDirectory.
437
- * @returns The iterator
438
- */
439
- public entries(): IterableIterator<[string, any]> {
440
- return this.root.entries();
441
- }
442
-
443
- /**
444
- * {@inheritDoc IDirectory.countSubDirectory}
445
- */
446
- public countSubDirectory(): number {
447
- return this.root.countSubDirectory();
448
- }
449
-
450
- /**
451
- * Get an iterator over the keys under this IDirectory.
452
- * @returns The iterator
453
- */
454
- public keys(): IterableIterator<string> {
455
- return this.root.keys();
456
- }
457
-
458
- /**
459
- * Get an iterator over the values under this IDirectory.
460
- * @returns The iterator
461
- */
462
- public values(): IterableIterator<any> {
463
- return this.root.values();
464
- }
465
-
466
- /**
467
- * {@inheritDoc IDirectory.createSubDirectory}
468
- */
469
- public createSubDirectory(subdirName: string): IDirectory {
470
- return this.root.createSubDirectory(subdirName);
471
- }
472
-
473
- /**
474
- * {@inheritDoc IDirectory.getSubDirectory}
475
- */
476
- public getSubDirectory(subdirName: string): IDirectory | undefined {
477
- return this.root.getSubDirectory(subdirName);
478
- }
479
-
480
- /**
481
- * {@inheritDoc IDirectory.hasSubDirectory}
482
- */
483
- public hasSubDirectory(subdirName: string): boolean {
484
- return this.root.hasSubDirectory(subdirName);
485
- }
486
-
487
- /**
488
- * {@inheritDoc IDirectory.deleteSubDirectory}
489
- */
490
- public deleteSubDirectory(subdirName: string): boolean {
491
- return this.root.deleteSubDirectory(subdirName);
492
- }
493
-
494
- /**
495
- * {@inheritDoc IDirectory.subdirectories}
496
- */
497
- public subdirectories(): IterableIterator<[string, IDirectory]> {
498
- return this.root.subdirectories();
499
- }
500
-
501
- /**
502
- * {@inheritDoc IDirectory.getWorkingDirectory}
503
- */
504
- public getWorkingDirectory(relativePath: string): IDirectory | undefined {
505
- const absolutePath = this.makeAbsolute(relativePath);
506
- if (absolutePath === posix.sep) {
507
- return this.root;
508
- }
509
-
510
- let currentSubDir = this.root;
511
- const subdirs = absolutePath.substr(1).split(posix.sep);
512
- for (const subdir of subdirs) {
513
- currentSubDir = currentSubDir.getSubDirectory(subdir) as SubDirectory;
514
- if (!currentSubDir) {
515
- return undefined;
516
- }
517
- }
518
- return currentSubDir;
519
- }
520
-
521
- /**
522
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
523
- * @internal
524
- */
525
- protected summarizeCore(
526
- serializer: IFluidSerializer,
527
- telemetryContext?: ITelemetryContext,
528
- ): ISummaryTreeWithStats {
529
- return this.serializeDirectory(this.root, serializer);
530
- }
531
-
532
- /**
533
- * Submits an operation
534
- * @param op - Op to submit
535
- * @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
536
- * this op while it has not been ack'd. This will be sent when we receive this op back from the server.
537
- * @internal
538
- */
539
- public submitDirectoryMessage(op: IDirectoryOperation, localOpMetadata: unknown) {
540
- this.submitLocalMessage(op, localOpMetadata);
541
- }
542
-
543
- /**
544
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
545
- * @internal
546
- */
547
- protected onDisconnect() {}
548
-
549
- /**
550
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
551
- * @internal
552
- */
553
- protected reSubmitCore(content: any, localOpMetadata: unknown) {
554
- const message = content as IDirectoryOperation;
555
- const handler = this.messageHandlers.get(message.type);
556
- assert(handler !== undefined, 0x00d /* Missing message handler for message type */);
557
- handler.submit(message, localOpMetadata);
558
- }
559
-
560
- /**
561
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
562
- * @internal
563
- */
564
- protected async loadCore(storage: IChannelStorageService) {
565
- const data = await readAndParse(storage, snapshotFileName);
566
- const newFormat = data as IDirectoryNewStorageFormat;
567
- if (Array.isArray(newFormat.blobs)) {
568
- // New storage format
569
- this.populate(newFormat.content);
570
- await Promise.all(newFormat.blobs.map(async (value) => {
571
- const dataExtra = await readAndParse(storage, value);
572
- this.populate(dataExtra as IDirectoryDataObject);
573
- }));
574
- } else {
575
- // Old storage format
576
- this.populate(data as IDirectoryDataObject);
577
- }
578
- }
579
-
580
- /**
581
- * Populate the directory with the given directory data.
582
- * @param data - A JSON string containing serialized directory data
583
- * @internal
584
- */
585
- protected populate(data: IDirectoryDataObject) {
586
- const stack: [SubDirectory, IDirectoryDataObject][] = [];
587
- stack.push([this.root, data]);
588
- while (stack.length > 0) {
589
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
590
- const [currentSubDir, currentSubDirObject] = stack.pop()!;
591
- if (currentSubDirObject.subdirectories) {
592
- for (const [subdirName, subdirObject] of Object.entries(currentSubDirObject.subdirectories)) {
593
- let newSubDir = currentSubDir.getSubDirectory(subdirName) as SubDirectory;
594
- if (!newSubDir) {
595
- newSubDir = new SubDirectory(
596
- this,
597
- this.runtime,
598
- this.serializer,
599
- posix.join(currentSubDir.absolutePath, subdirName),
600
- );
601
- currentSubDir.populateSubDirectory(subdirName, newSubDir);
602
- }
603
- stack.push([newSubDir, subdirObject]);
604
- }
605
- }
606
-
607
- if (currentSubDirObject.storage) {
608
- for (const [key, serializable] of Object.entries(currentSubDirObject.storage)) {
609
- const localValue = this.makeLocal(
610
- key,
611
- currentSubDir.absolutePath,
612
- serializable,
613
- );
614
- currentSubDir.populateStorage(key, localValue);
615
- }
616
- }
617
- }
618
- }
619
-
620
- /**
621
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
622
- * @internal
623
- */
624
- protected processCore(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown): void {
625
- if (message.type === MessageType.Operation) {
626
- const op: IDirectoryOperation = message.contents as IDirectoryOperation;
627
- const handler = this.messageHandlers.get(op.type);
628
- assert(handler !== undefined, 0x00e /* Missing message handler for message type */);
629
- handler.process(op, local, localOpMetadata);
630
- }
631
- }
632
-
633
- /**
634
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
635
- * @internal
636
- */
637
- protected rollback(content: any, localOpMetadata: unknown) {
638
- const op: IDirectoryOperation = content as IDirectoryOperation;
639
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
640
- if (subdir) {
641
- subdir.rollback(op, localOpMetadata);
642
- }
643
- }
644
-
645
- /**
646
- * Converts the given relative path to absolute against the root.
647
- * @param relativePath - The path to convert
648
- */
649
- private makeAbsolute(relativePath: string): string {
650
- return posix.resolve(posix.sep, relativePath);
651
- }
652
-
653
- /**
654
- * The remote ISerializableValue we're receiving (either as a result of a snapshot load or an incoming set op)
655
- * will have the information we need to create a real object, but will not be the real object yet. For example,
656
- * we might know it's a map and the ID but not have the actual map or its data yet. makeLocal's job
657
- * is to convert that information into a real object for local usage.
658
- * @param key - Key of element being converted
659
- * @param absolutePath - Path of element being converted
660
- * @param serializable - The remote information that we can convert into a real object
661
- * @returns The local value that was produced
662
- */
663
- private makeLocal(
664
- key: string,
665
- absolutePath: string,
666
- serializable: ISerializableValue,
667
- ): ILocalValue {
668
- assert(
669
- serializable.type === ValueType[ValueType.Plain] || serializable.type === ValueType[ValueType.Shared],
670
- 0x1e4 /* "Unexpected serializable type" */,
671
- );
672
- return this.localValueMaker.fromSerializable(serializable);
673
- }
674
-
675
- /**
676
- * Set the message handlers for the directory.
677
- */
678
- private setMessageHandlers(): void {
679
- this.messageHandlers.set(
680
- "clear",
681
- {
682
- process: (op: IDirectoryClearOperation, local, localOpMetadata) => {
683
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
684
- if (subdir) {
685
- subdir.processClearMessage(op, local, localOpMetadata);
686
- }
687
- },
688
- submit: (op: IDirectoryClearOperation, localOpMetadata: unknown) => {
689
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
690
- if (subdir) {
691
- subdir.resubmitClearMessage(op, localOpMetadata);
692
- }
693
- },
694
- },
695
- );
696
- this.messageHandlers.set(
697
- "delete",
698
- {
699
- process: (op: IDirectoryDeleteOperation, local, localOpMetadata) => {
700
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
701
- if (subdir) {
702
- subdir.processDeleteMessage(op, local, localOpMetadata);
703
- }
704
- },
705
- submit: (op: IDirectoryDeleteOperation, localOpMetadata: unknown) => {
706
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
707
- if (subdir) {
708
- subdir.resubmitKeyMessage(op, localOpMetadata);
709
- }
710
- },
711
- },
712
- );
713
- this.messageHandlers.set(
714
- "set",
715
- {
716
- process: (op: IDirectorySetOperation, local, localOpMetadata) => {
717
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
718
- if (subdir) {
719
- const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
720
- subdir.processSetMessage(op, context, local, localOpMetadata);
721
- }
722
- },
723
- submit: (op: IDirectorySetOperation, localOpMetadata: unknown) => {
724
- const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
725
- if (subdir) {
726
- subdir.resubmitKeyMessage(op, localOpMetadata);
727
- }
728
- },
729
- },
730
- );
731
-
732
- this.messageHandlers.set(
733
- "createSubDirectory",
734
- {
735
- process: (op: IDirectoryCreateSubDirectoryOperation, local, localOpMetadata) => {
736
- const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
737
- if (parentSubdir) {
738
- parentSubdir.processCreateSubDirectoryMessage(op, local, localOpMetadata);
739
- }
740
- },
741
- submit: (op: IDirectoryCreateSubDirectoryOperation, localOpMetadata: unknown) => {
742
- const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
743
- if (parentSubdir) {
744
- // We don't reuse the metadata but send a new one on each submit.
745
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
746
- }
747
- },
748
- },
749
- );
750
-
751
- this.messageHandlers.set(
752
- "deleteSubDirectory",
753
- {
754
- process: (op: IDirectoryDeleteSubDirectoryOperation, local, localOpMetadata) => {
755
- const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
756
- if (parentSubdir) {
757
- parentSubdir.processDeleteSubDirectoryMessage(op, local, localOpMetadata);
758
- }
759
- },
760
- submit: (op: IDirectoryDeleteSubDirectoryOperation, localOpMetadata: unknown) => {
761
- const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
762
- if (parentSubdir) {
763
- // We don't reuse the metadata but send a new one on each submit.
764
- parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
765
- }
766
- },
767
- },
768
- );
769
- }
770
-
771
- /**
772
- * @internal
773
- */
774
- protected applyStashedOp() {
775
- throw new Error("not implemented");
776
- }
777
-
778
- private serializeDirectory(
779
- root: SubDirectory,
780
- serializer: IFluidSerializer,
781
- telemetryContext?: ITelemetryContext,
782
- ): ISummaryTreeWithStats {
783
- const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
784
-
785
- const builder = new SummaryTreeBuilder();
786
- let counter = 0;
787
- const blobs: string[] = [];
788
-
789
- const stack: [SubDirectory, IDirectoryDataObject][] = [];
790
- const content: IDirectoryDataObject = {};
791
- stack.push([root, content]);
792
-
793
- while (stack.length > 0) {
794
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
795
- const [currentSubDir, currentSubDirObject] = stack.pop()!;
796
- for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
797
- if (!currentSubDirObject.storage) {
798
- currentSubDirObject.storage = {};
799
- }
800
- const result: ISerializableValue = {
801
- type: value.type,
802
- // eslint-disable-next-line @typescript-eslint/ban-types
803
- value: value.value && JSON.parse(value.value) as object,
804
- };
805
- if (value.value && value.value.length >= MinValueSizeSeparateSnapshotBlob) {
806
- const extraContent: IDirectoryDataObject = {};
807
- let largeContent = extraContent;
808
- if (currentSubDir.absolutePath !== posix.sep) {
809
- for (const dir of currentSubDir.absolutePath.substr(1).split(posix.sep)) {
810
- const subDataObject: IDirectoryDataObject = {};
811
- largeContent.subdirectories = { [dir]: subDataObject };
812
- largeContent = subDataObject;
813
- }
814
- }
815
- largeContent.storage = { [key]: result };
816
- const blobName = `blob${counter}`;
817
- counter++;
818
- blobs.push(blobName);
819
- builder.addBlob(blobName, JSON.stringify(extraContent));
820
- } else {
821
- currentSubDirObject.storage[key] = result;
822
- }
823
- }
824
-
825
- for (const [subdirName, subdir] of currentSubDir.subdirectories()) {
826
- if (!currentSubDirObject.subdirectories) {
827
- currentSubDirObject.subdirectories = {};
828
- }
829
- const subDataObject: IDirectoryDataObject = {};
830
- currentSubDirObject.subdirectories[subdirName] = subDataObject;
831
- stack.push([subdir as SubDirectory, subDataObject]);
832
- }
833
- }
834
-
835
- const newFormat: IDirectoryNewStorageFormat = {
836
- blobs,
837
- content,
838
- };
839
- builder.addBlob(snapshotFileName, JSON.stringify(newFormat));
840
-
841
- return builder.getSummaryTree();
842
- }
452
+ export class SharedDirectory
453
+ extends SharedObject<ISharedDirectoryEvents>
454
+ implements ISharedDirectory
455
+ {
456
+ /**
457
+ * Create a new shared directory
458
+ *
459
+ * @param runtime - Data store runtime the new shared directory belongs to
460
+ * @param id - Optional name of the shared directory
461
+ * @returns Newly create shared directory (but not attached yet)
462
+ */
463
+ public static create(runtime: IFluidDataStoreRuntime, id?: string): SharedDirectory {
464
+ return runtime.createChannel(id, DirectoryFactory.Type) as SharedDirectory;
465
+ }
466
+
467
+ /**
468
+ * Get a factory for SharedDirectory to register with the data store.
469
+ *
470
+ * @returns A factory that creates and load SharedDirectory
471
+ */
472
+ public static getFactory(): IChannelFactory {
473
+ return new DirectoryFactory();
474
+ }
475
+
476
+ /**
477
+ * String representation for the class.
478
+ */
479
+ public [Symbol.toStringTag]: string = "SharedDirectory";
480
+
481
+ /**
482
+ * {@inheritDoc IDirectory.absolutePath}
483
+ */
484
+ public get absolutePath(): string {
485
+ return this.root.absolutePath;
486
+ }
487
+
488
+ /***/
489
+ public readonly localValueMaker: LocalValueMaker;
490
+
491
+ /**
492
+ * Root of the SharedDirectory, most operations on the SharedDirectory itself act on the root.
493
+ */
494
+ private readonly root: SubDirectory = new SubDirectory(
495
+ { seq: 0, clientSeq: 0 },
496
+ new Set(),
497
+ this,
498
+ this.runtime,
499
+ this.serializer,
500
+ posix.sep,
501
+ );
502
+
503
+ /**
504
+ * Mapping of op types to message handlers.
505
+ */
506
+ private readonly messageHandlers: Map<string, IDirectoryMessageHandler> = new Map();
507
+
508
+ /**
509
+ * Constructs a new shared directory. If the object is non-local an id and service interfaces will
510
+ * be provided.
511
+ * @param id - String identifier for the SharedDirectory
512
+ * @param runtime - Data store runtime
513
+ * @param type - Type identifier
514
+ */
515
+ public constructor(
516
+ id: string,
517
+ runtime: IFluidDataStoreRuntime,
518
+ attributes: IChannelAttributes,
519
+ ) {
520
+ super(id, runtime, attributes, "fluid_directory_");
521
+ this.localValueMaker = new LocalValueMaker(this.serializer);
522
+ this.setMessageHandlers();
523
+ // Mirror the containedValueChanged op on the SharedDirectory
524
+ this.root.on("containedValueChanged", (changed: IValueChanged, local: boolean) => {
525
+ this.emit("containedValueChanged", changed, local, this);
526
+ });
527
+ this.root.on("subDirectoryCreated", (relativePath: string, local: boolean) => {
528
+ this.emit("subDirectoryCreated", relativePath, local, this);
529
+ });
530
+ this.root.on("subDirectoryDeleted", (relativePath: string, local: boolean) => {
531
+ this.emit("subDirectoryDeleted", relativePath, local, this);
532
+ });
533
+ }
534
+
535
+ /**
536
+ * {@inheritDoc IDirectory.get}
537
+ */
538
+ // TODO: Use `unknown` instead (breaking change).
539
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
540
+ public get<T = any>(key: string): T | undefined {
541
+ return this.root.get<T>(key);
542
+ }
543
+
544
+ /**
545
+ * {@inheritDoc IDirectory.set}
546
+ */
547
+ public set<T = unknown>(key: string, value: T): this {
548
+ this.root.set(key, value);
549
+ return this;
550
+ }
551
+
552
+ public dispose(error?: Error): void {
553
+ this.root.dispose(error);
554
+ }
555
+
556
+ public get disposed(): boolean {
557
+ return this.root.disposed;
558
+ }
559
+
560
+ /**
561
+ * Deletes the given key from within this IDirectory.
562
+ * @param key - The key to delete
563
+ * @returns True if the key existed and was deleted, false if it did not exist
564
+ */
565
+ public delete(key: string): boolean {
566
+ return this.root.delete(key);
567
+ }
568
+
569
+ /**
570
+ * Deletes all keys from within this IDirectory.
571
+ */
572
+ public clear(): void {
573
+ this.root.clear();
574
+ }
575
+
576
+ /**
577
+ * Checks whether the given key exists in this IDirectory.
578
+ * @param key - The key to check
579
+ * @returns True if the key exists, false otherwise
580
+ */
581
+ public has(key: string): boolean {
582
+ return this.root.has(key);
583
+ }
584
+
585
+ /**
586
+ * The number of entries under this IDirectory.
587
+ */
588
+ public get size(): number {
589
+ return this.root.size;
590
+ }
591
+
592
+ /**
593
+ * Issue a callback on each entry under this IDirectory.
594
+ * @param callback - Callback to issue
595
+ */
596
+ // TODO: Use `unknown` instead (breaking change).
597
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
598
+ public forEach(callback: (value: any, key: string, map: Map<string, any>) => void): void {
599
+ // eslint-disable-next-line unicorn/no-array-for-each, unicorn/no-array-callback-reference
600
+ this.root.forEach(callback);
601
+ }
602
+
603
+ /**
604
+ * Get an iterator over the entries under this IDirectory.
605
+ * @returns The iterator
606
+ */
607
+ // TODO: Use `unknown` instead (breaking change).
608
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
609
+ public [Symbol.iterator](): IterableIterator<[string, any]> {
610
+ return this.root[Symbol.iterator]();
611
+ }
612
+
613
+ /**
614
+ * Get an iterator over the entries under this IDirectory.
615
+ * @returns The iterator
616
+ */
617
+ // TODO: Use `unknown` instead (breaking change).
618
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
619
+ public entries(): IterableIterator<[string, any]> {
620
+ return this.root.entries();
621
+ }
622
+
623
+ /**
624
+ * {@inheritDoc IDirectory.countSubDirectory}
625
+ */
626
+ public countSubDirectory(): number {
627
+ return this.root.countSubDirectory();
628
+ }
629
+
630
+ /**
631
+ * Get an iterator over the keys under this IDirectory.
632
+ * @returns The iterator
633
+ */
634
+ public keys(): IterableIterator<string> {
635
+ return this.root.keys();
636
+ }
637
+
638
+ /**
639
+ * Get an iterator over the values under this IDirectory.
640
+ * @returns The iterator
641
+ */
642
+ // TODO: Use `unknown` instead (breaking change).
643
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
644
+ public values(): IterableIterator<any> {
645
+ return this.root.values();
646
+ }
647
+
648
+ /**
649
+ * {@inheritDoc IDirectory.createSubDirectory}
650
+ */
651
+ public createSubDirectory(subdirName: string): IDirectory {
652
+ return this.root.createSubDirectory(subdirName);
653
+ }
654
+
655
+ /**
656
+ * {@inheritDoc IDirectory.getSubDirectory}
657
+ */
658
+ public getSubDirectory(subdirName: string): IDirectory | undefined {
659
+ return this.root.getSubDirectory(subdirName);
660
+ }
661
+
662
+ /**
663
+ * {@inheritDoc IDirectory.hasSubDirectory}
664
+ */
665
+ public hasSubDirectory(subdirName: string): boolean {
666
+ return this.root.hasSubDirectory(subdirName);
667
+ }
668
+
669
+ /**
670
+ * {@inheritDoc IDirectory.deleteSubDirectory}
671
+ */
672
+ public deleteSubDirectory(subdirName: string): boolean {
673
+ return this.root.deleteSubDirectory(subdirName);
674
+ }
675
+
676
+ /**
677
+ * {@inheritDoc IDirectory.subdirectories}
678
+ */
679
+ public subdirectories(): IterableIterator<[string, IDirectory]> {
680
+ return this.root.subdirectories();
681
+ }
682
+
683
+ /**
684
+ * {@inheritDoc IDirectory.getWorkingDirectory}
685
+ */
686
+ public getWorkingDirectory(relativePath: string): IDirectory | undefined {
687
+ const absolutePath = this.makeAbsolute(relativePath);
688
+ if (absolutePath === posix.sep) {
689
+ return this.root;
690
+ }
691
+
692
+ let currentSubDir = this.root;
693
+ const subdirs = absolutePath.slice(1).split(posix.sep);
694
+ for (const subdir of subdirs) {
695
+ currentSubDir = currentSubDir.getSubDirectory(subdir) as SubDirectory;
696
+ if (!currentSubDir) {
697
+ return undefined;
698
+ }
699
+ }
700
+ return currentSubDir;
701
+ }
702
+
703
+ /**
704
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
705
+ */
706
+ protected summarizeCore(
707
+ serializer: IFluidSerializer,
708
+ telemetryContext?: ITelemetryContext,
709
+ ): ISummaryTreeWithStats {
710
+ return this.serializeDirectory(this.root, serializer);
711
+ }
712
+
713
+ /**
714
+ * Submits an operation
715
+ * @param op - Op to submit
716
+ * @param localOpMetadata - The local metadata associated with the op. We send a unique id that is used to track
717
+ * this op while it has not been ack'd. This will be sent when we receive this op back from the server.
718
+ */
719
+ public submitDirectoryMessage(op: IDirectoryOperation, localOpMetadata: unknown): void {
720
+ this.submitLocalMessage(op, localOpMetadata);
721
+ }
722
+
723
+ /**
724
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
725
+ */
726
+ protected onDisconnect(): void {}
727
+
728
+ /**
729
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
730
+ */
731
+ protected reSubmitCore(content: unknown, localOpMetadata: unknown): void {
732
+ const message = content as IDirectoryOperation;
733
+ const handler = this.messageHandlers.get(message.type);
734
+ assert(handler !== undefined, 0x00d /* Missing message handler for message type */);
735
+ handler.submit(message, localOpMetadata);
736
+ }
737
+
738
+ /**
739
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
740
+ */
741
+ protected async loadCore(storage: IChannelStorageService): Promise<void> {
742
+ const data = await readAndParse(storage, snapshotFileName);
743
+ const newFormat = data as IDirectoryNewStorageFormat;
744
+ if (Array.isArray(newFormat.blobs)) {
745
+ // New storage format
746
+ this.populate(newFormat.content);
747
+ await Promise.all(
748
+ newFormat.blobs.map(async (value) => {
749
+ const dataExtra = await readAndParse(storage, value);
750
+ this.populate(dataExtra as IDirectoryDataObject);
751
+ }),
752
+ );
753
+ } else {
754
+ // Old storage format
755
+ this.populate(data as IDirectoryDataObject);
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Populate the directory with the given directory data.
761
+ * @param data - A JSON string containing serialized directory data
762
+ */
763
+ protected populate(data: IDirectoryDataObject): void {
764
+ const stack: [SubDirectory, IDirectoryDataObject][] = [];
765
+ stack.push([this.root, data]);
766
+
767
+ while (stack.length > 0) {
768
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
769
+ const [currentSubDir, currentSubDirObject] = stack.pop()!;
770
+ if (currentSubDirObject.subdirectories) {
771
+ // Utilize a map to store the seq -> clientSeq for the newly created subdirectory
772
+ const tempSeqNums = new Map<number, number>();
773
+ for (const [subdirName, subdirObject] of Object.entries(
774
+ currentSubDirObject.subdirectories,
775
+ )) {
776
+ let newSubDir = currentSubDir.getSubDirectory(subdirName) as SubDirectory;
777
+ let seqData: SequenceData;
778
+ if (!newSubDir) {
779
+ const createInfo = subdirObject.ci;
780
+ // We do not store the client sequence number in the storage because the order has already been
781
+ // guaranteed during the serialization process. As a result, it is only essential to utilize the
782
+ // "fake" client sequence number to signify the loading order, and there is no need to retain
783
+ // the actual client sequence number at this point.
784
+ if (createInfo !== undefined && createInfo.csn > -1) {
785
+ // If csn is -1, then initialize it with 0, otherwise we will never process ops for this
786
+ // sub directory. This could be done at serialization time too, but we need to maintain
787
+ // back compat too and also we will actually know the state when it was serialized.
788
+ if (!tempSeqNums.has(createInfo.csn)) {
789
+ tempSeqNums.set(createInfo.csn, 0);
790
+ }
791
+ let fakeClientSeq = tempSeqNums.get(createInfo.csn) as number;
792
+ seqData = { seq: createInfo.csn, clientSeq: fakeClientSeq };
793
+ tempSeqNums.set(createInfo.csn, ++fakeClientSeq);
794
+ } else {
795
+ seqData = {
796
+ seq: 0,
797
+ clientSeq: ++currentSubDir.localCreationSeq,
798
+ };
799
+ }
800
+ newSubDir = new SubDirectory(
801
+ seqData,
802
+ createInfo !== undefined
803
+ ? new Set<string>(createInfo.ccIds)
804
+ : new Set(),
805
+ this,
806
+ this.runtime,
807
+ this.serializer,
808
+ posix.join(currentSubDir.absolutePath, subdirName),
809
+ );
810
+ currentSubDir.populateSubDirectory(subdirName, newSubDir);
811
+ // Record the newly inserted subdirectory to the creation tracker
812
+ currentSubDir.ackedCreationSeqTracker.set(subdirName, {
813
+ ...seqData,
814
+ });
815
+ }
816
+ stack.push([newSubDir, subdirObject]);
817
+ }
818
+ }
819
+
820
+ if (currentSubDirObject.storage) {
821
+ for (const [key, serializable] of Object.entries(currentSubDirObject.storage)) {
822
+ const localValue = this.makeLocal(
823
+ key,
824
+ currentSubDir.absolutePath,
825
+ serializable,
826
+ );
827
+ currentSubDir.populateStorage(key, localValue);
828
+ }
829
+ }
830
+ }
831
+ }
832
+
833
+ /**
834
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
835
+ */
836
+ protected processCore(
837
+ message: ISequencedDocumentMessage,
838
+ local: boolean,
839
+ localOpMetadata: unknown,
840
+ ): void {
841
+ if (message.type === MessageType.Operation) {
842
+ const op: IDirectoryOperation = message.contents as IDirectoryOperation;
843
+ const handler = this.messageHandlers.get(op.type);
844
+ assert(handler !== undefined, 0x00e /* Missing message handler for message type */);
845
+ handler.process(message, op, local, localOpMetadata);
846
+ }
847
+ }
848
+
849
+ /**
850
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
851
+ */
852
+ protected rollback(content: unknown, localOpMetadata: unknown): void {
853
+ const op: IDirectoryOperation = content as IDirectoryOperation;
854
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
855
+ if (subdir) {
856
+ subdir.rollback(op, localOpMetadata);
857
+ }
858
+ }
859
+
860
+ /**
861
+ * Converts the given relative path to absolute against the root.
862
+ * @param relativePath - The path to convert
863
+ */
864
+ private makeAbsolute(relativePath: string): string {
865
+ return posix.resolve(posix.sep, relativePath);
866
+ }
867
+
868
+ /**
869
+ * The remote ISerializableValue we're receiving (either as a result of a snapshot load or an incoming set op)
870
+ * will have the information we need to create a real object, but will not be the real object yet. For example,
871
+ * we might know it's a map and the ID but not have the actual map or its data yet. makeLocal's job
872
+ * is to convert that information into a real object for local usage.
873
+ * @param key - Key of element being converted
874
+ * @param absolutePath - Path of element being converted
875
+ * @param serializable - The remote information that we can convert into a real object
876
+ * @returns The local value that was produced
877
+ */
878
+ private makeLocal(
879
+ key: string,
880
+ absolutePath: string,
881
+ // eslint-disable-next-line import/no-deprecated
882
+ serializable: ISerializableValue,
883
+ ): ILocalValue {
884
+ assert(
885
+ serializable.type === ValueType[ValueType.Plain] ||
886
+ serializable.type === ValueType[ValueType.Shared],
887
+ 0x1e4 /* "Unexpected serializable type" */,
888
+ );
889
+ return this.localValueMaker.fromSerializable(serializable);
890
+ }
891
+
892
+ /**
893
+ * This checks if there is pending delete op for local delete for a any subdir in the relative path.
894
+ * @param relativePath - path of sub directory.
895
+ * @returns `true` if there is pending delete, `false` otherwise.
896
+ */
897
+ private isSubDirectoryDeletePending(relativePath: string): boolean {
898
+ const absolutePath = this.makeAbsolute(relativePath);
899
+ if (absolutePath === posix.sep) {
900
+ return false;
901
+ }
902
+ let currentParent = this.root;
903
+ const nodeList = absolutePath.split(posix.sep);
904
+ let start = 1;
905
+ while (start < nodeList.length) {
906
+ const subDirName = nodeList[start];
907
+ if (currentParent.isSubDirectoryDeletePending(subDirName)) {
908
+ return true;
909
+ }
910
+ currentParent = currentParent.getSubDirectory(subDirName) as SubDirectory;
911
+ if (currentParent === undefined) {
912
+ return true;
913
+ }
914
+ start += 1;
915
+ }
916
+ return false;
917
+ }
918
+
919
+ /**
920
+ * Set the message handlers for the directory.
921
+ */
922
+ private setMessageHandlers(): void {
923
+ this.messageHandlers.set("clear", {
924
+ process: (
925
+ msg: ISequencedDocumentMessage,
926
+ op: IDirectoryClearOperation,
927
+ local,
928
+ localOpMetadata,
929
+ ) => {
930
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
931
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
932
+ // as we are going to delete this subDirectory.
933
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
934
+ subdir.processClearMessage(msg, op, local, localOpMetadata);
935
+ }
936
+ },
937
+ submit: (op: IDirectoryClearOperation, localOpMetadata: unknown) => {
938
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
939
+ if (subdir) {
940
+ subdir.resubmitClearMessage(op, localOpMetadata);
941
+ }
942
+ },
943
+ applyStashedOp: (op: IDirectoryClearOperation): IClearLocalOpMetadata | undefined => {
944
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
945
+ if (subdir) {
946
+ return subdir.applyStashedClearMessage(op);
947
+ }
948
+ },
949
+ });
950
+ this.messageHandlers.set("delete", {
951
+ process: (
952
+ msg: ISequencedDocumentMessage,
953
+ op: IDirectoryDeleteOperation,
954
+ local,
955
+ localOpMetadata,
956
+ ) => {
957
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
958
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
959
+ // as we are going to delete this subDirectory.
960
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
961
+ subdir.processDeleteMessage(msg, op, local, localOpMetadata);
962
+ }
963
+ },
964
+ submit: (op: IDirectoryDeleteOperation, localOpMetadata: unknown) => {
965
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
966
+ if (subdir) {
967
+ subdir.resubmitKeyMessage(op, localOpMetadata);
968
+ }
969
+ },
970
+ applyStashedOp: (
971
+ op: IDirectoryDeleteOperation,
972
+ ): IKeyEditLocalOpMetadata | undefined => {
973
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
974
+ if (subdir) {
975
+ return subdir.applyStashedDeleteMessage(op);
976
+ }
977
+ },
978
+ });
979
+ this.messageHandlers.set("set", {
980
+ process: (
981
+ msg: ISequencedDocumentMessage,
982
+ op: IDirectorySetOperation,
983
+ local,
984
+ localOpMetadata,
985
+ ) => {
986
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
987
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
988
+ // as we are going to delete this subDirectory.
989
+ if (subdir && !this.isSubDirectoryDeletePending(op.path)) {
990
+ const context = local ? undefined : this.makeLocal(op.key, op.path, op.value);
991
+ subdir.processSetMessage(msg, op, context, local, localOpMetadata);
992
+ }
993
+ },
994
+ submit: (op: IDirectorySetOperation, localOpMetadata: unknown) => {
995
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
996
+ if (subdir) {
997
+ subdir.resubmitKeyMessage(op, localOpMetadata);
998
+ }
999
+ },
1000
+ applyStashedOp: (op: IDirectorySetOperation): IKeyEditLocalOpMetadata | undefined => {
1001
+ const subdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1002
+ if (subdir) {
1003
+ const context = this.makeLocal(op.key, op.path, op.value);
1004
+ return subdir.applyStashedSetMessage(op, context);
1005
+ }
1006
+ },
1007
+ });
1008
+
1009
+ this.messageHandlers.set("createSubDirectory", {
1010
+ process: (
1011
+ msg: ISequencedDocumentMessage,
1012
+ op: IDirectoryCreateSubDirectoryOperation,
1013
+ local,
1014
+ localOpMetadata,
1015
+ ) => {
1016
+ const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1017
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
1018
+ // as we are going to delete this subDirectory.
1019
+ if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
1020
+ parentSubdir.processCreateSubDirectoryMessage(msg, op, local, localOpMetadata);
1021
+ }
1022
+ },
1023
+ submit: (op: IDirectoryCreateSubDirectoryOperation, localOpMetadata: unknown) => {
1024
+ const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1025
+ if (parentSubdir) {
1026
+ // We don't reuse the metadata but send a new one on each submit.
1027
+ parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
1028
+ }
1029
+ },
1030
+ applyStashedOp: (
1031
+ op: IDirectoryCreateSubDirectoryOperation,
1032
+ ): ICreateSubDirLocalOpMetadata | undefined => {
1033
+ const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1034
+ if (parentSubdir) {
1035
+ return parentSubdir.applyStashedCreateSubDirMessage(op);
1036
+ }
1037
+ },
1038
+ });
1039
+
1040
+ this.messageHandlers.set("deleteSubDirectory", {
1041
+ process: (
1042
+ msg: ISequencedDocumentMessage,
1043
+ op: IDirectoryDeleteSubDirectoryOperation,
1044
+ local,
1045
+ localOpMetadata,
1046
+ ) => {
1047
+ const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1048
+ // If there is pending delete op for any subDirectory in the op.path, then don't apply the this op
1049
+ // as we are going to delete this subDirectory.
1050
+ if (parentSubdir && !this.isSubDirectoryDeletePending(op.path)) {
1051
+ parentSubdir.processDeleteSubDirectoryMessage(msg, op, local, localOpMetadata);
1052
+ }
1053
+ },
1054
+ submit: (op: IDirectoryDeleteSubDirectoryOperation, localOpMetadata: unknown) => {
1055
+ const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1056
+ if (parentSubdir) {
1057
+ // We don't reuse the metadata but send a new one on each submit.
1058
+ parentSubdir.resubmitSubDirectoryMessage(op, localOpMetadata);
1059
+ }
1060
+ },
1061
+ applyStashedOp: (
1062
+ op: IDirectoryDeleteSubDirectoryOperation,
1063
+ ): IDeleteSubDirLocalOpMetadata | undefined => {
1064
+ const parentSubdir = this.getWorkingDirectory(op.path) as SubDirectory | undefined;
1065
+ if (parentSubdir) {
1066
+ return parentSubdir.applyStashedDeleteSubDirMessage(op);
1067
+ }
1068
+ },
1069
+ });
1070
+ }
1071
+
1072
+ /**
1073
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
1074
+ */
1075
+ protected applyStashedOp(op: unknown): unknown {
1076
+ const handler = this.messageHandlers.get((op as IDirectoryOperation).type);
1077
+ if (handler === undefined) {
1078
+ throw new Error("no apply stashed op handler");
1079
+ }
1080
+ return handler.applyStashedOp(op as IDirectoryOperation);
1081
+ }
1082
+
1083
+ private serializeDirectory(
1084
+ root: SubDirectory,
1085
+ serializer: IFluidSerializer,
1086
+ telemetryContext?: ITelemetryContext,
1087
+ ): ISummaryTreeWithStats {
1088
+ const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
1089
+
1090
+ const builder = new SummaryTreeBuilder();
1091
+ let counter = 0;
1092
+ const blobs: string[] = [];
1093
+
1094
+ const stack: [SubDirectory, IDirectoryDataObject][] = [];
1095
+ const content: IDirectoryDataObject = {};
1096
+ stack.push([root, content]);
1097
+
1098
+ while (stack.length > 0) {
1099
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1100
+ const [currentSubDir, currentSubDirObject] = stack.pop()!;
1101
+ currentSubDirObject.ci = currentSubDir.getSerializableCreateInfo();
1102
+ for (const [key, value] of currentSubDir.getSerializedStorage(serializer)) {
1103
+ if (!currentSubDirObject.storage) {
1104
+ currentSubDirObject.storage = {};
1105
+ }
1106
+ // eslint-disable-next-line import/no-deprecated
1107
+ const result: ISerializableValue = {
1108
+ type: value.type,
1109
+ value: value.value && (JSON.parse(value.value) as object),
1110
+ };
1111
+ if (value.value && value.value.length >= MinValueSizeSeparateSnapshotBlob) {
1112
+ const extraContent: IDirectoryDataObject = {};
1113
+ let largeContent = extraContent;
1114
+ if (currentSubDir.absolutePath !== posix.sep) {
1115
+ for (const dir of currentSubDir.absolutePath.slice(1).split(posix.sep)) {
1116
+ const subDataObject: IDirectoryDataObject = {};
1117
+ largeContent.subdirectories = { [dir]: subDataObject };
1118
+ largeContent = subDataObject;
1119
+ }
1120
+ }
1121
+ largeContent.storage = { [key]: result };
1122
+ const blobName = `blob${counter}`;
1123
+ counter++;
1124
+ blobs.push(blobName);
1125
+ builder.addBlob(blobName, JSON.stringify(extraContent));
1126
+ } else {
1127
+ currentSubDirObject.storage[key] = result;
1128
+ }
1129
+ }
1130
+
1131
+ for (const [subdirName, subdir] of currentSubDir.subdirectories()) {
1132
+ if (!currentSubDirObject.subdirectories) {
1133
+ currentSubDirObject.subdirectories = {};
1134
+ }
1135
+ const subDataObject: IDirectoryDataObject = {};
1136
+ currentSubDirObject.subdirectories[subdirName] = subDataObject;
1137
+ stack.push([subdir as SubDirectory, subDataObject]);
1138
+ }
1139
+ }
1140
+
1141
+ const newFormat: IDirectoryNewStorageFormat = {
1142
+ blobs,
1143
+ content,
1144
+ };
1145
+ builder.addBlob(snapshotFileName, JSON.stringify(newFormat));
1146
+
1147
+ return builder.getSummaryTree();
1148
+ }
843
1149
  }
844
1150
 
845
1151
  interface IKeyEditLocalOpMetadata {
846
- type: "edit";
847
- pendingMessageId: number;
848
- previousValue: ILocalValue | undefined;
1152
+ type: "edit";
1153
+ pendingMessageId: number;
1154
+ previousValue: ILocalValue | undefined;
849
1155
  }
850
1156
 
851
1157
  interface IClearLocalOpMetadata {
852
- type: "clear";
853
- pendingMessageId: number;
854
- previousStorage: Map<string, ILocalValue>;
1158
+ type: "clear";
1159
+ pendingMessageId: number;
1160
+ previousStorage: Map<string, ILocalValue>;
855
1161
  }
856
1162
 
857
1163
  interface ICreateSubDirLocalOpMetadata {
858
- type: "createSubDir";
859
- pendingMessageId: number;
860
- previouslyExisted: boolean;
1164
+ type: "createSubDir";
861
1165
  }
862
1166
 
863
1167
  interface IDeleteSubDirLocalOpMetadata {
864
- type: "deleteSubDir";
865
- pendingMessageId: number;
866
- subDirectory: SubDirectory | undefined;
1168
+ type: "deleteSubDir";
1169
+ subDirectory: SubDirectory | undefined;
867
1170
  }
868
1171
 
869
1172
  type SubDirLocalOpMetadata = ICreateSubDirLocalOpMetadata | IDeleteSubDirLocalOpMetadata;
870
- type DirectoryLocalOpMetadata = IClearLocalOpMetadata | IKeyEditLocalOpMetadata | SubDirLocalOpMetadata;
1173
+ export type DirectoryLocalOpMetadata =
1174
+ | IClearLocalOpMetadata
1175
+ | IKeyEditLocalOpMetadata
1176
+ | SubDirLocalOpMetadata;
1177
+
1178
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
871
1179
 
872
1180
  function isKeyEditLocalOpMetadata(metadata: any): metadata is IKeyEditLocalOpMetadata {
873
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" && metadata.type === "edit";
1181
+ return (
1182
+ metadata !== undefined &&
1183
+ typeof metadata.pendingMessageId === "number" &&
1184
+ metadata.type === "edit"
1185
+ );
874
1186
  }
875
1187
 
876
1188
  function isClearLocalOpMetadata(metadata: any): metadata is IClearLocalOpMetadata {
877
- return metadata !== undefined && metadata.type === "clear" && typeof metadata.pendingMessageId === "number" &&
878
- typeof metadata.previousStorage === "object";
1189
+ return (
1190
+ metadata !== undefined &&
1191
+ metadata.type === "clear" &&
1192
+ typeof metadata.pendingMessageId === "number" &&
1193
+ typeof metadata.previousStorage === "object"
1194
+ );
879
1195
  }
880
1196
 
881
1197
  function isSubDirLocalOpMetadata(metadata: any): metadata is SubDirLocalOpMetadata {
882
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
883
- ((metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean") ||
884
- metadata.type === "deleteSubDir");
1198
+ return (
1199
+ metadata !== undefined &&
1200
+ (metadata.type === "createSubDir" || metadata.type === "deleteSubDir")
1201
+ );
885
1202
  }
886
1203
 
887
1204
  function isDirectoryLocalOpMetadata(metadata: any): metadata is DirectoryLocalOpMetadata {
888
- return metadata !== undefined && typeof metadata.pendingMessageId === "number" &&
889
- (metadata.type === "edit" || metadata.type === "deleteSubDir" ||
890
- (metadata.type === "clear" && typeof metadata.previousStorage === "object") ||
891
- (metadata.type === "createSubDir" && typeof metadata.previouslyExisted === "boolean"));
1205
+ return (
1206
+ isKeyEditLocalOpMetadata(metadata) ||
1207
+ isClearLocalOpMetadata(metadata) ||
1208
+ isSubDirLocalOpMetadata(metadata)
1209
+ );
1210
+ }
1211
+
1212
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
1213
+
1214
+ function assertNonNullClientId(clientId: string | null): asserts clientId is string {
1215
+ assert(clientId !== null, 0x6af /* client id should never be null */);
892
1216
  }
893
1217
 
894
1218
  /**
@@ -896,962 +1220,1455 @@ function isDirectoryLocalOpMetadata(metadata: any): metadata is DirectoryLocalOp
896
1220
  * @sealed
897
1221
  */
898
1222
  class SubDirectory extends TypedEventEmitter<IDirectoryEvents> implements IDirectory {
899
- /**
900
- * Tells if the sub directory is deleted or not.
901
- */
902
- private _deleted = false;
903
-
904
- /**
905
- * String representation for the class.
906
- */
907
- public [Symbol.toStringTag]: string = "SubDirectory";
908
-
909
- /**
910
- * The in-memory data the directory is storing.
911
- */
912
- private readonly _storage: Map<string, ILocalValue> = new Map();
913
-
914
- /**
915
- * The subdirectories the directory is holding.
916
- */
917
- private readonly _subdirectories: Map<string, SubDirectory> = new Map();
918
-
919
- /**
920
- * Keys that have been modified locally but not yet ack'd from the server.
921
- */
922
- private readonly pendingKeys: Map<string, number[]> = new Map();
923
-
924
- /**
925
- * Subdirectories that have been modified locally but not yet ack'd from the server.
926
- */
927
- private readonly pendingSubDirectories: Map<string, number[]> = new Map();
928
-
929
- /**
930
- * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
931
- */
932
- private pendingMessageId: number = -1;
933
-
934
- /**
935
- * The pending ids of any clears that have been performed locally but not yet ack'd from the server
936
- */
937
- private readonly pendingClearMessageIds: number[] = [];
938
-
939
- /**
940
- * Constructor.
941
- * @param directory - Reference back to the SharedDirectory to perform operations
942
- * @param runtime - The data store runtime this directory is associated with
943
- * @param serializer - The serializer to serialize / parse handles
944
- * @param absolutePath - The absolute path of this IDirectory
945
- */
946
- constructor(
947
- private readonly directory: SharedDirectory,
948
- private readonly runtime: IFluidDataStoreRuntime,
949
- private readonly serializer: IFluidSerializer,
950
- public readonly absolutePath: string,
951
- ) {
952
- super();
953
- }
954
-
955
- public dispose(error?: Error): void {
956
- this._deleted = true;
957
- this.emit("disposed", this);
958
- }
959
-
960
- /**
961
- * Unmark the deleted property when rolling back delete.
962
- */
963
- private undispose(): void {
964
- this._deleted = false;
965
- }
966
-
967
- public get disposed(): boolean {
968
- return this._deleted;
969
- }
970
-
971
- private throwIfDisposed() {
972
- if (this._deleted) {
973
- throw new UsageError("Cannot access Disposed subDirectory");
974
- }
975
- }
976
-
977
- /**
978
- * Checks whether the given key exists in this IDirectory.
979
- * @param key - The key to check
980
- * @returns True if the key exists, false otherwise
981
- */
982
- public has(key: string): boolean {
983
- this.throwIfDisposed();
984
- return this._storage.has(key);
985
- }
986
-
987
- /**
988
- * {@inheritDoc IDirectory.get}
989
- */
990
- public get<T = any>(key: string): T | undefined {
991
- this.throwIfDisposed();
992
- return this._storage.get(key)?.value as T | undefined;
993
- }
994
-
995
- /**
996
- * {@inheritDoc IDirectory.set}
997
- */
998
- public set<T = any>(key: string, value: T): this {
999
- this.throwIfDisposed();
1000
- // Undefined/null keys can't be serialized to JSON in the manner we currently snapshot.
1001
- if (key === undefined || key === null) {
1002
- throw new Error("Undefined and null keys are not supported");
1003
- }
1004
-
1005
- // Create a local value and serialize it.
1006
- const localValue = this.directory.localValueMaker.fromInMemory(value);
1007
- const serializableValue = makeSerializable(
1008
- localValue,
1009
- this.serializer,
1010
- this.directory.handle);
1011
-
1012
- // Set the value locally.
1013
- const previousValue = this.setCore(
1014
- key,
1015
- localValue,
1016
- true,
1017
- );
1018
-
1019
- // If we are not attached, don't submit the op.
1020
- if (!this.directory.isAttached()) {
1021
- return this;
1022
- }
1023
-
1024
- const op: IDirectorySetOperation = {
1025
- key,
1026
- path: this.absolutePath,
1027
- type: "set",
1028
- value: serializableValue,
1029
- };
1030
- this.submitKeyMessage(op, previousValue);
1031
- return this;
1032
- }
1033
-
1034
- /**
1035
- * {@inheritDoc IDirectory.countSubDirectory}
1036
- */
1037
- public countSubDirectory(): number {
1038
- return this._subdirectories.size;
1039
- }
1040
-
1041
- /**
1042
- * {@inheritDoc IDirectory.createSubDirectory}
1043
- */
1044
- public createSubDirectory(subdirName: string): IDirectory {
1045
- this.throwIfDisposed();
1046
- // Undefined/null subdirectory names can't be serialized to JSON in the manner we currently snapshot.
1047
- if (subdirName === undefined || subdirName === null) {
1048
- throw new Error("SubDirectory name may not be undefined or null");
1049
- }
1050
-
1051
- if (subdirName.includes(posix.sep)) {
1052
- throw new Error(`SubDirectory name may not contain ${posix.sep}`);
1053
- }
1054
-
1055
- // Create the sub directory locally first.
1056
- const isNew = this.createSubDirectoryCore(subdirName, true);
1057
-
1058
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1059
- const subDir: IDirectory = this._subdirectories.get(subdirName)!;
1060
-
1061
- // If we are not attached, don't submit the op.
1062
- if (!this.directory.isAttached()) {
1063
- return subDir;
1064
- }
1065
-
1066
- const op: IDirectoryCreateSubDirectoryOperation = {
1067
- path: this.absolutePath,
1068
- subdirName,
1069
- type: "createSubDirectory",
1070
- };
1071
- this.submitCreateSubDirectoryMessage(op, !isNew);
1072
-
1073
- return subDir;
1074
- }
1075
-
1076
- /**
1077
- * {@inheritDoc IDirectory.getSubDirectory}
1078
- */
1079
- public getSubDirectory(subdirName: string): IDirectory | undefined {
1080
- this.throwIfDisposed();
1081
- return this._subdirectories.get(subdirName);
1082
- }
1083
-
1084
- /**
1085
- * {@inheritDoc IDirectory.hasSubDirectory}
1086
- */
1087
- public hasSubDirectory(subdirName: string): boolean {
1088
- this.throwIfDisposed();
1089
- return this._subdirectories.has(subdirName);
1090
- }
1091
-
1092
- /**
1093
- * {@inheritDoc IDirectory.deleteSubDirectory}
1094
- */
1095
- public deleteSubDirectory(subdirName: string): boolean {
1096
- this.throwIfDisposed();
1097
- // Delete the sub directory locally first.
1098
- const subDir = this.deleteSubDirectoryCore(subdirName, true);
1099
-
1100
- // If we are not attached, don't submit the op.
1101
- if (!this.directory.isAttached()) {
1102
- return subDir !== undefined;
1103
- }
1104
-
1105
- const op: IDirectoryDeleteSubDirectoryOperation = {
1106
- path: this.absolutePath,
1107
- subdirName,
1108
- type: "deleteSubDirectory",
1109
- };
1110
-
1111
- this.submitDeleteSubDirectoryMessage(op, subDir);
1112
- return subDir !== undefined;
1113
- }
1114
-
1115
- /**
1116
- * {@inheritDoc IDirectory.subdirectories}
1117
- */
1118
- public subdirectories(): IterableIterator<[string, IDirectory]> {
1119
- this.throwIfDisposed();
1120
- return this._subdirectories.entries();
1121
- }
1122
-
1123
- /**
1124
- * {@inheritDoc IDirectory.getWorkingDirectory}
1125
- */
1126
- public getWorkingDirectory(relativePath: string): IDirectory | undefined {
1127
- this.throwIfDisposed();
1128
- return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
1129
- }
1130
-
1131
- /**
1132
- * Deletes the given key from within this IDirectory.
1133
- * @param key - The key to delete
1134
- * @returns True if the key existed and was deleted, false if it did not exist
1135
- */
1136
- public delete(key: string): boolean {
1137
- this.throwIfDisposed();
1138
- // Delete the key locally first.
1139
- const previousValue = this.deleteCore(key, true);
1140
-
1141
- // If we are not attached, don't submit the op.
1142
- if (!this.directory.isAttached()) {
1143
- return previousValue !== undefined;
1144
- }
1145
-
1146
- const op: IDirectoryDeleteOperation = {
1147
- key,
1148
- path: this.absolutePath,
1149
- type: "delete",
1150
- };
1151
-
1152
- this.submitKeyMessage(op, previousValue);
1153
- return previousValue !== undefined;
1154
- }
1155
-
1156
- /**
1157
- * Deletes all keys from within this IDirectory.
1158
- */
1159
- public clear(): void {
1160
- this.throwIfDisposed();
1161
-
1162
- // If we are not attached, don't submit the op.
1163
- if (!this.directory.isAttached()) {
1164
- this.clearCore(true);
1165
- return;
1166
- }
1167
-
1168
- const copy = new Map<string, ILocalValue>(this._storage);
1169
- this.clearCore(true);
1170
- const op: IDirectoryClearOperation = {
1171
- path: this.absolutePath,
1172
- type: "clear",
1173
- };
1174
- this.submitClearMessage(op, copy);
1175
- }
1176
-
1177
- /**
1178
- * Issue a callback on each entry under this IDirectory.
1179
- * @param callback - Callback to issue
1180
- */
1181
- public forEach(callback: (value: any, key: string, map: Map<string, any>) => void): void {
1182
- this.throwIfDisposed();
1183
- this._storage.forEach((localValue, key, map) => {
1184
- callback(localValue.value, key, map);
1185
- });
1186
- }
1187
-
1188
- /**
1189
- * The number of entries under this IDirectory.
1190
- */
1191
- public get size(): number {
1192
- this.throwIfDisposed();
1193
- return this._storage.size;
1194
- }
1195
-
1196
- /**
1197
- * Get an iterator over the entries under this IDirectory.
1198
- * @returns The iterator
1199
- */
1200
- public entries(): IterableIterator<[string, any]> {
1201
- this.throwIfDisposed();
1202
- const localEntriesIterator = this._storage.entries();
1203
- const iterator = {
1204
- next(): IteratorResult<[string, any]> {
1205
- const nextVal = localEntriesIterator.next();
1206
- if (nextVal.done) {
1207
- return { value: undefined, done: true };
1208
- } else {
1209
- // Unpack the stored value
1210
- return { value: [nextVal.value[0], nextVal.value[1].value], done: false };
1211
- }
1212
- },
1213
- [Symbol.iterator]() {
1214
- return this;
1215
- },
1216
- };
1217
- return iterator;
1218
- }
1219
-
1220
- /**
1221
- * Get an iterator over the keys under this IDirectory.
1222
- * @returns The iterator
1223
- */
1224
- public keys(): IterableIterator<string> {
1225
- this.throwIfDisposed();
1226
- return this._storage.keys();
1227
- }
1228
-
1229
- /**
1230
- * Get an iterator over the values under this IDirectory.
1231
- * @returns The iterator
1232
- */
1233
- public values(): IterableIterator<any> {
1234
- this.throwIfDisposed();
1235
- const localValuesIterator = this._storage.values();
1236
- const iterator = {
1237
- next(): IteratorResult<any> {
1238
- const nextVal = localValuesIterator.next();
1239
- if (nextVal.done) {
1240
- return { value: undefined, done: true };
1241
- } else {
1242
- // Unpack the stored value
1243
- return { value: nextVal.value.value, done: false };
1244
- }
1245
- },
1246
- [Symbol.iterator]() {
1247
- return this;
1248
- },
1249
- };
1250
- return iterator;
1251
- }
1252
-
1253
- /**
1254
- * Get an iterator over the entries under this IDirectory.
1255
- * @returns The iterator
1256
- */
1257
- public [Symbol.iterator](): IterableIterator<[string, any]> {
1258
- this.throwIfDisposed();
1259
- return this.entries();
1260
- }
1261
-
1262
- /**
1263
- * Process a clear operation.
1264
- * @param op - The op to process
1265
- * @param local - Whether the message originated from the local client
1266
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1267
- * For messages from a remote client, this will be undefined.
1268
- * @internal
1269
- */
1270
- public processClearMessage(
1271
- op: IDirectoryClearOperation,
1272
- local: boolean,
1273
- localOpMetadata: unknown,
1274
- ): void {
1275
- this.throwIfDisposed();
1276
- if (local) {
1277
- assert(isClearLocalOpMetadata(localOpMetadata),
1278
- 0x00f /* pendingMessageId is missing from the local client's operation */);
1279
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
1280
- assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
1281
- 0x32a /* pendingMessageId does not match */);
1282
- return;
1283
- }
1284
- this.clearExceptPendingKeys();
1285
- }
1286
-
1287
- /**
1288
- * Process a delete operation.
1289
- * @param op - The op to process
1290
- * @param local - Whether the message originated from the local client
1291
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1292
- * For messages from a remote client, this will be undefined.
1293
- * @internal
1294
- */
1295
- public processDeleteMessage(
1296
- op: IDirectoryDeleteOperation,
1297
- local: boolean,
1298
- localOpMetadata: unknown,
1299
- ): void {
1300
- this.throwIfDisposed();
1301
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1302
- return;
1303
- }
1304
- this.deleteCore(op.key, local);
1305
- }
1306
-
1307
- /**
1308
- * Process a set operation.
1309
- * @param op - The op to process
1310
- * @param local - Whether the message originated from the local client
1311
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1312
- * For messages from a remote client, this will be undefined.
1313
- * @internal
1314
- */
1315
- public processSetMessage(
1316
- op: IDirectorySetOperation,
1317
- context: ILocalValue | undefined,
1318
- local: boolean,
1319
- localOpMetadata: unknown,
1320
- ): void {
1321
- this.throwIfDisposed();
1322
- if (!this.needProcessStorageOperation(op, local, localOpMetadata)) {
1323
- return;
1324
- }
1325
-
1326
- // needProcessStorageOperation should have returned false if local is true
1327
- // so we can assume context is not undefined
1328
-
1329
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1330
- this.setCore(op.key, context!, local);
1331
- }
1332
-
1333
- /**
1334
- * Process a create subdirectory operation.
1335
- * @param op - The op to process
1336
- * @param local - Whether the message originated from the local client
1337
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1338
- * For messages from a remote client, this will be undefined.
1339
- * @internal
1340
- */
1341
- public processCreateSubDirectoryMessage(
1342
- op: IDirectoryCreateSubDirectoryOperation,
1343
- local: boolean,
1344
- localOpMetadata: unknown,
1345
- ): void {
1346
- this.throwIfDisposed();
1347
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1348
- return;
1349
- }
1350
- this.createSubDirectoryCore(op.subdirName, local);
1351
- }
1352
-
1353
- /**
1354
- * Process a delete subdirectory operation.
1355
- * @param op - The op to process
1356
- * @param local - Whether the message originated from the local client
1357
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1358
- * For messages from a remote client, this will be undefined.
1359
- * @internal
1360
- */
1361
- public processDeleteSubDirectoryMessage(
1362
- op: IDirectoryDeleteSubDirectoryOperation,
1363
- local: boolean,
1364
- localOpMetadata: unknown,
1365
- ): void {
1366
- this.throwIfDisposed();
1367
- if (!this.needProcessSubDirectoryOperation(op, local, localOpMetadata)) {
1368
- return;
1369
- }
1370
- this.deleteSubDirectoryCore(op.subdirName, local);
1371
- }
1372
-
1373
- /**
1374
- * Submit a clear operation.
1375
- * @param op - The operation
1376
- */
1377
- private submitClearMessage(op: IDirectoryClearOperation,
1378
- previousValue: Map<string, ILocalValue>): void {
1379
- this.throwIfDisposed();
1380
- const pendingMsgId = ++this.pendingMessageId;
1381
- this.pendingClearMessageIds.push(pendingMsgId);
1382
- const metadata: IClearLocalOpMetadata = {
1383
- type: "clear",
1384
- pendingMessageId: pendingMsgId,
1385
- previousStorage: previousValue,
1386
- };
1387
- this.directory.submitDirectoryMessage(op, metadata);
1388
- }
1389
-
1390
- /**
1391
- * Resubmit a clear operation.
1392
- * @param op - The operation
1393
- * @internal
1394
- */
1395
- public resubmitClearMessage(op: IDirectoryClearOperation, localOpMetadata: unknown): void {
1396
- assert(isClearLocalOpMetadata(localOpMetadata), 0x32b /* Invalid localOpMetadata for clear */);
1397
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1398
- const pendingClearMessageId = this.pendingClearMessageIds.shift();
1399
- assert(pendingClearMessageId === localOpMetadata.pendingMessageId,
1400
- 0x32c /* pendingMessageId does not match */);
1401
- this.submitClearMessage(op, localOpMetadata.previousStorage);
1402
- }
1403
-
1404
- /**
1405
- * Get a new pending message id for the op and cache it to track the pending op
1406
- */
1407
- private getKeyMessageId(op: IDirectoryKeyOperation): number {
1408
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1409
- const pendingMessageId = ++this.pendingMessageId;
1410
- const pendingMessageIds = this.pendingKeys.get(op.key);
1411
- if (pendingMessageIds !== undefined) {
1412
- pendingMessageIds.push(pendingMessageId);
1413
- } else {
1414
- this.pendingKeys.set(op.key, [pendingMessageId]);
1415
- }
1416
- return pendingMessageId;
1417
- }
1418
-
1419
- /**
1420
- * Submit a key operation.
1421
- * @param op - The operation
1422
- * @param previousValue - The value of the key before this op
1423
- */
1424
- private submitKeyMessage(op: IDirectoryKeyOperation, previousValue?: ILocalValue): void {
1425
- this.throwIfDisposed();
1426
- const pendingMessageId = this.getKeyMessageId(op);
1427
- const localMetadata = { type: "edit", pendingMessageId, previousValue };
1428
- this.directory.submitDirectoryMessage(op, localMetadata);
1429
- }
1430
-
1431
- /**
1432
- * Submit a key message to remote clients based on a previous submit.
1433
- * @param op - The map key message
1434
- * @param localOpMetadata - Metadata from the previous submit
1435
- * @internal
1436
- */
1437
- public resubmitKeyMessage(op: IDirectoryKeyOperation, localOpMetadata: unknown): void {
1438
- assert(isKeyEditLocalOpMetadata(localOpMetadata), 0x32d /* Invalid localOpMetadata in submit */);
1439
-
1440
- // clear the old pending message id
1441
- const pendingMessageIds = this.pendingKeys.get(op.key);
1442
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
1443
- 0x32e /* Unexpected pending message received */);
1444
- pendingMessageIds.shift();
1445
- if (pendingMessageIds.length === 0) {
1446
- this.pendingKeys.delete(op.key);
1447
- }
1448
-
1449
- this.submitKeyMessage(op, localOpMetadata.previousValue);
1450
- }
1451
-
1452
- /**
1453
- * Get a new pending message id for the op and cache it to track the pending op
1454
- */
1455
- private getSubDirMessageId(op: IDirectorySubDirectoryOperation): number {
1456
- // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1457
- const newMessageId = ++this.pendingMessageId;
1458
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1459
- if (pendingMessageIds !== undefined) {
1460
- pendingMessageIds.push(newMessageId);
1461
- } else {
1462
- this.pendingSubDirectories.set(op.subdirName, [newMessageId]);
1463
- }
1464
- return newMessageId;
1465
- }
1466
-
1467
- /**
1468
- * Submit a create subdirectory operation.
1469
- * @param op - The operation
1470
- * @param prevExisted - Whether the subdirectory existed before the op
1471
- */
1472
- private submitCreateSubDirectoryMessage(op: IDirectorySubDirectoryOperation,
1473
- prevExisted: boolean): void {
1474
- this.throwIfDisposed();
1475
- const newMessageId = this.getSubDirMessageId(op);
1476
-
1477
- const localOpMetadata: ICreateSubDirLocalOpMetadata = {
1478
- type: "createSubDir",
1479
- pendingMessageId: newMessageId,
1480
- previouslyExisted: prevExisted,
1481
- };
1482
- this.directory.submitDirectoryMessage(op, localOpMetadata);
1483
- }
1484
-
1485
- /**
1486
- * Submit a delete subdirectory operation.
1487
- * @param op - The operation
1488
- * @param subDir - Any subdirectory deleted by the op
1489
- */
1490
- private submitDeleteSubDirectoryMessage(op: IDirectorySubDirectoryOperation,
1491
- subDir: SubDirectory | undefined): void {
1492
- this.throwIfDisposed();
1493
- const newMessageId = this.getSubDirMessageId(op);
1494
-
1495
- const localOpMetadata: IDeleteSubDirLocalOpMetadata = {
1496
- type: "deleteSubDir",
1497
- pendingMessageId: newMessageId,
1498
- subDirectory: subDir,
1499
- };
1500
- this.directory.submitDirectoryMessage(op, localOpMetadata);
1501
- }
1502
-
1503
- /**
1504
- * Submit a subdirectory operation again
1505
- * @param op - The operation
1506
- * @param localOpMetadata - metadata submitted with the op originally
1507
- * @internal
1508
- */
1509
- public resubmitSubDirectoryMessage(op: IDirectorySubDirectoryOperation, localOpMetadata: unknown): void {
1510
- assert(isSubDirLocalOpMetadata(localOpMetadata), 0x32f /* Invalid localOpMetadata for sub directory op */);
1511
-
1512
- // clear the old pending message id
1513
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1514
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
1515
- 0x330 /* Unexpected pending message received */);
1516
- pendingMessageIds.shift();
1517
- if (pendingMessageIds.length === 0) {
1518
- this.pendingSubDirectories.delete(op.subdirName);
1519
- }
1520
-
1521
- if (localOpMetadata.type === "createSubDir") {
1522
- this.submitCreateSubDirectoryMessage(op, localOpMetadata.previouslyExisted);
1523
- } else {
1524
- this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
1525
- }
1526
- }
1527
-
1528
- /**
1529
- * Get the storage of this subdirectory in a serializable format, to be used in snapshotting.
1530
- * @param serializer - The serializer to use to serialize handles in its values.
1531
- * @returns The JSONable string representing the storage of this subdirectory
1532
- * @internal
1533
- */
1534
- public *getSerializedStorage(serializer: IFluidSerializer) {
1535
- this.throwIfDisposed();
1536
- for (const [key, localValue] of this._storage) {
1537
- const value = localValue.makeSerialized(serializer, this.directory.handle);
1538
- const res: [string, ISerializedValue] = [key, value];
1539
- yield res;
1540
- }
1541
- }
1542
-
1543
- /**
1544
- * Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
1545
- * @param key - The key to populate
1546
- * @param localValue - The local value to populate into it
1547
- * @internal
1548
- */
1549
- public populateStorage(key: string, localValue: ILocalValue): void {
1550
- this.throwIfDisposed();
1551
- this._storage.set(key, localValue);
1552
- }
1553
-
1554
- /**
1555
- * Populate a subdirectory into this subdirectory, to be used when loading from snapshot.
1556
- * @param subdirName - The name of the subdirectory to add
1557
- * @param newSubDir - The new subdirectory to add
1558
- * @internal
1559
- */
1560
- public populateSubDirectory(subdirName: string, newSubDir: SubDirectory): void {
1561
- this.throwIfDisposed();
1562
- this._subdirectories.set(subdirName, newSubDir);
1563
- }
1564
-
1565
- /**
1566
- * Retrieve the local value at the given key. This is used to get value type information stashed on the local
1567
- * value so op handlers can be retrieved
1568
- * @param key - The key to retrieve from
1569
- * @returns The local value
1570
- * @internal
1571
- */
1572
- public getLocalValue<T extends ILocalValue = ILocalValue>(key: string): T {
1573
- this.throwIfDisposed();
1574
- return this._storage.get(key) as T;
1575
- }
1576
-
1577
- /**
1578
- * Remove the pendingMessageId from the map tracking it on rollback
1579
- * @param map - map tracking the pending messages
1580
- * @param key - key of the edit in the op
1581
- */
1582
- private rollbackPendingMessageId(map: Map<string, number[]>, key: string, pendingMessageId) {
1583
- const pendingMessageIds = map.get(key);
1584
- const lastPendingMessageId = pendingMessageIds?.pop();
1585
- if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
1586
- throw new Error("Rollback op does not match last pending");
1587
- }
1588
- if (pendingMessageIds.length === 0) {
1589
- map.delete(key);
1590
- }
1591
- }
1592
-
1593
- /**
1594
- * Rollback a local op
1595
- * @param op - The operation to rollback
1596
- * @param localOpMetadata - The local metadata associated with the op.
1597
- */
1598
- public rollback(op: any, localOpMetadata: unknown) {
1599
- if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
1600
- throw new Error("Invalid localOpMetadata");
1601
- }
1602
-
1603
- if (op.type === "clear" && localOpMetadata.type === "clear") {
1604
- localOpMetadata.previousStorage.forEach((localValue, key) => {
1605
- this.setCore(key, localValue, true);
1606
- });
1607
-
1608
- const lastPendingClearId = this.pendingClearMessageIds.pop();
1609
- if (lastPendingClearId === undefined || lastPendingClearId !== localOpMetadata.pendingMessageId) {
1610
- throw new Error("Rollback op does match last clear");
1611
- }
1612
- } else if ((op.type === "delete" || op.type === "set") && localOpMetadata.type === "edit") {
1613
- if (localOpMetadata.previousValue === undefined) {
1614
- this.deleteCore(op.key, true);
1615
- } else {
1616
- this.setCore(op.key, localOpMetadata.previousValue, true);
1617
- }
1618
-
1619
- this.rollbackPendingMessageId(this.pendingKeys, op.key, localOpMetadata.pendingMessageId);
1620
- } else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
1621
- if (!localOpMetadata.previouslyExisted) {
1622
- this.deleteSubDirectoryCore(op.subdirName, true);
1623
- }
1624
-
1625
- this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1626
- } else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
1627
- if (localOpMetadata.subDirectory !== undefined) {
1628
- this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
1629
- // don't need to register events because deleting never unregistered
1630
- this._subdirectories.set(op.subdirName, localOpMetadata.subDirectory);
1631
- this.emit("subDirectoryCreated", op.subdirName, true, this);
1632
- }
1633
-
1634
- this.rollbackPendingMessageId(this.pendingSubDirectories, op.subdirName, localOpMetadata.pendingMessageId);
1635
- } else {
1636
- throw new Error("Unsupported op for rollback");
1637
- }
1638
- }
1639
-
1640
- /**
1641
- * Converts the given relative path into an absolute path.
1642
- * @param path - Relative path to convert
1643
- * @returns The equivalent absolute path
1644
- */
1645
- private makeAbsolute(relativePath: string): string {
1646
- return posix.resolve(this.absolutePath, relativePath);
1647
- }
1648
-
1649
- /**
1650
- * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1651
- * not process the incoming operation.
1652
- * @param op - Operation to check
1653
- * @param local - Whether the operation originated from the local client
1654
- * @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
1655
- * For ops from a remote client, this will be undefined.
1656
- * @returns True if the operation should be processed, false otherwise
1657
- */
1658
- private needProcessStorageOperation(
1659
- op: IDirectoryKeyOperation,
1660
- local: boolean,
1661
- localOpMetadata: unknown,
1662
- ): boolean {
1663
- if (this.pendingClearMessageIds.length > 0) {
1664
- if (local) {
1665
- assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata) &&
1666
- localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
1667
- 0x010 /* "Received out of order storage op when there is an unackd clear message" */);
1668
- }
1669
- // If I have a NACK clear, we can ignore all ops.
1670
- return false;
1671
- }
1672
-
1673
- const pendingKeyMessageId = this.pendingKeys.get(op.key);
1674
- if (pendingKeyMessageId !== undefined) {
1675
- // Found an NACK op, clear it from the directory if the latest sequence number in the directory
1676
- // match the message's and don't process the op.
1677
- if (local) {
1678
- assert(localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata),
1679
- 0x011 /* pendingMessageId is missing from the local client's operation */);
1680
- const pendingMessageIds = this.pendingKeys.get(op.key);
1681
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
1682
- 0x331 /* Unexpected pending message received */);
1683
- pendingMessageIds.shift();
1684
- if (pendingMessageIds.length === 0) {
1685
- this.pendingKeys.delete(op.key);
1686
- }
1687
- }
1688
- return false;
1689
- }
1690
-
1691
- // If we don't have a NACK op on the key, we need to process the remote ops.
1692
- return !local;
1693
- }
1694
-
1695
- /**
1696
- * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
1697
- * not process the incoming operation.
1698
- * @param op - Operation to check
1699
- * @param local - Whether the message originated from the local client
1700
- * @param message - The message
1701
- * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1702
- * For messages from a remote client, this will be undefined.
1703
- * @returns True if the operation should be processed, false otherwise
1704
- */
1705
- private needProcessSubDirectoryOperation(
1706
- op: IDirectorySubDirectoryOperation,
1707
- local: boolean,
1708
- localOpMetadata: unknown,
1709
- ): boolean {
1710
- const pendingSubDirectoryMessageId = this.pendingSubDirectories.get(op.subdirName);
1711
- if (pendingSubDirectoryMessageId !== undefined) {
1712
- if (local) {
1713
- assert(isSubDirLocalOpMetadata(localOpMetadata),
1714
- 0x012 /* pendingMessageId is missing from the local client's operation */);
1715
- const pendingMessageIds = this.pendingSubDirectories.get(op.subdirName);
1716
- assert(pendingMessageIds !== undefined && pendingMessageIds[0] === localOpMetadata.pendingMessageId,
1717
- 0x332 /* Unexpected pending message received */);
1718
- pendingMessageIds.shift();
1719
- if (pendingMessageIds.length === 0) {
1720
- this.pendingSubDirectories.delete(op.subdirName);
1721
- }
1722
- }
1723
- return false;
1724
- }
1725
-
1726
- return !local;
1727
- }
1728
-
1729
- /**
1730
- * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
1731
- */
1732
- private clearExceptPendingKeys() {
1733
- // Assuming the pendingKeys is small and the map is large
1734
- // we will get the value for the pendingKeys and clear the map
1735
- const temp = new Map<string, ILocalValue>();
1736
- this.pendingKeys.forEach((value, key, map) => {
1737
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1738
- temp.set(key, this._storage.get(key)!);
1739
- });
1740
- this.clearCore(false);
1741
- temp.forEach((value, key, map) => {
1742
- this.setCore(key, value, true);
1743
- });
1744
- }
1745
-
1746
- /**
1747
- * Clear implementation used for both locally sourced clears as well as incoming remote clears.
1748
- * @param local - Whether the message originated from the local client
1749
- */
1750
- private clearCore(local: boolean) {
1751
- this._storage.clear();
1752
- this.directory.emit("clear", local, this.directory);
1753
- }
1754
-
1755
- /**
1756
- * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
1757
- * @param key - The key being deleted
1758
- * @param local - Whether the message originated from the local client
1759
- * @returns Previous local value of the key if it existed, undefined if it did not exist
1760
- */
1761
- private deleteCore(key: string, local: boolean): ILocalValue | undefined {
1762
- const previousLocalValue = this._storage.get(key);
1763
- const previousValue = previousLocalValue?.value;
1764
- const successfullyRemoved = this._storage.delete(key);
1765
- if (successfullyRemoved) {
1766
- const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
1767
- this.directory.emit("valueChanged", event, local, this.directory);
1768
- const containedEvent: IValueChanged = { key, previousValue };
1769
- this.emit("containedValueChanged", containedEvent, local, this);
1770
- }
1771
- return previousLocalValue;
1772
- }
1773
-
1774
- /**
1775
- * Set implementation used for both locally sourced sets as well as incoming remote sets.
1776
- * @param key - The key being set
1777
- * @param value - The value being set
1778
- * @param local - Whether the message originated from the local client
1779
- * @returns Previous local value of the key, if any
1780
- */
1781
- private setCore(key: string, value: ILocalValue, local: boolean): ILocalValue | undefined {
1782
- const previousLocalValue = this._storage.get(key);
1783
- const previousValue = previousLocalValue?.value;
1784
- this._storage.set(key, value);
1785
- const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
1786
- this.directory.emit("valueChanged", event, local, this.directory);
1787
- const containedEvent: IValueChanged = { key, previousValue };
1788
- this.emit("containedValueChanged", containedEvent, local, this);
1789
- return previousLocalValue;
1790
- }
1791
-
1792
- /**
1793
- * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1794
- * @param subdirName - The name of the subdirectory being created
1795
- * @param local - Whether the message originated from the local client
1796
- * @returns - True if is newly created, false if it already existed.
1797
- */
1798
- private createSubDirectoryCore(subdirName: string, local: boolean): boolean {
1799
- if (!this._subdirectories.has(subdirName)) {
1800
- const absolutePath = posix.join(this.absolutePath, subdirName);
1801
- const subDir = new SubDirectory(this.directory, this.runtime, this.serializer, absolutePath);
1802
- this.registerEventsOnSubDirectory(subDir, subdirName);
1803
- this._subdirectories.set(subdirName, subDir);
1804
- this.emit("subDirectoryCreated", subdirName, local, this);
1805
- return true;
1806
- }
1807
- return false;
1808
- }
1809
-
1810
- private registerEventsOnSubDirectory(subDirectory: SubDirectory, subDirName: string) {
1811
- subDirectory.on("subDirectoryCreated", (relativePath: string, local: boolean) => {
1812
- this.emit("subDirectoryCreated", posix.join(subDirName, relativePath), local, this);
1813
- });
1814
- subDirectory.on("subDirectoryDeleted", (relativePath: string, local: boolean) => {
1815
- this.emit("subDirectoryDeleted", posix.join(subDirName, relativePath), local, this);
1816
- });
1817
- }
1818
-
1819
- /**
1820
- * Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
1821
- * @param subdirName - The name of the subdirectory being deleted
1822
- * @param local - Whether the message originated from the local client
1823
- */
1824
- private deleteSubDirectoryCore(subdirName: string, local: boolean) {
1825
- const previousValue = this._subdirectories.get(subdirName);
1826
- // This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
1827
- // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
1828
- if (previousValue !== undefined) {
1829
- this._subdirectories.delete(subdirName);
1830
- this.disposeSubDirectoryTree(previousValue);
1831
- this.emit("subDirectoryDeleted", subdirName, local, this);
1832
- }
1833
- return previousValue;
1834
- }
1835
-
1836
- private disposeSubDirectoryTree(directory: IDirectory | undefined) {
1837
- if (!directory) {
1838
- return;
1839
- }
1840
- // Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
1841
- const subDirectories = directory.subdirectories();
1842
- for (const [_, subDirectory] of subDirectories) {
1843
- this.disposeSubDirectoryTree(subDirectory);
1844
- }
1845
- if (typeof directory.dispose === "function") {
1846
- directory.dispose();
1847
- }
1848
- }
1849
-
1850
- private undeleteSubDirectoryTree(directory: SubDirectory) {
1851
- // Restore deleted subdirectory tree. This will unmark "deleted" from the subdirectories from bottom to top.
1852
- for (const [_, subDirectory] of this._subdirectories.entries()) {
1853
- this.undeleteSubDirectoryTree(subDirectory);
1854
- }
1855
- directory.undispose();
1856
- }
1223
+ /**
1224
+ * Tells if the sub directory is deleted or not.
1225
+ */
1226
+ private _deleted = false;
1227
+
1228
+ /**
1229
+ * String representation for the class.
1230
+ */
1231
+ public [Symbol.toStringTag]: string = "SubDirectory";
1232
+
1233
+ /**
1234
+ * The in-memory data the directory is storing.
1235
+ */
1236
+ private readonly _storage: Map<string, ILocalValue> = new Map();
1237
+
1238
+ /**
1239
+ * The subdirectories the directory is holding.
1240
+ */
1241
+ private readonly _subdirectories: Map<string, SubDirectory> = new Map();
1242
+
1243
+ /**
1244
+ * Keys that have been modified locally but not yet ack'd from the server. This is for operations on keys like
1245
+ * set/delete operations on keys. The value of this map is list of pendingMessageIds at which that key
1246
+ * was modified. We don't store the type of ops, and behaviour of key ops are different from behaviour of sub
1247
+ * directory ops, so we have separate map from subDirectories tracker.
1248
+ */
1249
+ private readonly pendingKeys: Map<string, number[]> = new Map();
1250
+
1251
+ /**
1252
+ * Subdirectories that have been deleted locally but not yet ack'd from the server. This maintains the record
1253
+ * of delete op that are pending or yet to be acked from server. This is maintained just to track the locally
1254
+ * deleted sub directory.
1255
+ */
1256
+ private readonly pendingDeleteSubDirectoriesTracker: Map<string, number> = new Map();
1257
+
1258
+ /**
1259
+ * Subdirectories that have been created locally but not yet ack'd from the server. This maintains the record
1260
+ * of create op that are pending or yet to be acked from server. This is maintained just to track the locally
1261
+ * created sub directory.
1262
+ */
1263
+ private readonly pendingCreateSubDirectoriesTracker: Map<string, number> = new Map();
1264
+
1265
+ /**
1266
+ * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
1267
+ */
1268
+ private pendingMessageId: number = -1;
1269
+
1270
+ /**
1271
+ * The pending ids of any clears that have been performed locally but not yet ack'd from the server
1272
+ */
1273
+ private readonly pendingClearMessageIds: number[] = [];
1274
+
1275
+ /**
1276
+ * Assigns a unique ID to each subdirectory created locally but pending for acknowledgement, facilitating the tracking
1277
+ * of the creation order.
1278
+ */
1279
+ public localCreationSeq: number = 0;
1280
+
1281
+ /**
1282
+ * Maintains a bidirectional association between ack'd subdirectories and their seqData.
1283
+ * This helps to ensure iteration order which is consistent with the JS map spec.
1284
+ */
1285
+ public readonly ackedCreationSeqTracker: DirectoryCreationTracker;
1286
+
1287
+ /**
1288
+ * Similar to {@link ackedCreationSeqTracker}, but for local (unacked) entries.
1289
+ */
1290
+ public readonly localCreationSeqTracker: DirectoryCreationTracker;
1291
+
1292
+ /**
1293
+ * Constructor.
1294
+ * @param sequenceNumber - Message seq number at which this was created.
1295
+ * @param clientIds - Ids of client which created this directory.
1296
+ * @param directory - Reference back to the SharedDirectory to perform operations
1297
+ * @param runtime - The data store runtime this directory is associated with
1298
+ * @param serializer - The serializer to serialize / parse handles
1299
+ * @param absolutePath - The absolute path of this IDirectory
1300
+ */
1301
+ public constructor(
1302
+ private readonly seqData: SequenceData,
1303
+ private readonly clientIds: Set<string>,
1304
+ private readonly directory: SharedDirectory,
1305
+ private readonly runtime: IFluidDataStoreRuntime,
1306
+ private readonly serializer: IFluidSerializer,
1307
+ public readonly absolutePath: string,
1308
+ ) {
1309
+ super();
1310
+ this.localCreationSeqTracker = new DirectoryCreationTracker();
1311
+ this.ackedCreationSeqTracker = new DirectoryCreationTracker();
1312
+ }
1313
+
1314
+ public dispose(error?: Error): void {
1315
+ this._deleted = true;
1316
+ this.emit("disposed", this);
1317
+ }
1318
+
1319
+ /**
1320
+ * Unmark the deleted property only when rolling back delete.
1321
+ */
1322
+ private undispose(): void {
1323
+ this._deleted = false;
1324
+ this.emit("undisposed", this);
1325
+ }
1326
+
1327
+ public get disposed(): boolean {
1328
+ return this._deleted;
1329
+ }
1330
+
1331
+ private throwIfDisposed(): void {
1332
+ if (this._deleted) {
1333
+ throw new UsageError("Cannot access Disposed subDirectory");
1334
+ }
1335
+ }
1336
+
1337
+ /**
1338
+ * Checks whether the given key exists in this IDirectory.
1339
+ * @param key - The key to check
1340
+ * @returns True if the key exists, false otherwise
1341
+ */
1342
+ public has(key: string): boolean {
1343
+ this.throwIfDisposed();
1344
+ return this._storage.has(key);
1345
+ }
1346
+
1347
+ /**
1348
+ * {@inheritDoc IDirectory.get}
1349
+ */
1350
+ public get<T = unknown>(key: string): T | undefined {
1351
+ this.throwIfDisposed();
1352
+ return this._storage.get(key)?.value as T | undefined;
1353
+ }
1354
+
1355
+ /**
1356
+ * {@inheritDoc IDirectory.set}
1357
+ */
1358
+ public set<T = unknown>(key: string, value: T): this {
1359
+ this.throwIfDisposed();
1360
+ // Undefined/null keys can't be serialized to JSON in the manner we currently snapshot.
1361
+ if (key === undefined || key === null) {
1362
+ throw new Error("Undefined and null keys are not supported");
1363
+ }
1364
+
1365
+ // Create a local value and serialize it.
1366
+ const localValue = this.directory.localValueMaker.fromInMemory(value);
1367
+ const serializableValue = makeSerializable(
1368
+ localValue,
1369
+ this.serializer,
1370
+ this.directory.handle,
1371
+ );
1372
+
1373
+ // Set the value locally.
1374
+ const previousValue = this.setCore(key, localValue, true);
1375
+
1376
+ // If we are not attached, don't submit the op.
1377
+ if (!this.directory.isAttached()) {
1378
+ return this;
1379
+ }
1380
+
1381
+ const op: IDirectorySetOperation = {
1382
+ key,
1383
+ path: this.absolutePath,
1384
+ type: "set",
1385
+ value: serializableValue,
1386
+ };
1387
+ this.submitKeyMessage(op, previousValue);
1388
+ return this;
1389
+ }
1390
+
1391
+ /**
1392
+ * {@inheritDoc IDirectory.countSubDirectory}
1393
+ */
1394
+ public countSubDirectory(): number {
1395
+ return this._subdirectories.size;
1396
+ }
1397
+
1398
+ /**
1399
+ * {@inheritDoc IDirectory.createSubDirectory}
1400
+ */
1401
+ public createSubDirectory(subdirName: string): IDirectory {
1402
+ this.throwIfDisposed();
1403
+ // Undefined/null subdirectory names can't be serialized to JSON in the manner we currently snapshot.
1404
+ if (subdirName === undefined || subdirName === null) {
1405
+ throw new Error("SubDirectory name may not be undefined or null");
1406
+ }
1407
+
1408
+ if (subdirName.includes(posix.sep)) {
1409
+ throw new Error(`SubDirectory name may not contain ${posix.sep}`);
1410
+ }
1411
+
1412
+ // Create the sub directory locally first.
1413
+ const isNew = this.createSubDirectoryCore(
1414
+ subdirName,
1415
+ true,
1416
+ this.getLocalSeq(),
1417
+ this.runtime.clientId ?? "detached",
1418
+ );
1419
+ const subDir = this._subdirectories.get(subdirName);
1420
+ assert(subDir !== undefined, 0x5aa /* subdirectory should exist after creation */);
1421
+
1422
+ // If we are not attached, don't submit the op.
1423
+ if (!this.directory.isAttached()) {
1424
+ return subDir;
1425
+ }
1426
+
1427
+ // Only submit the op, if it is newly created.
1428
+ if (isNew) {
1429
+ const op: IDirectoryCreateSubDirectoryOperation = {
1430
+ path: this.absolutePath,
1431
+ subdirName,
1432
+ type: "createSubDirectory",
1433
+ };
1434
+ this.submitCreateSubDirectoryMessage(op);
1435
+ }
1436
+
1437
+ return subDir;
1438
+ }
1439
+
1440
+ /**
1441
+ * @returns The Sequence Data which should be used for local changes.
1442
+ * @remarks While detached, 0 is used rather than -1 to represent a change which should be universally known (as opposed to known
1443
+ * only by the local client). This ensures that if the directory is later attached, none of its data needs to be updated (the values
1444
+ * last set while detached will now be known to any new client, until they are changed).
1445
+ *
1446
+ * The client sequence number is incremented by 1 for maintaining the internal order of locally created subdirectories
1447
+ * TODO: Convert these conventions to named constants. The semantics used here match those for merge-tree.
1448
+ */
1449
+ private getLocalSeq(): SequenceData {
1450
+ return this.directory.isAttached()
1451
+ ? { seq: -1, clientSeq: ++this.localCreationSeq }
1452
+ : { seq: 0, clientSeq: ++this.localCreationSeq };
1453
+ }
1454
+
1455
+ /**
1456
+ * {@inheritDoc IDirectory.getSubDirectory}
1457
+ */
1458
+ public getSubDirectory(subdirName: string): IDirectory | undefined {
1459
+ this.throwIfDisposed();
1460
+ return this._subdirectories.get(subdirName);
1461
+ }
1462
+
1463
+ /**
1464
+ * {@inheritDoc IDirectory.hasSubDirectory}
1465
+ */
1466
+ public hasSubDirectory(subdirName: string): boolean {
1467
+ this.throwIfDisposed();
1468
+ return this._subdirectories.has(subdirName);
1469
+ }
1470
+
1471
+ /**
1472
+ * {@inheritDoc IDirectory.deleteSubDirectory}
1473
+ */
1474
+ public deleteSubDirectory(subdirName: string): boolean {
1475
+ this.throwIfDisposed();
1476
+ // Delete the sub directory locally first.
1477
+ const subDir = this.deleteSubDirectoryCore(subdirName, true);
1478
+
1479
+ // If we are not attached, don't submit the op.
1480
+ if (!this.directory.isAttached()) {
1481
+ return subDir !== undefined;
1482
+ }
1483
+
1484
+ // Only submit the op, if the directory existed and we deleted it.
1485
+ if (subDir !== undefined) {
1486
+ const op: IDirectoryDeleteSubDirectoryOperation = {
1487
+ path: this.absolutePath,
1488
+ subdirName,
1489
+ type: "deleteSubDirectory",
1490
+ };
1491
+
1492
+ this.submitDeleteSubDirectoryMessage(op, subDir);
1493
+ }
1494
+ return subDir !== undefined;
1495
+ }
1496
+
1497
+ /**
1498
+ * {@inheritDoc IDirectory.subdirectories}
1499
+ */
1500
+ public subdirectories(): IterableIterator<[string, IDirectory]> {
1501
+ this.throwIfDisposed();
1502
+ const ackedSubdirsInOrder = this.ackedCreationSeqTracker.keys();
1503
+ const localSubdirsInOrder = this.localCreationSeqTracker.keys(
1504
+ (key) => !this.ackedCreationSeqTracker.has(key),
1505
+ );
1506
+
1507
+ const subdirNames = [...ackedSubdirsInOrder, ...localSubdirsInOrder];
1508
+
1509
+ assert(
1510
+ subdirNames.length === this._subdirectories.size,
1511
+ 0x85c /* The count of keys for iteration should be consistent with the size of actual data */,
1512
+ );
1513
+
1514
+ const entriesIterator = {
1515
+ index: 0,
1516
+ dirs: this._subdirectories,
1517
+ next(): IteratorResult<[string, any]> {
1518
+ if (this.index < subdirNames.length) {
1519
+ const subdirName = subdirNames[this.index++];
1520
+ const subdir = this.dirs.get(subdirName);
1521
+ return { value: [subdirName, subdir], done: false };
1522
+ }
1523
+ return { value: undefined, done: true };
1524
+ },
1525
+ [Symbol.iterator](): IterableIterator<[string, any]> {
1526
+ return this;
1527
+ },
1528
+ };
1529
+
1530
+ return entriesIterator;
1531
+ }
1532
+
1533
+ /**
1534
+ * {@inheritDoc IDirectory.getWorkingDirectory}
1535
+ */
1536
+ public getWorkingDirectory(relativePath: string): IDirectory | undefined {
1537
+ this.throwIfDisposed();
1538
+ return this.directory.getWorkingDirectory(this.makeAbsolute(relativePath));
1539
+ }
1540
+
1541
+ /**
1542
+ * This checks if there is pending delete op for local delete for a given child subdirectory.
1543
+ * @param subDirName - directory name.
1544
+ * @returns true if there is pending delete.
1545
+ */
1546
+ public isSubDirectoryDeletePending(subDirName: string): boolean {
1547
+ if (this.pendingDeleteSubDirectoriesTracker.has(subDirName)) {
1548
+ return true;
1549
+ }
1550
+ return false;
1551
+ }
1552
+
1553
+ /**
1554
+ * Deletes the given key from within this IDirectory.
1555
+ * @param key - The key to delete
1556
+ * @returns True if the key existed and was deleted, false if it did not exist
1557
+ */
1558
+ public delete(key: string): boolean {
1559
+ this.throwIfDisposed();
1560
+ // Delete the key locally first.
1561
+ const previousValue = this.deleteCore(key, true);
1562
+
1563
+ // If we are not attached, don't submit the op.
1564
+ if (!this.directory.isAttached()) {
1565
+ return previousValue !== undefined;
1566
+ }
1567
+
1568
+ const op: IDirectoryDeleteOperation = {
1569
+ key,
1570
+ path: this.absolutePath,
1571
+ type: "delete",
1572
+ };
1573
+
1574
+ this.submitKeyMessage(op, previousValue);
1575
+ return previousValue !== undefined;
1576
+ }
1577
+
1578
+ /**
1579
+ * Deletes all keys from within this IDirectory.
1580
+ */
1581
+ public clear(): void {
1582
+ this.throwIfDisposed();
1583
+
1584
+ // If we are not attached, don't submit the op.
1585
+ if (!this.directory.isAttached()) {
1586
+ this.clearCore(true);
1587
+ return;
1588
+ }
1589
+
1590
+ const copy = new Map<string, ILocalValue>(this._storage);
1591
+ this.clearCore(true);
1592
+ const op: IDirectoryClearOperation = {
1593
+ path: this.absolutePath,
1594
+ type: "clear",
1595
+ };
1596
+ this.submitClearMessage(op, copy);
1597
+ }
1598
+
1599
+ /**
1600
+ * Issue a callback on each entry under this IDirectory.
1601
+ * @param callback - Callback to issue
1602
+ */
1603
+ public forEach(
1604
+ callback: (value: unknown, key: string, map: Map<string, unknown>) => void,
1605
+ ): void {
1606
+ this.throwIfDisposed();
1607
+ // eslint-disable-next-line unicorn/no-array-for-each
1608
+ this._storage.forEach((localValue, key, map) => {
1609
+ callback(localValue.value, key, map);
1610
+ });
1611
+ }
1612
+
1613
+ /**
1614
+ * The number of entries under this IDirectory.
1615
+ */
1616
+ public get size(): number {
1617
+ this.throwIfDisposed();
1618
+ return this._storage.size;
1619
+ }
1620
+
1621
+ /**
1622
+ * Get an iterator over the entries under this IDirectory.
1623
+ * @returns The iterator
1624
+ */
1625
+ public entries(): IterableIterator<[string, unknown]> {
1626
+ this.throwIfDisposed();
1627
+ const localEntriesIterator = this._storage.entries();
1628
+ const iterator = {
1629
+ next(): IteratorResult<[string, unknown]> {
1630
+ const nextVal = localEntriesIterator.next();
1631
+ return nextVal.done
1632
+ ? { value: undefined, done: true }
1633
+ : { value: [nextVal.value[0], nextVal.value[1].value], done: false };
1634
+ },
1635
+ [Symbol.iterator](): IterableIterator<[string, unknown]> {
1636
+ return this;
1637
+ },
1638
+ };
1639
+ return iterator;
1640
+ }
1641
+
1642
+ /**
1643
+ * Get an iterator over the keys under this IDirectory.
1644
+ * @returns The iterator
1645
+ */
1646
+ public keys(): IterableIterator<string> {
1647
+ this.throwIfDisposed();
1648
+ return this._storage.keys();
1649
+ }
1650
+
1651
+ /**
1652
+ * Get an iterator over the values under this IDirectory.
1653
+ * @returns The iterator
1654
+ */
1655
+ public values(): IterableIterator<unknown> {
1656
+ this.throwIfDisposed();
1657
+ const localValuesIterator = this._storage.values();
1658
+ const iterator = {
1659
+ next(): IteratorResult<unknown> {
1660
+ const nextVal = localValuesIterator.next();
1661
+ return nextVal.done
1662
+ ? { value: undefined, done: true }
1663
+ : { value: nextVal.value.value, done: false };
1664
+ },
1665
+ [Symbol.iterator](): IterableIterator<unknown> {
1666
+ return this;
1667
+ },
1668
+ };
1669
+ return iterator;
1670
+ }
1671
+
1672
+ /**
1673
+ * Get an iterator over the entries under this IDirectory.
1674
+ * @returns The iterator
1675
+ */
1676
+ public [Symbol.iterator](): IterableIterator<[string, unknown]> {
1677
+ this.throwIfDisposed();
1678
+ return this.entries();
1679
+ }
1680
+
1681
+ /**
1682
+ * Process a clear operation.
1683
+ * @param msg - The message from the server to apply.
1684
+ * @param op - The op to process
1685
+ * @param local - Whether the message originated from the local client
1686
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1687
+ * For messages from a remote client, this will be undefined.
1688
+ * @internal
1689
+ */
1690
+ public processClearMessage(
1691
+ msg: ISequencedDocumentMessage,
1692
+ op: IDirectoryClearOperation,
1693
+ local: boolean,
1694
+ localOpMetadata: unknown,
1695
+ ): void {
1696
+ this.throwIfDisposed();
1697
+ if (!this.isMessageForCurrentInstanceOfSubDirectory(msg)) {
1698
+ return;
1699
+ }
1700
+ if (local) {
1701
+ assert(
1702
+ isClearLocalOpMetadata(localOpMetadata),
1703
+ 0x00f /* pendingMessageId is missing from the local client's operation */,
1704
+ );
1705
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
1706
+ assert(
1707
+ pendingClearMessageId === localOpMetadata.pendingMessageId,
1708
+ 0x32a /* pendingMessageId does not match */,
1709
+ );
1710
+ return;
1711
+ }
1712
+ this.clearExceptPendingKeys(false);
1713
+ }
1714
+
1715
+ /**
1716
+ * Apply clear operation locally and generate metadata
1717
+ * @param op - Op to apply
1718
+ * @returns metadata generated for stahed op
1719
+ */
1720
+ public applyStashedClearMessage(op: IDirectoryClearOperation): IClearLocalOpMetadata {
1721
+ this.throwIfDisposed();
1722
+ const previousValue = new Map<string, ILocalValue>(this._storage);
1723
+ this.clearExceptPendingKeys(true);
1724
+ const pendingMsgId = ++this.pendingMessageId;
1725
+ this.pendingClearMessageIds.push(pendingMsgId);
1726
+ const metadata: IClearLocalOpMetadata = {
1727
+ type: "clear",
1728
+ pendingMessageId: pendingMsgId,
1729
+ previousStorage: previousValue,
1730
+ };
1731
+ return metadata;
1732
+ }
1733
+
1734
+ /**
1735
+ * Process a delete operation.
1736
+ * @param msg - The message from the server to apply.
1737
+ * @param op - The op to process
1738
+ * @param local - Whether the message originated from the local client
1739
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1740
+ * For messages from a remote client, this will be undefined.
1741
+ * @internal
1742
+ */
1743
+ public processDeleteMessage(
1744
+ msg: ISequencedDocumentMessage,
1745
+ op: IDirectoryDeleteOperation,
1746
+ local: boolean,
1747
+ localOpMetadata: unknown,
1748
+ ): void {
1749
+ this.throwIfDisposed();
1750
+ if (
1751
+ !(
1752
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1753
+ this.needProcessStorageOperation(op, local, localOpMetadata)
1754
+ )
1755
+ ) {
1756
+ return;
1757
+ }
1758
+ this.deleteCore(op.key, local);
1759
+ }
1760
+
1761
+ /**
1762
+ * Apply delete operation locally and generate metadata
1763
+ * @param op - Op to apply
1764
+ * @returns metadata generated for stahed op
1765
+ */
1766
+ public applyStashedDeleteMessage(op: IDirectoryDeleteOperation): IKeyEditLocalOpMetadata {
1767
+ this.throwIfDisposed();
1768
+ const previousValue = this.deleteCore(op.key, true);
1769
+ const pendingMessageId = this.getKeyMessageId(op);
1770
+ const localMetadata: IKeyEditLocalOpMetadata = {
1771
+ type: "edit",
1772
+ pendingMessageId,
1773
+ previousValue,
1774
+ };
1775
+ return localMetadata;
1776
+ }
1777
+
1778
+ /**
1779
+ * Process a set operation.
1780
+ * @param msg - The message from the server to apply.
1781
+ * @param op - The op to process
1782
+ * @param local - Whether the message originated from the local client
1783
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1784
+ * For messages from a remote client, this will be undefined.
1785
+ * @internal
1786
+ */
1787
+ public processSetMessage(
1788
+ msg: ISequencedDocumentMessage,
1789
+ op: IDirectorySetOperation,
1790
+ context: ILocalValue | undefined,
1791
+ local: boolean,
1792
+ localOpMetadata: unknown,
1793
+ ): void {
1794
+ this.throwIfDisposed();
1795
+ if (
1796
+ !(
1797
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1798
+ this.needProcessStorageOperation(op, local, localOpMetadata)
1799
+ )
1800
+ ) {
1801
+ return;
1802
+ }
1803
+
1804
+ // needProcessStorageOperation should have returned false if local is true
1805
+ // so we can assume context is not undefined
1806
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1807
+ this.setCore(op.key, context!, local);
1808
+ }
1809
+
1810
+ /**
1811
+ * Apply set operation locally and generate metadata
1812
+ * @param op - Op to apply
1813
+ * @returns metadata generated for stahed op
1814
+ */
1815
+ public applyStashedSetMessage(
1816
+ op: IDirectorySetOperation,
1817
+ context: ILocalValue,
1818
+ ): IKeyEditLocalOpMetadata {
1819
+ this.throwIfDisposed();
1820
+ // Set the value locally.
1821
+ const previousValue = this.setCore(op.key, context, true);
1822
+
1823
+ // Create metadata
1824
+ const pendingMessageId = this.getKeyMessageId(op);
1825
+ const localMetadata: IKeyEditLocalOpMetadata = {
1826
+ type: "edit",
1827
+ pendingMessageId,
1828
+ previousValue,
1829
+ };
1830
+ return localMetadata;
1831
+ }
1832
+ /**
1833
+ * Process a create subdirectory operation.
1834
+ * @param msg - The message from the server to apply.
1835
+ * @param op - The op to process
1836
+ * @param local - Whether the message originated from the local client
1837
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1838
+ * For messages from a remote client, this will be undefined.
1839
+ * @internal
1840
+ */
1841
+ public processCreateSubDirectoryMessage(
1842
+ msg: ISequencedDocumentMessage,
1843
+ op: IDirectoryCreateSubDirectoryOperation,
1844
+ local: boolean,
1845
+ localOpMetadata: unknown,
1846
+ ): void {
1847
+ this.throwIfDisposed();
1848
+ if (
1849
+ !(
1850
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1851
+ this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)
1852
+ )
1853
+ ) {
1854
+ return;
1855
+ }
1856
+ assertNonNullClientId(msg.clientId);
1857
+ this.createSubDirectoryCore(
1858
+ op.subdirName,
1859
+ local,
1860
+ { seq: msg.sequenceNumber, clientSeq: msg.clientSequenceNumber },
1861
+ msg.clientId,
1862
+ );
1863
+ }
1864
+
1865
+ /**
1866
+ * Apply createSubDirectory operation locally and generate metadata
1867
+ * @param op - Op to apply
1868
+ * @returns metadata generated for stahed op
1869
+ */
1870
+ public applyStashedCreateSubDirMessage(
1871
+ op: IDirectoryCreateSubDirectoryOperation,
1872
+ ): ICreateSubDirLocalOpMetadata {
1873
+ this.throwIfDisposed();
1874
+ // Create the sub directory locally first.
1875
+ this.createSubDirectoryCore(
1876
+ op.subdirName,
1877
+ true,
1878
+ this.getLocalSeq(),
1879
+ this.runtime.clientId ?? "detached",
1880
+ );
1881
+ this.updatePendingSubDirMessageCount(op);
1882
+
1883
+ const localOpMetadata: ICreateSubDirLocalOpMetadata = {
1884
+ type: "createSubDir",
1885
+ };
1886
+ return localOpMetadata;
1887
+ }
1888
+
1889
+ /**
1890
+ * Process a delete subdirectory operation.
1891
+ * @param msg - The message from the server to apply.
1892
+ * @param op - The op to process
1893
+ * @param local - Whether the message originated from the local client
1894
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
1895
+ * For messages from a remote client, this will be undefined.
1896
+ * @internal
1897
+ */
1898
+ public processDeleteSubDirectoryMessage(
1899
+ msg: ISequencedDocumentMessage,
1900
+ op: IDirectoryDeleteSubDirectoryOperation,
1901
+ local: boolean,
1902
+ localOpMetadata: unknown,
1903
+ ): void {
1904
+ this.throwIfDisposed();
1905
+ if (
1906
+ !(
1907
+ this.isMessageForCurrentInstanceOfSubDirectory(msg) &&
1908
+ this.needProcessSubDirectoryOperation(msg, op, local, localOpMetadata)
1909
+ )
1910
+ ) {
1911
+ return;
1912
+ }
1913
+ this.deleteSubDirectoryCore(op.subdirName, local);
1914
+ }
1915
+
1916
+ /**
1917
+ * Apply deleteSubDirectory operation locally and generate metadata
1918
+ * @param op - Op to apply
1919
+ * @returns metadata generated for stahed op
1920
+ */
1921
+ public applyStashedDeleteSubDirMessage(
1922
+ op: IDirectoryDeleteSubDirectoryOperation,
1923
+ ): IDeleteSubDirLocalOpMetadata {
1924
+ this.throwIfDisposed();
1925
+ const subDir = this.deleteSubDirectoryCore(op.subdirName, true);
1926
+ this.updatePendingSubDirMessageCount(op);
1927
+ const metadata: IDeleteSubDirLocalOpMetadata = {
1928
+ type: "deleteSubDir",
1929
+ subDirectory: subDir,
1930
+ };
1931
+ return metadata;
1932
+ }
1933
+
1934
+ /**
1935
+ * Submit a clear operation.
1936
+ * @param op - The operation
1937
+ */
1938
+ private submitClearMessage(
1939
+ op: IDirectoryClearOperation,
1940
+ previousValue: Map<string, ILocalValue>,
1941
+ ): void {
1942
+ this.throwIfDisposed();
1943
+ const pendingMsgId = ++this.pendingMessageId;
1944
+ this.pendingClearMessageIds.push(pendingMsgId);
1945
+ const metadata: IClearLocalOpMetadata = {
1946
+ type: "clear",
1947
+ pendingMessageId: pendingMsgId,
1948
+ previousStorage: previousValue,
1949
+ };
1950
+ this.directory.submitDirectoryMessage(op, metadata);
1951
+ }
1952
+
1953
+ /**
1954
+ * Resubmit a clear operation.
1955
+ * @param op - The operation
1956
+ * @internal
1957
+ */
1958
+ public resubmitClearMessage(op: IDirectoryClearOperation, localOpMetadata: unknown): void {
1959
+ assert(
1960
+ isClearLocalOpMetadata(localOpMetadata),
1961
+ 0x32b /* Invalid localOpMetadata for clear */,
1962
+ );
1963
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1964
+ const pendingClearMessageId = this.pendingClearMessageIds.shift();
1965
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
1966
+ // is already deleted, in which case we don't need to submit the op.
1967
+ if (pendingClearMessageId === localOpMetadata.pendingMessageId) {
1968
+ this.submitClearMessage(op, localOpMetadata.previousStorage);
1969
+ }
1970
+ }
1971
+
1972
+ /**
1973
+ * Get a new pending message id for the op and cache it to track the pending op
1974
+ */
1975
+ private getKeyMessageId(op: IDirectoryKeyOperation): number {
1976
+ // We don't reuse the metadata pendingMessageId but send a new one on each submit.
1977
+ const pendingMessageId = ++this.pendingMessageId;
1978
+ const pendingMessageIds = this.pendingKeys.get(op.key);
1979
+ if (pendingMessageIds !== undefined) {
1980
+ pendingMessageIds.push(pendingMessageId);
1981
+ } else {
1982
+ this.pendingKeys.set(op.key, [pendingMessageId]);
1983
+ }
1984
+ return pendingMessageId;
1985
+ }
1986
+
1987
+ /**
1988
+ * Submit a key operation.
1989
+ * @param op - The operation
1990
+ * @param previousValue - The value of the key before this op
1991
+ */
1992
+ private submitKeyMessage(op: IDirectoryKeyOperation, previousValue?: ILocalValue): void {
1993
+ this.throwIfDisposed();
1994
+ const pendingMessageId = this.getKeyMessageId(op);
1995
+ const localMetadata = { type: "edit", pendingMessageId, previousValue };
1996
+ this.directory.submitDirectoryMessage(op, localMetadata);
1997
+ }
1998
+
1999
+ /**
2000
+ * Submit a key message to remote clients based on a previous submit.
2001
+ * @param op - The map key message
2002
+ * @param localOpMetadata - Metadata from the previous submit
2003
+ * @internal
2004
+ */
2005
+ public resubmitKeyMessage(op: IDirectoryKeyOperation, localOpMetadata: unknown): void {
2006
+ assert(
2007
+ isKeyEditLocalOpMetadata(localOpMetadata),
2008
+ 0x32d /* Invalid localOpMetadata in submit */,
2009
+ );
2010
+
2011
+ // clear the old pending message id
2012
+ const pendingMessageIds = this.pendingKeys.get(op.key);
2013
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
2014
+ // is already deleted, in which case we don't need to submit the op.
2015
+ if (pendingMessageIds !== undefined) {
2016
+ const index = pendingMessageIds.findIndex(
2017
+ (id) => id === localOpMetadata.pendingMessageId,
2018
+ );
2019
+ if (index === -1) {
2020
+ return;
2021
+ }
2022
+ pendingMessageIds.splice(index, 1);
2023
+ if (pendingMessageIds.length === 0) {
2024
+ this.pendingKeys.delete(op.key);
2025
+ }
2026
+ this.submitKeyMessage(op, localOpMetadata.previousValue);
2027
+ }
2028
+ }
2029
+
2030
+ private incrementPendingSubDirCount(map: Map<string, number>, subDirName: string) {
2031
+ const count = map.get(subDirName) ?? 0;
2032
+ map.set(subDirName, count + 1);
2033
+ }
2034
+
2035
+ private decrementPendingSubDirCount(map: Map<string, number>, subDirName: string) {
2036
+ const count = map.get(subDirName) ?? 0;
2037
+ map.set(subDirName, count - 1);
2038
+ if (count <= 1) {
2039
+ map.delete(subDirName);
2040
+ }
2041
+ }
2042
+
2043
+ /**
2044
+ * Update the count for pending create/delete of the sub directory so that it can be validated on receiving op
2045
+ * or while resubmitting the op.
2046
+ */
2047
+ private updatePendingSubDirMessageCount(op: IDirectorySubDirectoryOperation) {
2048
+ if (op.type === "deleteSubDirectory") {
2049
+ this.incrementPendingSubDirCount(
2050
+ this.pendingDeleteSubDirectoriesTracker,
2051
+ op.subdirName,
2052
+ );
2053
+ } else if (op.type === "createSubDirectory") {
2054
+ this.incrementPendingSubDirCount(
2055
+ this.pendingCreateSubDirectoriesTracker,
2056
+ op.subdirName,
2057
+ );
2058
+ }
2059
+ }
2060
+
2061
+ /**
2062
+ * Submit a create subdirectory operation.
2063
+ * @param op - The operation
2064
+ */
2065
+ private submitCreateSubDirectoryMessage(op: IDirectorySubDirectoryOperation): void {
2066
+ this.throwIfDisposed();
2067
+ this.updatePendingSubDirMessageCount(op);
2068
+
2069
+ const localOpMetadata: ICreateSubDirLocalOpMetadata = {
2070
+ type: "createSubDir",
2071
+ };
2072
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
2073
+ }
2074
+
2075
+ /**
2076
+ * Submit a delete subdirectory operation.
2077
+ * @param op - The operation
2078
+ * @param subDir - Any subdirectory deleted by the op
2079
+ */
2080
+ private submitDeleteSubDirectoryMessage(
2081
+ op: IDirectorySubDirectoryOperation,
2082
+ subDir: SubDirectory | undefined,
2083
+ ): void {
2084
+ this.throwIfDisposed();
2085
+ this.updatePendingSubDirMessageCount(op);
2086
+
2087
+ const localOpMetadata: IDeleteSubDirLocalOpMetadata = {
2088
+ type: "deleteSubDir",
2089
+ subDirectory: subDir,
2090
+ };
2091
+ this.directory.submitDirectoryMessage(op, localOpMetadata);
2092
+ }
2093
+
2094
+ /**
2095
+ * Submit a subdirectory operation again
2096
+ * @param op - The operation
2097
+ * @param localOpMetadata - metadata submitted with the op originally
2098
+ * @internal
2099
+ */
2100
+ public resubmitSubDirectoryMessage(
2101
+ op: IDirectorySubDirectoryOperation,
2102
+ localOpMetadata: unknown,
2103
+ ): void {
2104
+ assert(
2105
+ isSubDirLocalOpMetadata(localOpMetadata),
2106
+ 0x32f /* Invalid localOpMetadata for sub directory op */,
2107
+ );
2108
+
2109
+ // Only submit the op, if we have record for it, otherwise it is possible that the older instance
2110
+ // is already deleted, in which case we don't need to submit the op.
2111
+ if (
2112
+ localOpMetadata.type === "createSubDir" &&
2113
+ !this.pendingCreateSubDirectoriesTracker.has(op.subdirName)
2114
+ ) {
2115
+ return;
2116
+ } else if (
2117
+ localOpMetadata.type === "deleteSubDir" &&
2118
+ !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)
2119
+ ) {
2120
+ return;
2121
+ }
2122
+
2123
+ if (localOpMetadata.type === "createSubDir") {
2124
+ this.decrementPendingSubDirCount(
2125
+ this.pendingCreateSubDirectoriesTracker,
2126
+ op.subdirName,
2127
+ );
2128
+ this.submitCreateSubDirectoryMessage(op);
2129
+ } else {
2130
+ this.decrementPendingSubDirCount(
2131
+ this.pendingDeleteSubDirectoriesTracker,
2132
+ op.subdirName,
2133
+ );
2134
+ this.submitDeleteSubDirectoryMessage(op, localOpMetadata.subDirectory);
2135
+ }
2136
+ }
2137
+
2138
+ /**
2139
+ * Get the storage of this subdirectory in a serializable format, to be used in snapshotting.
2140
+ * @param serializer - The serializer to use to serialize handles in its values.
2141
+ * @returns The JSONable string representing the storage of this subdirectory
2142
+ * @internal
2143
+ */
2144
+ public *getSerializedStorage(
2145
+ serializer: IFluidSerializer,
2146
+ ): Generator<[string, ISerializedValue], void> {
2147
+ this.throwIfDisposed();
2148
+ for (const [key, localValue] of this._storage) {
2149
+ const value = localValue.makeSerialized(serializer, this.directory.handle);
2150
+ const res: [string, ISerializedValue] = [key, value];
2151
+ yield res;
2152
+ }
2153
+ }
2154
+
2155
+ public getSerializableCreateInfo() {
2156
+ this.throwIfDisposed();
2157
+ const createInfo: ICreateInfo = {
2158
+ csn: this.seqData.seq,
2159
+ ccIds: Array.from(this.clientIds),
2160
+ };
2161
+ return createInfo;
2162
+ }
2163
+
2164
+ /**
2165
+ * Populate a key value in this subdirectory's storage, to be used when loading from snapshot.
2166
+ * @param key - The key to populate
2167
+ * @param localValue - The local value to populate into it
2168
+ * @internal
2169
+ */
2170
+ public populateStorage(key: string, localValue: ILocalValue): void {
2171
+ this.throwIfDisposed();
2172
+ this._storage.set(key, localValue);
2173
+ }
2174
+
2175
+ /**
2176
+ * Populate a subdirectory into this subdirectory, to be used when loading from snapshot.
2177
+ * @param subdirName - The name of the subdirectory to add
2178
+ * @param newSubDir - The new subdirectory to add
2179
+ * @internal
2180
+ */
2181
+ public populateSubDirectory(subdirName: string, newSubDir: SubDirectory): void {
2182
+ this.throwIfDisposed();
2183
+ this._subdirectories.set(subdirName, newSubDir);
2184
+ }
2185
+
2186
+ /**
2187
+ * Retrieve the local value at the given key. This is used to get value type information stashed on the local
2188
+ * value so op handlers can be retrieved
2189
+ * @param key - The key to retrieve from
2190
+ * @returns The local value
2191
+ * @internal
2192
+ */
2193
+ public getLocalValue<T extends ILocalValue = ILocalValue>(key: string): T {
2194
+ this.throwIfDisposed();
2195
+ return this._storage.get(key) as T;
2196
+ }
2197
+
2198
+ /**
2199
+ * Remove the pendingMessageId from the map tracking it on rollback
2200
+ * @param map - map tracking the pending messages
2201
+ * @param key - key of the edit in the op
2202
+ */
2203
+ private rollbackPendingMessageId(
2204
+ map: Map<string, number[]>,
2205
+ key: string,
2206
+ pendingMessageId,
2207
+ ): void {
2208
+ const pendingMessageIds = map.get(key);
2209
+ const lastPendingMessageId = pendingMessageIds?.pop();
2210
+ if (!pendingMessageIds || lastPendingMessageId !== pendingMessageId) {
2211
+ throw new Error("Rollback op does not match last pending");
2212
+ }
2213
+ if (pendingMessageIds.length === 0) {
2214
+ map.delete(key);
2215
+ }
2216
+ }
2217
+
2218
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2219
+
2220
+ /**
2221
+ * Rollback a local op
2222
+ * @param op - The operation to rollback
2223
+ * @param localOpMetadata - The local metadata associated with the op.
2224
+ */
2225
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2226
+ public rollback(op: any, localOpMetadata: unknown): void {
2227
+ if (!isDirectoryLocalOpMetadata(localOpMetadata)) {
2228
+ throw new Error("Invalid localOpMetadata");
2229
+ }
2230
+
2231
+ if (op.type === "clear" && localOpMetadata.type === "clear") {
2232
+ for (const [key, localValue] of localOpMetadata.previousStorage.entries()) {
2233
+ this.setCore(key, localValue, true);
2234
+ }
2235
+
2236
+ const lastPendingClearId = this.pendingClearMessageIds.pop();
2237
+ if (
2238
+ lastPendingClearId === undefined ||
2239
+ lastPendingClearId !== localOpMetadata.pendingMessageId
2240
+ ) {
2241
+ throw new Error("Rollback op does match last clear");
2242
+ }
2243
+ } else if ((op.type === "delete" || op.type === "set") && localOpMetadata.type === "edit") {
2244
+ if (localOpMetadata.previousValue === undefined) {
2245
+ this.deleteCore(op.key as string, true);
2246
+ } else {
2247
+ this.setCore(op.key as string, localOpMetadata.previousValue, true);
2248
+ }
2249
+
2250
+ this.rollbackPendingMessageId(
2251
+ this.pendingKeys,
2252
+ op.key as string,
2253
+ localOpMetadata.pendingMessageId,
2254
+ );
2255
+ } else if (op.type === "createSubDirectory" && localOpMetadata.type === "createSubDir") {
2256
+ this.deleteSubDirectoryCore(op.subdirName as string, true);
2257
+
2258
+ this.decrementPendingSubDirCount(
2259
+ this.pendingCreateSubDirectoriesTracker,
2260
+ op.subdirName as string,
2261
+ );
2262
+ } else if (op.type === "deleteSubDirectory" && localOpMetadata.type === "deleteSubDir") {
2263
+ if (localOpMetadata.subDirectory !== undefined) {
2264
+ this.undeleteSubDirectoryTree(localOpMetadata.subDirectory);
2265
+ // don't need to register events because deleting never unregistered
2266
+ this._subdirectories.set(op.subdirName as string, localOpMetadata.subDirectory);
2267
+ // Restore the record in creation tracker
2268
+ if (isAcknowledgedOrDetached(localOpMetadata.subDirectory.seqData)) {
2269
+ this.ackedCreationSeqTracker.set(op.subdirName, {
2270
+ ...localOpMetadata.subDirectory.seqData,
2271
+ });
2272
+ } else {
2273
+ this.localCreationSeqTracker.set(op.subdirName, {
2274
+ ...localOpMetadata.subDirectory.seqData,
2275
+ });
2276
+ }
2277
+ this.emit("subDirectoryCreated", op.subdirName, true, this);
2278
+ }
2279
+
2280
+ this.decrementPendingSubDirCount(
2281
+ this.pendingDeleteSubDirectoriesTracker,
2282
+ op.subDirName as string,
2283
+ );
2284
+ } else {
2285
+ throw new Error("Unsupported op for rollback");
2286
+ }
2287
+ }
2288
+
2289
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
2290
+
2291
+ /**
2292
+ * Converts the given relative path into an absolute path.
2293
+ * @param path - Relative path to convert
2294
+ * @returns The equivalent absolute path
2295
+ */
2296
+ private makeAbsolute(relativePath: string): string {
2297
+ return posix.resolve(this.absolutePath, relativePath);
2298
+ }
2299
+
2300
+ /**
2301
+ * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
2302
+ * not process the incoming operation.
2303
+ * @param op - Operation to check
2304
+ * @param local - Whether the operation originated from the local client
2305
+ * @param localOpMetadata - For local client ops, this is the metadata that was submitted with the op.
2306
+ * For ops from a remote client, this will be undefined.
2307
+ * @returns True if the operation should be processed, false otherwise
2308
+ */
2309
+ private needProcessStorageOperation(
2310
+ op: IDirectoryKeyOperation,
2311
+ local: boolean,
2312
+ localOpMetadata: unknown,
2313
+ ): boolean {
2314
+ if (this.pendingClearMessageIds.length > 0) {
2315
+ if (local) {
2316
+ assert(
2317
+ localOpMetadata !== undefined &&
2318
+ isKeyEditLocalOpMetadata(localOpMetadata) &&
2319
+ localOpMetadata.pendingMessageId < this.pendingClearMessageIds[0],
2320
+ 0x010 /* "Received out of order storage op when there is an unackd clear message" */,
2321
+ );
2322
+ // Remove all pendingMessageIds lower than first pendingClearMessageId.
2323
+ const lowestPendingClearMessageId = this.pendingClearMessageIds[0];
2324
+ const pendingKeyMessageIdArray = this.pendingKeys.get(op.key);
2325
+ if (pendingKeyMessageIdArray !== undefined) {
2326
+ let index = 0;
2327
+ while (pendingKeyMessageIdArray[index] < lowestPendingClearMessageId) {
2328
+ index += 1;
2329
+ }
2330
+ const newPendingKeyMessageId = pendingKeyMessageIdArray.splice(index);
2331
+ if (newPendingKeyMessageId.length === 0) {
2332
+ this.pendingKeys.delete(op.key);
2333
+ } else {
2334
+ this.pendingKeys.set(op.key, newPendingKeyMessageId);
2335
+ }
2336
+ }
2337
+ }
2338
+
2339
+ // If I have a NACK clear, we can ignore all ops.
2340
+ return false;
2341
+ }
2342
+
2343
+ const pendingKeyMessageIds = this.pendingKeys.get(op.key);
2344
+ if (pendingKeyMessageIds !== undefined) {
2345
+ // Found an NACK op, clear it from the directory if the latest sequence number in the directory
2346
+ // match the message's and don't process the op.
2347
+ if (local) {
2348
+ assert(
2349
+ localOpMetadata !== undefined && isKeyEditLocalOpMetadata(localOpMetadata),
2350
+ 0x011 /* pendingMessageId is missing from the local client's operation */,
2351
+ );
2352
+ assert(
2353
+ pendingKeyMessageIds[0] === localOpMetadata.pendingMessageId,
2354
+ 0x331 /* Unexpected pending message received */,
2355
+ );
2356
+ pendingKeyMessageIds.shift();
2357
+ if (pendingKeyMessageIds.length === 0) {
2358
+ this.pendingKeys.delete(op.key);
2359
+ }
2360
+ }
2361
+ return false;
2362
+ }
2363
+
2364
+ // If we don't have a NACK op on the key, we need to process the remote ops.
2365
+ return !local;
2366
+ }
2367
+
2368
+ /**
2369
+ * This return true if the message is for the current instance of this sub directory. As the sub directory
2370
+ * can be deleted and created again, then this finds if the message is for current instance of directory or not.
2371
+ * @param msg - message for the directory
2372
+ */
2373
+ private isMessageForCurrentInstanceOfSubDirectory(msg: ISequencedDocumentMessage) {
2374
+ // If the message is either from the creator of directory or this directory was created when
2375
+ // container was detached or in case this directory is already live(known to other clients)
2376
+ // and the op was created after the directory was created then apply this op.
2377
+ return (
2378
+ (msg.clientId !== null && this.clientIds.has(msg.clientId)) ||
2379
+ this.clientIds.has("detached") ||
2380
+ (this.seqData.seq !== -1 && this.seqData.seq <= msg.referenceSequenceNumber)
2381
+ );
2382
+ }
2383
+
2384
+ /**
2385
+ * If our local operations that have not yet been ack'd will eventually overwrite an incoming operation, we should
2386
+ * not process the incoming operation.
2387
+ * @param op - Operation to check
2388
+ * @param local - Whether the message originated from the local client
2389
+ * @param message - The message
2390
+ * @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
2391
+ * For messages from a remote client, this will be undefined.
2392
+ * @returns True if the operation should be processed, false otherwise
2393
+ */
2394
+ private needProcessSubDirectoryOperation(
2395
+ msg: ISequencedDocumentMessage,
2396
+ op: IDirectorySubDirectoryOperation,
2397
+ local: boolean,
2398
+ localOpMetadata: unknown,
2399
+ ): boolean {
2400
+ assertNonNullClientId(msg.clientId);
2401
+ const pendingDeleteCount = this.pendingDeleteSubDirectoriesTracker.get(op.subdirName);
2402
+ const pendingCreateCount = this.pendingCreateSubDirectoriesTracker.get(op.subdirName);
2403
+ if (
2404
+ (pendingDeleteCount !== undefined && pendingDeleteCount > 0) ||
2405
+ (pendingCreateCount !== undefined && pendingCreateCount > 0)
2406
+ ) {
2407
+ if (local) {
2408
+ assert(
2409
+ isSubDirLocalOpMetadata(localOpMetadata),
2410
+ 0x012 /* pendingMessageId is missing from the local client's operation */,
2411
+ );
2412
+ if (localOpMetadata.type === "deleteSubDir") {
2413
+ assert(
2414
+ pendingDeleteCount !== undefined && pendingDeleteCount > 0,
2415
+ 0x6c2 /* pendingDeleteCount should exist */,
2416
+ );
2417
+ this.decrementPendingSubDirCount(
2418
+ this.pendingDeleteSubDirectoriesTracker,
2419
+ op.subdirName,
2420
+ );
2421
+ } else if (localOpMetadata.type === "createSubDir") {
2422
+ assert(
2423
+ pendingCreateCount !== undefined && pendingCreateCount > 0,
2424
+ 0x6c3 /* pendingCreateCount should exist */,
2425
+ );
2426
+ this.decrementPendingSubDirCount(
2427
+ this.pendingCreateSubDirectoriesTracker,
2428
+ op.subdirName,
2429
+ );
2430
+ }
2431
+ }
2432
+ if (op.type === "deleteSubDirectory") {
2433
+ const resetSubDirectoryTree = (directory: SubDirectory | undefined): void => {
2434
+ if (!directory) {
2435
+ return;
2436
+ }
2437
+ // If this is delete op and we have keys in this subDirectory, then we need to delete these
2438
+ // keys except the pending ones as they will be sequenced after this delete.
2439
+ directory.clearExceptPendingKeys(local);
2440
+ // In case of delete op, we need to reset the creation seqNum, clientSeqNum and client ids of
2441
+ // creators as the previous directory is getting deleted and we will initialize again when
2442
+ // we will receive op for the create again.
2443
+ directory.seqData.seq = -1;
2444
+ directory.seqData.clientSeq = -1;
2445
+ directory.clientIds.clear();
2446
+ // Do the same thing for the subtree of the directory. If create is not pending for a child, then just
2447
+ // delete it.
2448
+ const subDirectories = directory.subdirectories();
2449
+ for (const [subDirName, subDir] of subDirectories) {
2450
+ if (directory.pendingCreateSubDirectoriesTracker.has(subDirName)) {
2451
+ resetSubDirectoryTree(subDir as SubDirectory);
2452
+ continue;
2453
+ }
2454
+ directory.deleteSubDirectoryCore(subDirName, false);
2455
+ }
2456
+ };
2457
+ const subDirectory = this._subdirectories.get(op.subdirName);
2458
+ // Clear the creation tracker record
2459
+ this.ackedCreationSeqTracker.delete(op.subdirName);
2460
+ resetSubDirectoryTree(subDirectory);
2461
+ }
2462
+ if (op.type === "createSubDirectory") {
2463
+ const dir = this._subdirectories.get(op.subdirName);
2464
+ // Child sub directory create seq number can't be lower than the parent subdirectory.
2465
+ // The sequence number for multiple ops can be the same when multiple createSubDirectory occurs with grouped batching enabled, thus <= and not just <.
2466
+ if (this.seqData.seq !== -1 && this.seqData.seq <= msg.sequenceNumber) {
2467
+ if (dir?.seqData.seq === -1) {
2468
+ // Only set the sequence data based on the first message
2469
+ dir.seqData.seq = msg.sequenceNumber;
2470
+ dir.seqData.clientSeq = msg.clientSequenceNumber;
2471
+
2472
+ // set the creation seq in tracker
2473
+ if (
2474
+ !this.ackedCreationSeqTracker.has(op.subdirName) &&
2475
+ !this.pendingDeleteSubDirectoriesTracker.has(op.subdirName)
2476
+ ) {
2477
+ this.ackedCreationSeqTracker.set(op.subdirName, {
2478
+ seq: msg.sequenceNumber,
2479
+ clientSeq: msg.clientSequenceNumber,
2480
+ });
2481
+ if (local) {
2482
+ this.localCreationSeqTracker.delete(op.subdirName);
2483
+ }
2484
+ }
2485
+ }
2486
+ // The client created the dir at or after the dirs seq, so list its client id as a creator.
2487
+ if (
2488
+ dir !== undefined &&
2489
+ !dir.clientIds.has(msg.clientId) &&
2490
+ dir.seqData.seq <= msg.sequenceNumber
2491
+ ) {
2492
+ dir.clientIds.add(msg.clientId);
2493
+ }
2494
+ }
2495
+ }
2496
+ return false;
2497
+ }
2498
+
2499
+ return !local;
2500
+ }
2501
+
2502
+ /**
2503
+ * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
2504
+ */
2505
+ private clearExceptPendingKeys(local: boolean): void {
2506
+ // Assuming the pendingKeys is small and the map is large
2507
+ // we will get the value for the pendingKeys and clear the map
2508
+ const temp = new Map<string, ILocalValue>();
2509
+
2510
+ for (const [key] of this.pendingKeys) {
2511
+ const value = this._storage.get(key);
2512
+ // If this key is already deleted, then we don't need to add it again.
2513
+ if (value !== undefined) {
2514
+ temp.set(key, value);
2515
+ }
2516
+ }
2517
+
2518
+ this.clearCore(local);
2519
+
2520
+ for (const [key, value] of temp.entries()) {
2521
+ this.setCore(key, value, true);
2522
+ }
2523
+ }
2524
+
2525
+ /**
2526
+ * Clear implementation used for both locally sourced clears as well as incoming remote clears.
2527
+ * @param local - Whether the message originated from the local client
2528
+ */
2529
+ private clearCore(local: boolean): void {
2530
+ this._storage.clear();
2531
+ this.directory.emit("clear", local, this.directory);
2532
+ }
2533
+
2534
+ /**
2535
+ * Delete implementation used for both locally sourced deletes as well as incoming remote deletes.
2536
+ * @param key - The key being deleted
2537
+ * @param local - Whether the message originated from the local client
2538
+ * @returns Previous local value of the key if it existed, undefined if it did not exist
2539
+ */
2540
+ private deleteCore(key: string, local: boolean): ILocalValue | undefined {
2541
+ const previousLocalValue = this._storage.get(key);
2542
+ const previousValue: unknown = previousLocalValue?.value;
2543
+ const successfullyRemoved = this._storage.delete(key);
2544
+ if (successfullyRemoved) {
2545
+ const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
2546
+ this.directory.emit("valueChanged", event, local, this.directory);
2547
+ const containedEvent: IValueChanged = { key, previousValue };
2548
+ this.emit("containedValueChanged", containedEvent, local, this);
2549
+ }
2550
+ return previousLocalValue;
2551
+ }
2552
+
2553
+ /**
2554
+ * Set implementation used for both locally sourced sets as well as incoming remote sets.
2555
+ * @param key - The key being set
2556
+ * @param value - The value being set
2557
+ * @param local - Whether the message originated from the local client
2558
+ * @returns Previous local value of the key, if any
2559
+ */
2560
+ private setCore(key: string, value: ILocalValue, local: boolean): ILocalValue | undefined {
2561
+ const previousLocalValue = this._storage.get(key);
2562
+ const previousValue: unknown = previousLocalValue?.value;
2563
+ this._storage.set(key, value);
2564
+ const event: IDirectoryValueChanged = { key, path: this.absolutePath, previousValue };
2565
+ this.directory.emit("valueChanged", event, local, this.directory);
2566
+ const containedEvent: IValueChanged = { key, previousValue };
2567
+ this.emit("containedValueChanged", containedEvent, local, this);
2568
+ return previousLocalValue;
2569
+ }
2570
+
2571
+ /**
2572
+ * Create subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
2573
+ * @param subdirName - The name of the subdirectory being created
2574
+ * @param local - Whether the message originated from the local client
2575
+ * @param seqData - Sequence number and client sequence number at which this directory is created
2576
+ * @param clientId - Id of client which created this directory.
2577
+ * @returns True if is newly created, false if it already existed.
2578
+ */
2579
+ private createSubDirectoryCore(
2580
+ subdirName: string,
2581
+ local: boolean,
2582
+ seqData: SequenceData,
2583
+ clientId: string,
2584
+ ): boolean {
2585
+ const subdir = this._subdirectories.get(subdirName);
2586
+ if (subdir === undefined) {
2587
+ const absolutePath = posix.join(this.absolutePath, subdirName);
2588
+ const subDir = new SubDirectory(
2589
+ { ...seqData },
2590
+ new Set([clientId]),
2591
+ this.directory,
2592
+ this.runtime,
2593
+ this.serializer,
2594
+ absolutePath,
2595
+ );
2596
+ /**
2597
+ * Store the sequnce numbers of newly created subdirectory to the proper creation tracker, based
2598
+ * on whether the creation behavior has been ack'd or not
2599
+ */
2600
+ if (!isAcknowledgedOrDetached(seqData)) {
2601
+ this.localCreationSeqTracker.set(subdirName, { ...seqData });
2602
+ } else {
2603
+ this.ackedCreationSeqTracker.set(subdirName, { ...seqData });
2604
+ }
2605
+
2606
+ this.registerEventsOnSubDirectory(subDir, subdirName);
2607
+ this._subdirectories.set(subdirName, subDir);
2608
+ this.emit("subDirectoryCreated", subdirName, local, this);
2609
+ return true;
2610
+ } else {
2611
+ subdir.clientIds.add(clientId);
2612
+ }
2613
+ return false;
2614
+ }
2615
+
2616
+ private registerEventsOnSubDirectory(subDirectory: SubDirectory, subDirName: string): void {
2617
+ subDirectory.on("subDirectoryCreated", (relativePath: string, local: boolean) => {
2618
+ this.emit("subDirectoryCreated", posix.join(subDirName, relativePath), local, this);
2619
+ });
2620
+ subDirectory.on("subDirectoryDeleted", (relativePath: string, local: boolean) => {
2621
+ this.emit("subDirectoryDeleted", posix.join(subDirName, relativePath), local, this);
2622
+ });
2623
+ }
2624
+
2625
+ /**
2626
+ * Delete subdirectory implementation used for both locally sourced creation as well as incoming remote creation.
2627
+ * @param subdirName - The name of the subdirectory being deleted
2628
+ * @param local - Whether the message originated from the local client
2629
+ */
2630
+ private deleteSubDirectoryCore(subdirName: string, local: boolean): SubDirectory | undefined {
2631
+ const previousValue = this._subdirectories.get(subdirName);
2632
+ // This should make the subdirectory structure unreachable so it can be GC'd and won't appear in snapshots
2633
+ // Might want to consider cleaning out the structure more exhaustively though? But not when rollback.
2634
+ if (previousValue !== undefined) {
2635
+ this._subdirectories.delete(subdirName);
2636
+ /**
2637
+ * Remove the corresponding record from the proper creation tracker, based on whether the subdirectory has been
2638
+ * ack'd already or still not committed yet (could be both).
2639
+ */
2640
+ if (this.ackedCreationSeqTracker.has(subdirName)) {
2641
+ this.ackedCreationSeqTracker.delete(subdirName);
2642
+ }
2643
+ if (this.localCreationSeqTracker.has(subdirName)) {
2644
+ this.localCreationSeqTracker.delete(subdirName);
2645
+ }
2646
+ this.disposeSubDirectoryTree(previousValue);
2647
+ this.emit("subDirectoryDeleted", subdirName, local, this);
2648
+ }
2649
+ return previousValue;
2650
+ }
2651
+
2652
+ private disposeSubDirectoryTree(directory: IDirectory | undefined): void {
2653
+ if (!directory) {
2654
+ return;
2655
+ }
2656
+ // Dispose the subdirectory tree. This will dispose the subdirectories from bottom to top.
2657
+ const subDirectories = directory.subdirectories();
2658
+ for (const [_, subDirectory] of subDirectories) {
2659
+ this.disposeSubDirectoryTree(subDirectory);
2660
+ }
2661
+ if (typeof directory.dispose === "function") {
2662
+ directory.dispose();
2663
+ }
2664
+ }
2665
+
2666
+ private undeleteSubDirectoryTree(directory: SubDirectory): void {
2667
+ // Restore deleted subdirectory tree. Need to undispose the current directory first, then get access to the iterator.
2668
+ // This will unmark "deleted" from the subdirectories from top to bottom.
2669
+ directory.undispose();
2670
+ for (const [_, subDirectory] of directory.subdirectories()) {
2671
+ this.undeleteSubDirectoryTree(subDirectory as SubDirectory);
2672
+ }
2673
+ }
1857
2674
  }