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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.eslintrc.js +12 -14
  2. package/.mocharc.js +2 -2
  3. package/README.md +3 -3
  4. package/api-extractor.json +2 -2
  5. package/dist/directory.d.ts +38 -5
  6. package/dist/directory.d.ts.map +1 -1
  7. package/dist/directory.js +285 -88
  8. package/dist/directory.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/interfaces.d.ts +27 -17
  13. package/dist/interfaces.d.ts.map +1 -1
  14. package/dist/interfaces.js.map +1 -1
  15. package/dist/internalInterfaces.d.ts +39 -0
  16. package/dist/internalInterfaces.d.ts.map +1 -1
  17. package/dist/internalInterfaces.js.map +1 -1
  18. package/dist/localValues.d.ts +12 -3
  19. package/dist/localValues.d.ts.map +1 -1
  20. package/dist/localValues.js +10 -0
  21. package/dist/localValues.js.map +1 -1
  22. package/dist/map.d.ts +5 -5
  23. package/dist/map.d.ts.map +1 -1
  24. package/dist/map.js +15 -2
  25. package/dist/map.js.map +1 -1
  26. package/dist/mapKernel.d.ts +5 -5
  27. package/dist/mapKernel.d.ts.map +1 -1
  28. package/dist/mapKernel.js +58 -33
  29. package/dist/mapKernel.js.map +1 -1
  30. package/dist/packageVersion.d.ts +1 -1
  31. package/dist/packageVersion.js +1 -1
  32. package/dist/packageVersion.js.map +1 -1
  33. package/lib/directory.d.ts +38 -5
  34. package/lib/directory.d.ts.map +1 -1
  35. package/lib/directory.js +287 -90
  36. package/lib/directory.js.map +1 -1
  37. package/lib/index.d.ts +1 -1
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/index.js.map +1 -1
  40. package/lib/interfaces.d.ts +27 -17
  41. package/lib/interfaces.d.ts.map +1 -1
  42. package/lib/interfaces.js.map +1 -1
  43. package/lib/internalInterfaces.d.ts +39 -0
  44. package/lib/internalInterfaces.d.ts.map +1 -1
  45. package/lib/internalInterfaces.js.map +1 -1
  46. package/lib/localValues.d.ts +12 -3
  47. package/lib/localValues.d.ts.map +1 -1
  48. package/lib/localValues.js +10 -0
  49. package/lib/localValues.js.map +1 -1
  50. package/lib/map.d.ts +5 -5
  51. package/lib/map.d.ts.map +1 -1
  52. package/lib/map.js +16 -3
  53. package/lib/map.js.map +1 -1
  54. package/lib/mapKernel.d.ts +5 -5
  55. package/lib/mapKernel.d.ts.map +1 -1
  56. package/lib/mapKernel.js +59 -34
  57. package/lib/mapKernel.js.map +1 -1
  58. package/lib/packageVersion.d.ts +1 -1
  59. package/lib/packageVersion.js +1 -1
  60. package/lib/packageVersion.js.map +1 -1
  61. package/package.json +60 -59
  62. package/prettier.config.cjs +1 -1
  63. package/src/directory.ts +2207 -1848
  64. package/src/index.ts +1 -0
  65. package/src/interfaces.ts +309 -288
  66. package/src/internalInterfaces.ts +83 -38
  67. package/src/localValues.ts +95 -93
  68. package/src/map.ts +364 -345
  69. package/src/mapKernel.ts +729 -676
  70. package/src/packageVersion.ts +1 -1
  71. package/tsconfig.esnext.json +5 -5
  72. package/tsconfig.json +9 -15
package/src/map.ts CHANGED
@@ -5,29 +5,23 @@
5
5
 
6
6
  import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
7
7
  import {
8
- IChannelAttributes,
9
- IFluidDataStoreRuntime,
10
- IChannelStorageService,
11
- IChannelServices,
12
- IChannelFactory,
8
+ IChannelAttributes,
9
+ IFluidDataStoreRuntime,
10
+ IChannelStorageService,
11
+ IChannelServices,
12
+ IChannelFactory,
13
13
  } from "@fluidframework/datastore-definitions";
