@fluidframework/map 2.0.0-internal.3.0.2 → 2.0.0-internal.3.2.0

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