@fluidframework/map 2.0.0-dev.2.3.0.115467 → 2.0.0-dev.4.1.0.148229

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