14
14
  import { ISummaryTreeWithStats, ITelemetryContext } from "@fluidframework/runtime-definitions";
15
15
  import { readAndParse } from "@fluidframework/driver-utils";
16
- import {
17
- IFluidSerializer,
18
- SharedObject,
19
- } from "@fluidframework/shared-object-base";
16
+ import { IFluidSerializer, SharedObject } from "@fluidframework/shared-object-base";
20
17
  import { SummaryTreeBuilder } from "@fluidframework/runtime-utils";
21
- import {
22
- ISharedMap,
23
- ISharedMapEvents,
24
- } from "./interfaces";
25
- import { IMapDataObjectSerializable, MapKernel } from "./mapKernel";
18
+ import { ISharedMap, ISharedMapEvents } from "./interfaces";
19
+ import { IMapDataObjectSerializable, IMapOperation, MapKernel } from "./mapKernel";
26
20
  import { pkgVersion } from "./packageVersion";
27
21
 
28
22
  interface IMapSerializationFormat {
29
- blobs?: string[];
30
- content: IMapDataObjectSerializable;
23
+ blobs?: string[];
24
+ content: IMapDataObjectSerializable;
31
25
  }
32
26
 
33
27
  const snapshotFileName = "header";
@@ -38,339 +32,364 @@ const snapshotFileName = "header";
38
32
  * @sealed
39
33
  */
40
34
  export class MapFactory implements IChannelFactory {
41
- /**
42
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
43
- */
44
- public static readonly Type = "https://graph.microsoft.com/types/map";
45
-
46
- /**
47
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
48
- */
49
- public static readonly Attributes: IChannelAttributes = {
50
- type: MapFactory.Type,
51
- snapshotFormatVersion: "0.2",
52
- packageVersion: pkgVersion,
53
- };
54
-
55
- /**
56
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
57
- */
58
- public get type() {
59
- return MapFactory.Type;
60
- }
61
-
62
- /**
63
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
64
- */
65
- public get attributes() {
66
- return MapFactory.Attributes;
67
- }
68
-
69
- /**
70
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.load}
71
- */
72
- public async load(
73
- runtime: IFluidDataStoreRuntime,
74
- id: string,
75
- services: IChannelServices,
76
- attributes: IChannelAttributes): Promise<ISharedMap> {
77
- const map = new SharedMap(id, runtime, attributes);
78
- await map.load(services);
79
-
80
- return map;
81
- }
82
-
83
- /**
84
- * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.create}
85
- */
86
- public create(runtime: IFluidDataStoreRuntime, id: string): ISharedMap {
87
- const map = new SharedMap(id, runtime, MapFactory.Attributes);
88
- map.initializeLocal();
89
-
90
- return map;
91
- }
35
+ /**
36
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
37
+ */
38
+ public static readonly Type = "https://graph.microsoft.com/types/map";
39
+
40
+ /**
41
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
42
+ */
43
+ public static readonly Attributes: IChannelAttributes = {
44
+ type: MapFactory.Type,
45
+ snapshotFormatVersion: "0.2",
46
+ packageVersion: pkgVersion,
47
+ };
48
+
49
+ /**
50
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
51
+ */
52
+ public get type(): string {
53
+ return MapFactory.Type;
54
+ }
55
+
56
+ /**
57
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
58
+ */
59
+ public get attributes(): IChannelAttributes {
60
+ return MapFactory.Attributes;
61
+ }
62
+
63
+ /**
64
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.load}
65
+ */
66
+ public async load(
67
+ runtime: IFluidDataStoreRuntime,
68
+ id: string,
69
+ services: IChannelServices,
70
+ attributes: IChannelAttributes,
71
+ ): Promise<ISharedMap> {
72
+ const map = new SharedMap(id, runtime, attributes);
73
+ await map.load(services);
74
+
75
+ return map;
76
+ }
77
+
78
+ /**
79
+ * {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.create}
80
+ */
81
+ public create(runtime: IFluidDataStoreRuntime, id: string): ISharedMap {
82
+ const map = new SharedMap(id, runtime, MapFactory.Attributes);
83
+ map.initializeLocal();
84
+
85
+ return map;
86
+ }
92
87
  }
93
88
 
94
89
  /**
95
90
  * {@inheritDoc ISharedMap}
96
91
  */
97
92
  export class SharedMap extends SharedObject<ISharedMapEvents> implements ISharedMap {
98
- /**
99
- * Create a new shared map.
100
- * @param runtime - The data store runtime that the new shared map belongs to.
101
- * @param id - Optional name of the shared map.
102
- * @returns Newly created shared map.
103
- *
104
- * @example
105
- * To create a `SharedMap`, call the static create method:
106
- *
107
- * ```typescript
108
- * const myMap = SharedMap.create(this.runtime, id);
109
- * ```
110
- */
111
- public static create(runtime: IFluidDataStoreRuntime, id?: string): SharedMap {
112
- return runtime.createChannel(id, MapFactory.Type) as SharedMap;
113
- }
114
-
115
- /**
116
- * Get a factory for SharedMap to register with the data store.
117
- * @returns A factory that creates SharedMaps and loads them from storage.
118
- */
119
- public static getFactory(): IChannelFactory {
120
- return new MapFactory();
121
- }
122
-
123
- /**
124
- * String representation for the class.
125
- */
126
- public readonly [Symbol.toStringTag]: string = "SharedMap";
127
-
128
- /**
129
- * MapKernel which manages actual map operations.
130
- */
131
- private readonly kernel: MapKernel;
132
-
133
- /**
134
- * Do not call the constructor. Instead, you should use the {@link SharedMap.create | create method}.
135
- *
136
- * @param id - String identifier.
137
- * @param runtime - Data store runtime.
138
- * @param attributes - The attributes for the map.
139
- */
140
- constructor(
141
- id: string,
142
- runtime: IFluidDataStoreRuntime,
143
- attributes: IChannelAttributes,
144
- ) {
145
- super(id, runtime, attributes, "fluid_map_");
146
- this.kernel = new MapKernel(
147
- this.serializer,
148
- this.handle,
149
- (op, localOpMetadata) => this.submitLocalMessage(op, localOpMetadata),
150
- () => this.isAttached(),
151
- this,
152
- );
153
- }
154
-
155
- /**
156
- * Get an iterator over the keys in this map.
157
- * @returns The iterator
158
- */
159
- public keys(): IterableIterator<string> {
160
- return this.kernel.keys();
161
- }
162
-
163
- /**
164
- * Get an iterator over the entries in this map.
165
- * @returns The iterator
166
- */
167
- public entries(): IterableIterator<[string, any]> {
168
- return this.kernel.entries();
169
- }
170
-
171
- /**
172
- * Get an iterator over the values in this map.
173
- * @returns The iterator
174
- */
175
- public values(): IterableIterator<any> {
176
- return this.kernel.values();
177
- }
178
-
179
- /**
180
- * Get an iterator over the entries in this map.
181
- * @returns The iterator
182
- */
183
- public [Symbol.iterator](): IterableIterator<[string, any]> {
184
- return this.kernel.entries();
185
- }
186
-
187
- /**
188
- * The number of key/value pairs stored in the map.
189
- */
190
- public get size() {
191
- return this.kernel.size;
192
- }
193
-
194
- /**
195
- * Executes the given callback on each entry in the map.
196
- * @param callbackFn - Callback function
197
- */
198
- public forEach(callbackFn: (value: any, key: string, map: Map<string, any>) => void): void {
199
- this.kernel.forEach(callbackFn);
200
- }
201
-
202
- /**
203
- * {@inheritDoc ISharedMap.get}
204
- */
205
- public get<T = any>(key: string): T | undefined {
206
- return this.kernel.get<T>(key);
207
- }
208
-
209
- /**
210
- * Check if a key exists in the map.
211
- * @param key - The key to check
212
- * @returns True if the key exists, false otherwise
213
- */
214
- public has(key: string): boolean {
215
- return this.kernel.has(key);
216
- }
217
-
218
- /**
219
- * {@inheritDoc ISharedMap.set}
220
- */
221
- public set(key: string, value: any): this {
222
- this.kernel.set(key, value);
223
- return this;
224
- }
225
-
226
- /**
227
- * Delete a key from the map.
228
- * @param key - Key to delete
229
- * @returns True if the key existed and was deleted, false if it did not exist
230
- */
231
- public delete(key: string): boolean {
232
- return this.kernel.delete(key);
233
- }
234
-
235
- /**
236
- * Clear all data from the map.
237
- */
238
- public clear(): void {
239
- this.kernel.clear();
240
- }
241
-
242
- /**
243
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
244
- * @internal
245
- */
246
- protected summarizeCore(
247
- serializer: IFluidSerializer,
248
- telemetryContext?: ITelemetryContext,
249
- ): ISummaryTreeWithStats {
250
- let currentSize = 0;
251
- let counter = 0;
252
- let headerBlob: IMapDataObjectSerializable = {};
253
- const blobs: string[] = [];
254
-
255
- const builder = new SummaryTreeBuilder();
256
-
257
- const data = this.kernel.getSerializedStorage(serializer);
258
-
259
- // If single property exceeds this size, it goes into its own blob
260
- const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
261
-
262
- // Maximum blob size for multiple map properties
263
- // Should be bigger than MinValueSizeSeparateSnapshotBlob
264
- const MaxSnapshotBlobSize = 16 * 1024;
265
-
266
- // Partitioning algorithm:
267
- // 1) Split large (over MinValueSizeSeparateSnapshotBlob = 8K) properties into their own blobs.
268
- // Naming (across snapshots) of such blob does not have to be stable across snapshots,
269
- // As de-duping process (in driver) should not care about paths, only content.
270
- // 2) Split remaining properties into blobs of MaxSnapshotBlobSize (16K) size.
271
- // This process does not produce stable partitioning. This means
272
- // modification (including addition / deletion) of property can shift properties across blobs
273
- // and result in non-incremental snapshot.
274
- // This can be improved in the future, without being format breaking change, as loading sequence
275
- // loads all blobs at once and partitioning schema has no impact on that process.
276
- for (const key of Object.keys(data)) {
277
- const value = data[key];
278
- if (value.value && value.value.length >= MinValueSizeSeparateSnapshotBlob) {
279
- const blobName = `blob${counter}`;
280
- counter++;
281
- blobs.push(blobName);
282
- const content: IMapDataObjectSerializable = {
283
- [key]: {
284
- type: value.type,
285
- value: JSON.parse(value.value),
286
- },
287
- };
288
- builder.addBlob(blobName, JSON.stringify(content));
289
- } else {
290
- currentSize += value.type.length + 21; // Approximation cost of property header
291
- if (value.value) {
292
- currentSize += value.value.length;
293
- }
294
-
295
- if (currentSize > MaxSnapshotBlobSize) {
296
- const blobName = `blob${counter}`;
297
- counter++;
298
- blobs.push(blobName);
299
- builder.addBlob(blobName, JSON.stringify(headerBlob));
300
- headerBlob = {};
301
- currentSize = 0;
302
- }
303
- headerBlob[key] = {
304
- type: value.type,
305
- value: value.value === undefined ? undefined : JSON.parse(value.value),
306
- };
307
- }
308
- }
309
-
310
- const header: IMapSerializationFormat = {
311
- blobs,
312
- content: headerBlob,
313
- };
314
- builder.addBlob(snapshotFileName, JSON.stringify(header));
315
-
316
- return builder.getSummaryTree();
317
- }
318
-
319
- /**
320
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
321
- * @internal
322
- */
323
- protected async loadCore(storage: IChannelStorageService) {
324
- const json = await readAndParse<object>(storage, snapshotFileName);
325
- const newFormat = json as IMapSerializationFormat;
326
- if (Array.isArray(newFormat.blobs)) {
327
- this.kernel.populateFromSerializable(newFormat.content);
328
- await Promise.all(newFormat.blobs.map(async (value) => {
329
- const content = await readAndParse<IMapDataObjectSerializable>(storage, value);
330
- this.kernel.populateFromSerializable(content);
331
- }));
332
- } else {
333
- this.kernel.populateFromSerializable(json as IMapDataObjectSerializable);
334
- }
335
- }
336
-
337
- /**
338
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
339
- * @internal
340
- */
341
- protected onDisconnect() { }
342
-
343
- /**
344
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
345
- * @internal
346
- */
347
- protected reSubmitCore(content: any, localOpMetadata: unknown) {
348
- this.kernel.trySubmitMessage(content, localOpMetadata);
349
- }
350
-
351
- /**
352
- * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
353
- * @internal
354
- */
355
- protected applyStashedOp(content: any): unknown {
356
- return this.kernel.tryApplyStashedOp(content);
357
- }
358
-
359
- /**
360
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
361
- * @internal
362
- */
363
- protected processCore(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown) {
364
- if (message.type === MessageType.Operation) {
365
- this.kernel.tryProcessMessage(message.contents, local, localOpMetadata);
366
- }
367
- }
368
-
369
- /**
370
- * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
371
- * @internal
372
- */
373
- protected rollback(content: any, localOpMetadata: unknown) {
374
- this.kernel.rollback(content, localOpMetadata);
375
- }
93
+ /**
94
+ * Create a new shared map.
95
+ * @param runtime - The data store runtime that the new shared map belongs to.
96
+ * @param id - Optional name of the shared map.
97
+ * @returns Newly created shared map.
98
+ *
99
+ * @example
100
+ * To create a `SharedMap`, call the static create method:
101
+ *
102
+ * ```typescript
103
+ * const myMap = SharedMap.create(this.runtime, id);
104
+ * ```
105
+ */
106
+ public static create(runtime: IFluidDataStoreRuntime, id?: string): SharedMap {
107
+ return runtime.createChannel(id, MapFactory.Type) as SharedMap;
108
+ }
109
+
110
+ /**
111
+ * Get a factory for SharedMap to register with the data store.
112
+ * @returns A factory that creates SharedMaps and loads them from storage.
113
+ */
114
+ public static getFactory(): IChannelFactory {
115
+ return new MapFactory();
116
+ }
117
+
118
+ /**
119
+ * String representation for the class.
120
+ */
121
+ public readonly [Symbol.toStringTag]: string = "SharedMap";
122
+
123
+ /**
124
+ * MapKernel which manages actual map operations.
125
+ */
126
+ private readonly kernel: MapKernel;
127
+
128
+ /**
129
+ * Do not call the constructor. Instead, you should use the {@link SharedMap.create | create method}.
130
+ *
131
+ * @param id - String identifier.
132
+ * @param runtime - Data store runtime.
133
+ * @param attributes - The attributes for the map.
134
+ */
135
+ public constructor(
136
+ id: string,
137
+ runtime: IFluidDataStoreRuntime,
138
+ attributes: IChannelAttributes,
139
+ ) {
140
+ super(id, runtime, attributes, "fluid_map_");
141
+ this.kernel = new MapKernel(
142
+ this.serializer,
143
+ this.handle,
144
+ (op, localOpMetadata) => this.submitLocalMessage(op, localOpMetadata),
145
+ () => this.isAttached(),
146
+ this,
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Get an iterator over the keys in this map.
152
+ * @returns The iterator
153
+ */
154
+ public keys(): IterableIterator<string> {
155
+ return this.kernel.keys();
156
+ }
157
+
158
+ /**
159
+ * Get an iterator over the entries in this map.
160
+ * @returns The iterator
161
+ */
162
+ // TODO: Use `unknown` instead (breaking change).
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ public entries(): IterableIterator<[string, any]> {
165
+ return this.kernel.entries();
166
+ }
167
+
168
+ /**
169
+ * Get an iterator over the values in this map.
170
+ * @returns The iterator
171
+ */
172
+ // TODO: Use `unknown` instead (breaking change).
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ public values(): IterableIterator<any> {
175
+ return this.kernel.values();
176
+ }
177
+
178
+ /**
179
+ * Get an iterator over the entries in this map.
180
+ * @returns The iterator
181
+ */
182
+ // TODO: Use `unknown` instead (breaking change).
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ public [Symbol.iterator](): IterableIterator<[string, any]> {
185
+ return this.kernel.entries();
186
+ }
187
+
188
+ /**
189
+ * The number of key/value pairs stored in the map.
190
+ */
191
+ public get size(): number {
192
+ return this.kernel.size;
193
+ }
194
+
195
+ /**
196
+ * Executes the given callback on each entry in the map.
197
+ * @param callbackFn - Callback function
198
+ */
199
+ // TODO: Use `unknown` instead (breaking change).
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ public forEach(callbackFn: (value: any, key: string, map: Map<string, any>) => void): void {
202
+ // eslint-disable-next-line unicorn/no-array-for-each, unicorn/no-array-callback-reference
203
+ this.kernel.forEach(callbackFn);
204
+ }
205
+
206
+ /**
207
+ * {@inheritDoc ISharedMap.get}
208
+ */
209
+ // TODO: Use `unknown` instead (breaking change).
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
+ public get<T = any>(key: string): T | undefined {
212
+ return this.kernel.get<T>(key);
213
+ }
214
+
215
+ /**
216
+ * Check if a key exists in the map.
217
+ * @param key - The key to check
218
+ * @returns True if the key exists, false otherwise
219
+ */
220
+ public has(key: string): boolean {
221
+ return this.kernel.has(key);
222
+ }
223
+
224
+ /**
225
+ * {@inheritDoc ISharedMap.set}
226
+ */
227
+ public set(key: string, value: unknown): this {
228
+ this.kernel.set(key, value);
229
+ return this;
230
+ }
231
+
232
+ /**
233
+ * Delete a key from the map.
234
+ * @param key - Key to delete
235
+ * @returns True if the key existed and was deleted, false if it did not exist
236
+ */
237
+ public delete(key: string): boolean {
238
+ return this.kernel.delete(key);
239
+ }
240
+
241
+ /**
242
+ * Clear all data from the map.
243
+ */
244
+ public clear(): void {
245
+ this.kernel.clear();
246
+ }
247
+
248
+ /**
249
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.summarizeCore}
250
+ * @internal
251
+ */
252
+ protected summarizeCore(
253
+ serializer: IFluidSerializer,
254
+ telemetryContext?: ITelemetryContext,
255
+ ): ISummaryTreeWithStats {
256
+ let currentSize = 0;
257
+ let counter = 0;
258
+ let headerBlob: IMapDataObjectSerializable = {};
259
+ const blobs: string[] = [];
260
+
261
+ const builder = new SummaryTreeBuilder();
262
+
263
+ const data = this.kernel.getSerializedStorage(serializer);
264
+
265
+ // If single property exceeds this size, it goes into its own blob
266
+ const MinValueSizeSeparateSnapshotBlob = 8 * 1024;
267
+
268
+ // Maximum blob size for multiple map properties
269
+ // Should be bigger than MinValueSizeSeparateSnapshotBlob
270
+ const MaxSnapshotBlobSize = 16 * 1024;
271
+
272
+ // Partitioning algorithm:
273
+ // 1) Split large (over MinValueSizeSeparateSnapshotBlob = 8K) properties into their own blobs.
274
+ // Naming (across snapshots) of such blob does not have to be stable across snapshots,
275
+ // As de-duping process (in driver) should not care about paths, only content.
276
+ // 2) Split remaining properties into blobs of MaxSnapshotBlobSize (16K) size.
277
+ // This process does not produce stable partitioning. This means
278
+ // modification (including addition / deletion) of property can shift properties across blobs
279
+ // and result in non-incremental snapshot.
280
+ // This can be improved in the future, without being format breaking change, as loading sequence
281
+ // loads all blobs at once and partitioning schema has no impact on that process.
282
+ for (const key of Object.keys(data)) {
283
+ const value = data[key];
284
+ if (value.value && value.value.length >= MinValueSizeSeparateSnapshotBlob) {
285
+ const blobName = `blob${counter}`;
286
+ counter++;
287
+ blobs.push(blobName);
288
+ const content: IMapDataObjectSerializable = {
289
+ [key]: {
290
+ type: value.type,
291
+ value: JSON.parse(value.value) as unknown,
292
+ },
293
+ };
294
+ builder.addBlob(blobName, JSON.stringify(content));
295
+ } else {
296
+ currentSize += value.type.length + 21; // Approximation cost of property header
297
+ if (value.value) {
298
+ currentSize += value.value.length;
299
+ }
300
+
301
+ if (currentSize > MaxSnapshotBlobSize) {
302
+ const blobName = `blob${counter}`;
303
+ counter++;
304
+ blobs.push(blobName);
305
+ builder.addBlob(blobName, JSON.stringify(headerBlob));
306
+ headerBlob = {};
307
+ currentSize = 0;
308
+ }
309
+ headerBlob[key] = {
310
+ type: value.type,
311
+ value:
312
+ value.value === undefined
313
+ ? undefined
314
+ : (JSON.parse(value.value) as unknown),
315
+ };
316
+ }
317
+ }
318
+
319
+ const header: IMapSerializationFormat = {
320
+ blobs,
321
+ content: headerBlob,
322
+ };
323
+ builder.addBlob(snapshotFileName, JSON.stringify(header));
324
+
325
+ return builder.getSummaryTree();
326
+ }
327
+
328
+ /**
329
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
330
+ * @internal
331
+ */
332
+ protected async loadCore(storage: IChannelStorageService): Promise<void> {
333
+ const json = await readAndParse<object>(storage, snapshotFileName);
334
+ const newFormat = json as IMapSerializationFormat;
335
+ if (Array.isArray(newFormat.blobs)) {
336
+ this.kernel.populateFromSerializable(newFormat.content);
337
+ await Promise.all(
338
+ newFormat.blobs.map(async (value) => {
339
+ const content = await readAndParse<IMapDataObjectSerializable>(storage, value);
340
+ this.kernel.populateFromSerializable(content);
341
+ }),
342
+ );
343
+ } else {
344
+ this.kernel.populateFromSerializable(json as IMapDataObjectSerializable);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.onDisconnect}
350
+ * @internal
351
+ */
352
+ protected onDisconnect(): void {}
353
+
354
+ /**
355
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.reSubmitCore}
356
+ * @internal
357
+ */
358
+ protected reSubmitCore(content: unknown, localOpMetadata: unknown): void {
359
+ this.kernel.trySubmitMessage(content as IMapOperation, localOpMetadata);
360
+ }
361
+
362
+ /**
363
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.applyStashedOp}
364
+ * @internal
365
+ */
366
+ protected applyStashedOp(content: unknown): unknown {
367
+ return this.kernel.tryApplyStashedOp(content as IMapOperation);
368
+ }
369
+
370
+ /**
371
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.processCore}
372
+ * @internal
373
+ */
374
+ protected processCore(
375
+ message: ISequencedDocumentMessage,
376
+ local: boolean,
377
+ localOpMetadata: unknown,
378
+ ): void {
379
+ if (message.type === MessageType.Operation) {
380
+ this.kernel.tryProcessMessage(
381
+ message.contents as IMapOperation,
382
+ local,
383
+ localOpMetadata,
384
+ );
385
+ }
386
+ }
387
+
388
+ /**
389
+ * {@inheritDoc @fluidframework/shared-object-base#SharedObject.rollback}
390
+ * @internal
391
+ */
392
+ protected rollback(content: unknown, localOpMetadata: unknown): void {
393
+ this.kernel.rollback(content, localOpMetadata);
394
+ }
376
395
  }