@fluidframework/driver-utils 2.0.0-internal.3.0.5 → 2.0.0-internal.3.1.1

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 (155) hide show
  1. package/.eslintrc.js +17 -19
  2. package/.mocharc.js +2 -2
  3. package/api-extractor.json +2 -2
  4. package/dist/blobAggregationStorage.d.ts +2 -2
  5. package/dist/blobAggregationStorage.d.ts.map +1 -1
  6. package/dist/blobAggregationStorage.js +11 -6
  7. package/dist/blobAggregationStorage.js.map +1 -1
  8. package/dist/blobCacheStorageService.d.ts.map +1 -1
  9. package/dist/blobCacheStorageService.js.map +1 -1
  10. package/dist/buildSnapshotTree.d.ts.map +1 -1
  11. package/dist/buildSnapshotTree.js.map +1 -1
  12. package/dist/documentStorageServiceProxy.d.ts.map +1 -1
  13. package/dist/documentStorageServiceProxy.js.map +1 -1
  14. package/dist/emptyDocumentDeltaStorageService.d.ts.map +1 -1
  15. package/dist/emptyDocumentDeltaStorageService.js.map +1 -1
  16. package/dist/error.d.ts.map +1 -1
  17. package/dist/error.js.map +1 -1
  18. package/dist/fluidResolvedUrl.d.ts.map +1 -1
  19. package/dist/fluidResolvedUrl.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/insecureUrlResolver.d.ts.map +1 -1
  24. package/dist/insecureUrlResolver.js +2 -1
  25. package/dist/insecureUrlResolver.js.map +1 -1
  26. package/dist/mapWithExpiration.d.ts.map +1 -1
  27. package/dist/mapWithExpiration.js +5 -3
  28. package/dist/mapWithExpiration.js.map +1 -1
  29. package/dist/messageRecognition.d.ts.map +1 -1
  30. package/dist/messageRecognition.js.map +1 -1
  31. package/dist/multiDocumentServiceFactory.d.ts.map +1 -1
  32. package/dist/multiDocumentServiceFactory.js.map +1 -1
  33. package/dist/multiUrlResolver.d.ts.map +1 -1
  34. package/dist/multiUrlResolver.js.map +1 -1
  35. package/dist/network.d.ts +1 -1
  36. package/dist/network.d.ts.map +1 -1
  37. package/dist/network.js +4 -3
  38. package/dist/network.js.map +1 -1
  39. package/dist/networkUtils.d.ts.map +1 -1
  40. package/dist/networkUtils.js +2 -3
  41. package/dist/networkUtils.js.map +1 -1
  42. package/dist/packageVersion.d.ts +1 -1
  43. package/dist/packageVersion.js +1 -1
  44. package/dist/packageVersion.js.map +1 -1
  45. package/dist/parallelRequests.d.ts.map +1 -1
  46. package/dist/parallelRequests.js +19 -9
  47. package/dist/parallelRequests.js.map +1 -1
  48. package/dist/prefetchDocumentStorageService.d.ts.map +1 -1
  49. package/dist/prefetchDocumentStorageService.js.map +1 -1
  50. package/dist/rateLimiter.d.ts.map +1 -1
  51. package/dist/rateLimiter.js.map +1 -1
  52. package/dist/readAndParse.d.ts.map +1 -1
  53. package/dist/readAndParse.js.map +1 -1
  54. package/dist/runWithRetry.d.ts.map +1 -1
  55. package/dist/runWithRetry.js.map +1 -1
  56. package/dist/summaryForCreateNew.d.ts.map +1 -1
  57. package/dist/summaryForCreateNew.js.map +1 -1
  58. package/dist/treeConversions.d.ts.map +1 -1
  59. package/dist/treeConversions.js +1 -4
  60. package/dist/treeConversions.js.map +1 -1
  61. package/dist/treeUtils.d.ts +10 -10
  62. package/dist/treeUtils.d.ts.map +1 -1
  63. package/dist/treeUtils.js +10 -10
  64. package/dist/treeUtils.js.map +1 -1
  65. package/lib/blobAggregationStorage.d.ts +2 -2
  66. package/lib/blobAggregationStorage.d.ts.map +1 -1
  67. package/lib/blobAggregationStorage.js +11 -6
  68. package/lib/blobAggregationStorage.js.map +1 -1
  69. package/lib/blobCacheStorageService.d.ts.map +1 -1
  70. package/lib/blobCacheStorageService.js.map +1 -1
  71. package/lib/buildSnapshotTree.d.ts.map +1 -1
  72. package/lib/buildSnapshotTree.js.map +1 -1
  73. package/lib/documentStorageServiceProxy.d.ts.map +1 -1
  74. package/lib/documentStorageServiceProxy.js.map +1 -1
  75. package/lib/emptyDocumentDeltaStorageService.d.ts.map +1 -1
  76. package/lib/emptyDocumentDeltaStorageService.js.map +1 -1
  77. package/lib/error.d.ts.map +1 -1
  78. package/lib/error.js.map +1 -1
  79. package/lib/fluidResolvedUrl.d.ts.map +1 -1
  80. package/lib/fluidResolvedUrl.js.map +1 -1
  81. package/lib/index.d.ts +1 -1
  82. package/lib/index.d.ts.map +1 -1
  83. package/lib/index.js +1 -1
  84. package/lib/index.js.map +1 -1
  85. package/lib/insecureUrlResolver.d.ts.map +1 -1
  86. package/lib/insecureUrlResolver.js +2 -1
  87. package/lib/insecureUrlResolver.js.map +1 -1
  88. package/lib/mapWithExpiration.d.ts.map +1 -1
  89. package/lib/mapWithExpiration.js +5 -3
  90. package/lib/mapWithExpiration.js.map +1 -1
  91. package/lib/messageRecognition.d.ts.map +1 -1
  92. package/lib/messageRecognition.js +1 -1
  93. package/lib/messageRecognition.js.map +1 -1
  94. package/lib/multiDocumentServiceFactory.d.ts.map +1 -1
  95. package/lib/multiDocumentServiceFactory.js.map +1 -1
  96. package/lib/multiUrlResolver.d.ts.map +1 -1
  97. package/lib/multiUrlResolver.js.map +1 -1
  98. package/lib/network.d.ts +1 -1
  99. package/lib/network.d.ts.map +1 -1
  100. package/lib/network.js +4 -3
  101. package/lib/network.js.map +1 -1
  102. package/lib/networkUtils.d.ts.map +1 -1
  103. package/lib/networkUtils.js +2 -3
  104. package/lib/networkUtils.js.map +1 -1
  105. package/lib/packageVersion.d.ts +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/parallelRequests.d.ts.map +1 -1
  109. package/lib/parallelRequests.js +19 -9
  110. package/lib/parallelRequests.js.map +1 -1
  111. package/lib/prefetchDocumentStorageService.d.ts.map +1 -1
  112. package/lib/prefetchDocumentStorageService.js.map +1 -1
  113. package/lib/rateLimiter.d.ts.map +1 -1
  114. package/lib/rateLimiter.js.map +1 -1
  115. package/lib/readAndParse.d.ts.map +1 -1
  116. package/lib/readAndParse.js.map +1 -1
  117. package/lib/runWithRetry.d.ts.map +1 -1
  118. package/lib/runWithRetry.js.map +1 -1
  119. package/lib/summaryForCreateNew.d.ts.map +1 -1
  120. package/lib/summaryForCreateNew.js.map +1 -1
  121. package/lib/treeConversions.d.ts.map +1 -1
  122. package/lib/treeConversions.js +4 -7
  123. package/lib/treeConversions.js.map +1 -1
  124. package/lib/treeUtils.d.ts +10 -10
  125. package/lib/treeUtils.d.ts.map +1 -1
  126. package/lib/treeUtils.js +11 -11
  127. package/lib/treeUtils.js.map +1 -1
  128. package/package.json +128 -127
  129. package/prettier.config.cjs +1 -1
  130. package/src/blobAggregationStorage.ts +374 -322
  131. package/src/blobCacheStorageService.ts +20 -17
  132. package/src/buildSnapshotTree.ts +54 -51
  133. package/src/documentStorageServiceProxy.ts +49 -43
  134. package/src/emptyDocumentDeltaStorageService.ts +11 -10
  135. package/src/error.ts +5 -7
  136. package/src/fluidResolvedUrl.ts +9 -6
  137. package/src/index.ts +5 -1
  138. package/src/insecureUrlResolver.ts +127 -116
  139. package/src/mapWithExpiration.ts +111 -104
  140. package/src/messageRecognition.ts +25 -19
  141. package/src/multiDocumentServiceFactory.ts +73 -62
  142. package/src/multiUrlResolver.ts +26 -29
  143. package/src/network.ts +114 -112
  144. package/src/networkUtils.ts +37 -34
  145. package/src/packageVersion.ts +1 -1
  146. package/src/parallelRequests.ts +571 -509
  147. package/src/prefetchDocumentStorageService.ts +76 -74
  148. package/src/rateLimiter.ts +29 -29
  149. package/src/readAndParse.ts +7 -4
  150. package/src/runWithRetry.ts +105 -94
  151. package/src/summaryForCreateNew.ts +27 -24
  152. package/src/treeConversions.ts +48 -70
  153. package/src/treeUtils.ts +70 -74
  154. package/tsconfig.esnext.json +6 -6
  155. package/tsconfig.json +8 -12
@@ -4,61 +4,63 @@
4
4
  */
5
5
 
6
6
  import {
7
- IDocumentStorageService,
8
- IDocumentStorageServicePolicies,
9
- ISummaryContext,
7
+ FetchSource,
8
+ IDocumentStorageService,
9
+ IDocumentStorageServicePolicies,
10
+ ISummaryContext,
10
11
  } from "@fluidframework/driver-definitions";
11
12
  import {
12
- ICreateBlobResponse,
13
- ISnapshotTree,
14
- ISummaryHandle,
15
- ISummaryTree,
16
- IVersion,
17
- SummaryType,
13
+ ICreateBlobResponse,
14
+ ISnapshotTree,
15
+ ISummaryHandle,
16
+ ISummaryTree,
17
+ IVersion,
18
+ SummaryType,
18
19
  } from "@fluidframework/protocol-definitions";
19
20
  import {
20
- assert,
21
- bufferToString,
22
- stringToBuffer,
23
- unreachableCase,
24
- fromUtf8ToBase64,
25
- Uint8ArrayToString,
26
- } from "@fluidframework/common-utils";
21
+ assert,
22
+ bufferToString,
23
+ stringToBuffer,
24
+ unreachableCase,
25
+ fromUtf8ToBase64,
26
+ Uint8ArrayToString,
27
+ } from "@fluidframework/common-utils";
27
28
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
28
29
  import { loggerToMonitoringContext } from "@fluidframework/telemetry-utils";
29
30
 
30
31
  /*
31
32
  * Work around for bufferToString having a bug - it can't consume IsoBuffer!
32
33
  * To be removed once bufferToString is fixed!
33
- */
34
+ */
34
35
  function bufferToString2(blob: ArrayBufferLike, encoding: "utf-8" | "base64"): string {
35
- if (blob instanceof Uint8Array) { // IsoBuffer does not have ctor, so it's not in proto chain :(
36
- return Uint8ArrayToString(blob, encoding);
37
- }
38
- return bufferToString(blob, encoding);
36
+ if (blob instanceof Uint8Array) {
37
+ // IsoBuffer does not have ctor, so it's not in proto chain :(
38
+ return Uint8ArrayToString(blob, encoding);
39
+ }
40
+ return bufferToString(blob, encoding);
39
41
  }
40
42
 
41
- /**
42
- * Class responsible for aggregating smaller blobs into one and unpacking it later on.
43
- */
43
+ /**
44
+ * Class responsible for aggregating smaller blobs into one and unpacking it later on.
45
+ */
44
46
  class BlobAggregator {
45
- private readonly content: [string, string][] = [];
46
-
47
- public addBlob(key: string, content: string) {
48
- this.content.push([key, content]);
49
- }
50
-
51
- public getAggregatedBlobContent() {
52
- if (this.content.length === 0) {
53
- return undefined;
54
- }
55
- return JSON.stringify(this.content);
56
- }
57
-
58
- static load(input: ArrayBufferLike) {
59
- const data = bufferToString2(input, "utf-8");
60
- return JSON.parse(data) as [string, string][];
61
- }
47
+ private readonly content: [string, string][] = [];
48
+
49
+ public addBlob(key: string, content: string) {
50
+ this.content.push([key, content]);
51
+ }
52
+
53
+ public getAggregatedBlobContent() {
54
+ if (this.content.length === 0) {
55
+ return undefined;
56
+ }
57
+ return JSON.stringify(this.content);
58
+ }
59
+
60
+ static load(input: ArrayBufferLike) {
61
+ const data = bufferToString2(input, "utf-8");
62
+ return JSON.parse(data) as [string, string][];
63
+ }
62
64
  }
63
65
 
64
66
  /*
@@ -66,51 +68,56 @@ class BlobAggregator {
66
68
  * It relies on abstract methods for reads and storing unpacked blobs.
67
69
  */
68
70
  export abstract class SnapshotExtractor {
69
- protected readonly aggregatedBlobName = "__big";
70
- protected readonly virtualIdPrefix = "__";
71
-
72
- // counter for generation of virtual storage IDs
73
- protected virtualIdCounter = 0;
74
- protected getNextVirtualId() {
75
- return `${this.virtualIdPrefix}${++this.virtualIdCounter}`;
76
- }
77
-
78
- abstract getBlob(id: string, tree: ISnapshotTree): Promise<ArrayBufferLike>;
79
- abstract setBlob(id: string, tree: ISnapshotTree, content: string);
80
-
81
- public async unpackSnapshotCore(snapshot: ISnapshotTree, level = 0): Promise<void> {
82
- for (const key of Object.keys(snapshot.trees)) {
83
- const obj = snapshot.trees[key];
84
- await this.unpackSnapshotCore(obj, level + 1);
85
- }
86
-
87
- // For future proof, we will support multiple aggregated blobs with any name
88
- // that starts with this.aggregatedBlobName
89
- for (const key of Object.keys(snapshot.blobs)) {
90
- if (!key.startsWith(this.aggregatedBlobName)) { continue; }
91
- const blobId = snapshot.blobs[key];
92
- if (blobId !== undefined) {
93
- const blob = await this.getBlob(blobId, snapshot);
94
- for (const [path, value] of BlobAggregator.load(blob)) {
95
- const id = this.getNextVirtualId();
96
- this.setBlob(id, snapshot, value);
97
- const pathSplit = path.split("/");
98
- let subTree = snapshot;
99
- for (const subPath of pathSplit.slice(0, pathSplit.length - 1)) {
100
- if (subTree.trees[subPath] === undefined) {
101
- subTree.trees[subPath] = { blobs: {}, trees: {} };
102
- }
103
- subTree = subTree.trees[subPath];
104
- }
105
- const blobName = pathSplit[pathSplit.length - 1];
106
- assert(subTree.blobs[blobName] === undefined, 0x0f6 /* "real blob ID exists" */);
107
- subTree.blobs[blobName] = id;
108
- }
109
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
110
- delete snapshot.blobs[this.aggregatedBlobName];
111
- }
112
- }
113
- }
71
+ protected readonly aggregatedBlobName = "__big";
72
+ protected readonly virtualIdPrefix = "__";
73
+
74
+ // counter for generation of virtual storage IDs
75
+ protected virtualIdCounter = 0;
76
+ protected getNextVirtualId() {
77
+ return `${this.virtualIdPrefix}${++this.virtualIdCounter}`;
78
+ }
79
+
80
+ abstract getBlob(id: string, tree: ISnapshotTree): Promise<ArrayBufferLike>;
81
+ abstract setBlob(id: string, tree: ISnapshotTree, content: string);
82
+
83
+ public async unpackSnapshotCore(snapshot: ISnapshotTree, level = 0): Promise<void> {
84
+ for (const key of Object.keys(snapshot.trees)) {
85
+ const obj = snapshot.trees[key];
86
+ await this.unpackSnapshotCore(obj, level + 1);
87
+ }
88
+
89
+ // For future proof, we will support multiple aggregated blobs with any name
90
+ // that starts with this.aggregatedBlobName
91
+ for (const key of Object.keys(snapshot.blobs)) {
92
+ if (!key.startsWith(this.aggregatedBlobName)) {
93
+ continue;
94
+ }
95
+ const blobId = snapshot.blobs[key];
96
+ if (blobId !== undefined) {
97
+ const blob = await this.getBlob(blobId, snapshot);
98
+ for (const [path, value] of BlobAggregator.load(blob)) {
99
+ const id = this.getNextVirtualId();
100
+ this.setBlob(id, snapshot, value);
101
+ const pathSplit = path.split("/");
102
+ let subTree = snapshot;
103
+ for (const subPath of pathSplit.slice(0, pathSplit.length - 1)) {
104
+ if (subTree.trees[subPath] === undefined) {
105
+ subTree.trees[subPath] = { blobs: {}, trees: {} };
106
+ }
107
+ subTree = subTree.trees[subPath];
108
+ }
109
+ const blobName = pathSplit[pathSplit.length - 1];
110
+ assert(
111
+ subTree.blobs[blobName] === undefined,
112
+ 0x0f6 /* "real blob ID exists" */,
113
+ );
114
+ subTree.blobs[blobName] = id;
115
+ }
116
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
117
+ delete snapshot.blobs[this.aggregatedBlobName];
118
+ }
119
+ }
120
+ }
114
121
  }
115
122
 
116
123
  /*
@@ -127,16 +134,19 @@ export abstract class SnapshotExtractor {
127
134
  * fixing existing flows to allow switching of storage.
128
135
  */
129
136
  class SnapshotExtractorInPlace extends SnapshotExtractor {
130
- public async getBlob(id: string, tree: ISnapshotTree): Promise<ArrayBufferLike> {
131
- const blob = tree.blobs[id];
132
- assert(blob !== undefined, 0x0f7 /* "aggregate blob missing" */);
133
- return stringToBuffer(blob, "base64");
134
- }
135
-
136
- public setBlob(id: string, tree: ISnapshotTree, content: string) {
137
- assert(tree.blobs[id] === undefined, 0x0f8 /* "blob from aggregate blob exists on its own" */);
138
- tree.blobs[id] = fromUtf8ToBase64(content);
139
- }
137
+ public async getBlob(id: string, tree: ISnapshotTree): Promise<ArrayBufferLike> {
138
+ const blob = tree.blobs[id];
139
+ assert(blob !== undefined, 0x0f7 /* "aggregate blob missing" */);
140
+ return stringToBuffer(blob, "base64");
141
+ }
142
+
143
+ public setBlob(id: string, tree: ISnapshotTree, content: string) {
144
+ assert(
145
+ tree.blobs[id] === undefined,
146
+ 0x0f8 /* "blob from aggregate blob exists on its own" */,
147
+ );
148
+ tree.blobs[id] = fromUtf8ToBase64(content);
149
+ }
140
150
  }
141
151
 
142
152
  /*
@@ -145,230 +155,272 @@ class SnapshotExtractorInPlace extends SnapshotExtractor {
145
155
  * When snapshot is read, it will unpack aggregated blobs and provide them transparently to caller.
146
156
  */
147
157
  export class BlobAggregationStorage extends SnapshotExtractor implements IDocumentStorageService {
148
- // Tells data store if it can use incremental summary (i.e. reuse DDSes from previous summary
149
- // when only one DDS changed).
150
- // The answer has to be know long before we enable actual packing. The reason for the is the following:
151
- // A the moment when we enable packing, we should assume that all clients out there wil already have bits
152
- // that can unpack properly (i.e. enough time passed since we deployed bits that can unpack)
153
- // But we can still have clients where some of them already pack, and some do not. If one summary was
154
- // using packing, then it relies on non-incremental summaries going forward, even if next client who
155
- // produced summary is not packing!
156
- // This can have slight improvement by enabling it per file (based on "did summary we loaded from contain
157
- // aggregated blobs"), but that's harder to make reliable, so going for simplicity.
158
- static readonly fullDataStoreSummaries = true;
159
-
160
- protected loadedFromSummary = false;
161
-
162
- protected virtualBlobs = new Map<string, ArrayBufferLike>();
163
-
164
- static wrap(
165
- storage: IDocumentStorageService,
166
- logger: ITelemetryLogger,
167
- allowPacking?: boolean,
168
- packingLevel = 2,
169
- ) {
170
- if (storage instanceof BlobAggregationStorage) {
171
- return storage;
172
- }
173
- const mc = loggerToMonitoringContext(logger);
174
- const realAllowPackaging = mc.config.getBoolean("FluidAggregateBlobs") ?? allowPacking ?? false;
175
-
176
- // Always create BlobAggregationStorage even if storage is not asking for packing.
177
- // This is mostly to avoid cases where future changes in policy would result in inability to
178
- // load old files that were created with aggregation on.
179
- const minBlobSize = storage.policies?.minBlobSize;
180
- return new BlobAggregationStorage(storage, logger, realAllowPackaging, packingLevel, minBlobSize);
181
- }
182
-
183
- static async unpackSnapshot(snapshot: ISnapshotTree) {
184
- const converter = new SnapshotExtractorInPlace();
185
- await converter.unpackSnapshotCore(snapshot);
186
- }
187
-
188
- public get policies(): IDocumentStorageServicePolicies | undefined {
189
- const policies = this.storage.policies;
190
- if (policies) {
191
- return { ...policies, minBlobSize: undefined };
192
- }
193
- }
194
-
195
- public async unpackSnapshot(snapshot: ISnapshotTree) {
196
- // SummarizerNodeWithGC.refreshLatestSummary can call it when this.loadedFromSummary === false
197
- // (I assumed after file was created)
198
- // assert(!this.loadedFromSummary, "unpack without summary");
199
-
200
- this.loadedFromSummary = true;
201
- await this.unpackSnapshotCore(snapshot);
202
- }
203
-
204
- protected constructor(
205
- private readonly storage: IDocumentStorageService,
206
- private readonly logger: ITelemetryLogger,
207
- private readonly allowPacking: boolean,
208
- private readonly packingLevel: number,
209
- private readonly blobCutOffSize?: number) {
210
- super();
211
- }
212
-
213
- public setBlob(id: string, tree: ISnapshotTree, content: string) {
214
- this.virtualBlobs.set(id, stringToBuffer(content, "utf-8"));
215
- }
216
-
217
- public async getBlob(id: string, tree: ISnapshotTree): Promise<ArrayBufferLike> {
218
- return this.readBlob(id).catch((error) => {
219
- this.logger.sendErrorEvent({ eventName: "BlobDedupNoAggregateBlob" }, error);
220
- throw error;
221
- });
222
- }
223
-
224
- public get repositoryUrl() { return this.storage.repositoryUrl; }
225
- public async getVersions(versionId: string | null, count: number) {
226
- return this.storage.getVersions(versionId, count);
227
- }
228
-
229
- public async downloadSummary(handle: ISummaryHandle): Promise<ISummaryTree> {
230
- throw new Error("NYI");
231
- }
232
-
233
- // for now we are not optimizing these blobs, with assumption that this API is used only
234
- // for big blobs (images)
235
- public async createBlob(file: ArrayBufferLike): Promise<ICreateBlobResponse> {
236
- return this.storage.createBlob(file);
237
- }
238
-
239
- public async getSnapshotTree(version?: IVersion): Promise<ISnapshotTree | null> {
240
- const tree = await this.storage.getSnapshotTree(version);
241
- if (tree) {
242
- await this.unpackSnapshot(tree);
243
- }
244
- return tree;
245
- }
246
-
247
- public async readBlob(id: string): Promise<ArrayBufferLike> {
248
- if (this.isRealStorageId(id)) {
249
- return this.storage.readBlob(id);
250
- }
251
- // We support only reading blobs from the summary we loaded from.
252
- // This may need to be extended to any general summary in the future as runtime usage pattern
253
- // of storage changes (for example, data stores start to load from recent summary, not from original
254
- // summary whole container loaded from)
255
-
256
- // are there other ways we can get here? createFile is one flow, but we should not be reading blobs
257
- // in such flow
258
- assert(this.loadedFromSummary, 0x0f9 /* "never read summary" */);
259
- const blob = this.virtualBlobs.get(id);
260
- assert(blob !== undefined, 0x0fa /* "virtual blob not found" */);
261
- return blob;
262
- }
263
-
264
- public async uploadSummaryWithContext(summary: ISummaryTree, context: ISummaryContext): Promise<string> {
265
- const summaryNew = this.allowPacking ? await this.compressSmallBlobs(summary) : summary;
266
- return this.storage.uploadSummaryWithContext(summaryNew, context);
267
- }
268
-
269
- // For simplification, we assume that
270
- // - blob aggregation is done at data store level only for now
271
- // - data store either reuses previous summary, or generates full summary, i.e. there is no partial (some DDS)
272
- // summary produced by data stores.
273
- // These simplifications allow us not to touch handles, as they are self-contained (either do not use aggregated
274
- // blob Or contain aggregated blob that stays relevant for that sub-tree)
275
- // Note:
276
- // From perf perspective, it makes sense to place aggregated blobs one level up in the tree not to create extra
277
- // tree nodes (i.e. have shallow tree with less edges). But that creates problems with reusability of trees at
278
- // incremental summary time - we would need to understand handles and parse them. In current design we can skip
279
- // that step because if data store is reused, the hole sub-tree is reused included aggregated blob embedded into it
280
- // and that means we can do nothing and be correct!
281
- private async compressSmallBlobs(
282
- summary: ISummaryTree,
283
- path = "",
284
- level = 0,
285
- aggregatorArg?: BlobAggregator): Promise<ISummaryTree> {
286
- if (this.blobCutOffSize === undefined || this.blobCutOffSize < 0) {
287
- return summary;
288
- }
289
-
290
- let shouldCompress: boolean = false;
291
-
292
- let aggregator = aggregatorArg;
293
- // checking if this is a dataStore tree, since we only pack at data store level
294
- if (Object.keys(summary.tree).includes(".component")) {
295
- assert(aggregator === undefined, 0x0fb /* "logic err with aggregator" */);
296
- assert(level === this.packingLevel, 0x23b /* "we are not packing at the right level" */);
297
- aggregator = new BlobAggregator();
298
- shouldCompress = true;
299
- } else {
300
- assert(level !== this.packingLevel, 0x23c /* "we are not packing at the right level" */);
301
- }
302
-
303
- const newSummary: ISummaryTree = { ...summary };
304
- newSummary.tree = { ...newSummary.tree };
305
- for (const key of Object.keys(summary.tree)) {
306
- const obj = summary.tree[key];
307
- // Get path relative to root of data store (where we do aggregation)
308
- const newPath = shouldCompress ? key : `${path}/${key}`;
309
- switch (obj.type) {
310
- case SummaryType.Tree:
311
- // If client created empty tree, keep it as is
312
- // Also do not package search blobs - they are part of storage contract
313
- if (obj.tree !== {} && key !== "__search") {
314
- const tree = await this.compressSmallBlobs(obj, newPath, level + 1, aggregator);
315
- newSummary.tree[key] = tree;
316
- if (tree.tree === {}) {
317
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
318
- delete newSummary.tree[key];
319
- }
320
- }
321
- break;
322
- case SummaryType.Blob:
323
- if (aggregator && typeof obj.content == "string" && obj.content.length < this.blobCutOffSize) {
324
- aggregator.addBlob(newPath, obj.content);
325
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
326
- delete newSummary.tree[key];
327
- }
328
- break;
329
- case SummaryType.Handle: {
330
- // Would be nice to:
331
- // Trees: expand the tree
332
- // Blobs: parse handle and ensure it points to real blob, not virtual blob.
333
- // We can avoid it for now given data store is the granularity of incremental summaries.
334
- let handlePath = obj.handle;
335
- if (handlePath.startsWith("/")) {
336
- handlePath = handlePath.substr(1);
337
- }
338
- // Ensure only whole data stores can be reused, no reusing at deeper level!
339
- assert(level === 0, 0x0fc /* "tree reuse at lower level" */);
340
- assert(!handlePath.includes("/"),
341
- 0x0fd /* "data stores are writing incremental summaries!" */);
342
- break;
343
- }
344
- case SummaryType.Attachment:
345
- assert(this.isRealStorageId(obj.id), 0x0fe /* "attachment is aggregate blob" */);
346
- break;
347
- default:
348
- unreachableCase(obj, `Unknown type: ${(obj as any).type}`);
349
- }
350
- }
351
-
352
- assert(newSummary.tree[this.aggregatedBlobName] === undefined, 0x0ff /* "duplicate aggregate blob" */);
353
- if (shouldCompress) {
354
- // Note: It would be great to add code here to unpack aggregate blob back to normal blobs
355
- // If only one blob made it into aggregate. Currently that does not happen as we always have
356
- // at least one .component blob and at least one DDS that has .attributes blob, so it's not an issue.
357
- // But it's possible that in future that would be great addition!
358
- // Good news - it's backward compatible change.
359
- assert(aggregator !== undefined, 0x100 /* "logic error" */);
360
- const content = aggregator.getAggregatedBlobContent();
361
- if (content !== undefined) {
362
- newSummary.tree[this.aggregatedBlobName] = {
363
- type: SummaryType.Blob,
364
- content,
365
- };
366
- }
367
- }
368
- return newSummary;
369
- }
370
-
371
- protected isRealStorageId(id: string): boolean {
372
- return !id.startsWith(this.virtualIdPrefix);
373
- }
158
+ // Tells data store if it can use incremental summary (i.e. reuse DDSes from previous summary
159
+ // when only one DDS changed).
160
+ // The answer has to be know long before we enable actual packing. The reason for the is the following:
161
+ // A the moment when we enable packing, we should assume that all clients out there wil already have bits
162
+ // that can unpack properly (i.e. enough time passed since we deployed bits that can unpack)
163
+ // But we can still have clients where some of them already pack, and some do not. If one summary was
164
+ // using packing, then it relies on non-incremental summaries going forward, even if next client who
165
+ // produced summary is not packing!
166
+ // This can have slight improvement by enabling it per file (based on "did summary we loaded from contain
167
+ // aggregated blobs"), but that's harder to make reliable, so going for simplicity.
168
+ static readonly fullDataStoreSummaries = true;
169
+
170
+ protected loadedFromSummary = false;
171
+
172
+ protected virtualBlobs = new Map<string, ArrayBufferLike>();
173
+
174
+ static wrap(
175
+ storage: IDocumentStorageService,
176
+ logger: ITelemetryLogger,
177
+ allowPacking?: boolean,
178
+ packingLevel = 2,
179
+ ) {
180
+ if (storage instanceof BlobAggregationStorage) {
181
+ return storage;
182
+ }
183
+ const mc = loggerToMonitoringContext(logger);
184
+ const realAllowPackaging =
185
+ mc.config.getBoolean("FluidAggregateBlobs") ?? allowPacking ?? false;
186
+
187
+ // Always create BlobAggregationStorage even if storage is not asking for packing.
188
+ // This is mostly to avoid cases where future changes in policy would result in inability to
189
+ // load old files that were created with aggregation on.
190
+ const minBlobSize = storage.policies?.minBlobSize;
191
+ return new BlobAggregationStorage(
192
+ storage,
193
+ logger,
194
+ realAllowPackaging,
195
+ packingLevel,
196
+ minBlobSize,
197
+ );
198
+ }
199
+
200
+ static async unpackSnapshot(snapshot: ISnapshotTree) {
201
+ const converter = new SnapshotExtractorInPlace();
202
+ await converter.unpackSnapshotCore(snapshot);
203
+ }
204
+
205
+ public get policies(): IDocumentStorageServicePolicies | undefined {
206
+ const policies = this.storage.policies;
207
+ if (policies) {
208
+ return { ...policies, minBlobSize: undefined };
209
+ }
210
+ }
211
+
212
+ public async unpackSnapshot(snapshot: ISnapshotTree) {
213
+ // SummarizerNodeWithGC.refreshLatestSummary can call it when this.loadedFromSummary === false
214
+ // (I assumed after file was created)
215
+ // assert(!this.loadedFromSummary, "unpack without summary");
216
+
217
+ this.loadedFromSummary = true;
218
+ await this.unpackSnapshotCore(snapshot);
219
+ }
220
+
221
+ protected constructor(
222
+ private readonly storage: IDocumentStorageService,
223
+ private readonly logger: ITelemetryLogger,
224
+ private readonly allowPacking: boolean,
225
+ private readonly packingLevel: number,
226
+ private readonly blobCutOffSize?: number,
227
+ ) {
228
+ super();
229
+ }
230
+
231
+ public setBlob(id: string, tree: ISnapshotTree, content: string) {
232
+ this.virtualBlobs.set(id, stringToBuffer(content, "utf-8"));
233
+ }
234
+
235
+ public async getBlob(id: string, tree: ISnapshotTree): Promise<ArrayBufferLike> {
236
+ return this.readBlob(id).catch((error) => {
237
+ this.logger.sendErrorEvent({ eventName: "BlobDedupNoAggregateBlob" }, error);
238
+ throw error;
239
+ });
240
+ }
241
+
242
+ public get repositoryUrl() {
243
+ return this.storage.repositoryUrl;
244
+ }
245
+ public async getVersions(
246
+ versionId: string | null,
247
+ count: number,
248
+ scenarioName?: string,
249
+ fetchSource?: FetchSource,
250
+ ) {
251
+ return this.storage.getVersions(versionId, count, scenarioName, fetchSource);
252
+ }
253
+
254
+ public async downloadSummary(handle: ISummaryHandle): Promise<ISummaryTree> {
255
+ throw new Error("NYI");
256
+ }
257
+
258
+ // for now we are not optimizing these blobs, with assumption that this API is used only
259
+ // for big blobs (images)
260
+ public async createBlob(file: ArrayBufferLike): Promise<ICreateBlobResponse> {
261
+ return this.storage.createBlob(file);
262
+ }
263
+
264
+ public async getSnapshotTree(version?: IVersion): Promise<ISnapshotTree | null> {
265
+ const tree = await this.storage.getSnapshotTree(version);
266
+ if (tree) {
267
+ await this.unpackSnapshot(tree);
268
+ }
269
+ return tree;
270
+ }
271
+
272
+ public async readBlob(id: string): Promise<ArrayBufferLike> {
273
+ if (this.isRealStorageId(id)) {
274
+ return this.storage.readBlob(id);
275
+ }
276
+ // We support only reading blobs from the summary we loaded from.
277
+ // This may need to be extended to any general summary in the future as runtime usage pattern
278
+ // of storage changes (for example, data stores start to load from recent summary, not from original
279
+ // summary whole container loaded from)
280
+
281
+ // are there other ways we can get here? createFile is one flow, but we should not be reading blobs
282
+ // in such flow
283
+ assert(this.loadedFromSummary, 0x0f9 /* "never read summary" */);
284
+ const blob = this.virtualBlobs.get(id);
285
+ assert(blob !== undefined, 0x0fa /* "virtual blob not found" */);
286
+ return blob;
287
+ }
288
+
289
+ public async uploadSummaryWithContext(
290
+ summary: ISummaryTree,
291
+ context: ISummaryContext,
292
+ ): Promise<string> {
293
+ const summaryNew = this.allowPacking ? await this.compressSmallBlobs(summary) : summary;
294
+ return this.storage.uploadSummaryWithContext(summaryNew, context);
295
+ }
296
+
297
+ // For simplification, we assume that
298
+ // - blob aggregation is done at data store level only for now
299
+ // - data store either reuses previous summary, or generates full summary, i.e. there is no partial (some DDS)
300
+ // summary produced by data stores.
301
+ // These simplifications allow us not to touch handles, as they are self-contained (either do not use aggregated
302
+ // blob Or contain aggregated blob that stays relevant for that sub-tree)
303
+ // Note:
304
+ // From perf perspective, it makes sense to place aggregated blobs one level up in the tree not to create extra
305
+ // tree nodes (i.e. have shallow tree with less edges). But that creates problems with reusability of trees at
306
+ // incremental summary time - we would need to understand handles and parse them. In current design we can skip
307
+ // that step because if data store is reused, the hole sub-tree is reused included aggregated blob embedded into it
308
+ // and that means we can do nothing and be correct!
309
+ private async compressSmallBlobs(
310
+ summary: ISummaryTree,
311
+ path = "",
312
+ level = 0,
313
+ aggregatorArg?: BlobAggregator,
314
+ ): Promise<ISummaryTree> {
315
+ if (this.blobCutOffSize === undefined || this.blobCutOffSize < 0) {
316
+ return summary;
317
+ }
318
+
319
+ let shouldCompress: boolean = false;
320
+
321
+ let aggregator = aggregatorArg;
322
+ // checking if this is a dataStore tree, since we only pack at data store level
323
+ if (Object.keys(summary.tree).includes(".component")) {
324
+ assert(aggregator === undefined, 0x0fb /* "logic err with aggregator" */);
325
+ assert(
326
+ level === this.packingLevel,
327
+ 0x23b /* "we are not packing at the right level" */,
328
+ );
329
+ aggregator = new BlobAggregator();
330
+ shouldCompress = true;
331
+ } else {
332
+ assert(
333
+ level !== this.packingLevel,
334
+ 0x23c /* "we are not packing at the right level" */,
335
+ );
336
+ }
337
+
338
+ const newSummary: ISummaryTree = { ...summary };
339
+ newSummary.tree = { ...newSummary.tree };
340
+ for (const key of Object.keys(summary.tree)) {
341
+ const obj = summary.tree[key];
342
+ // Get path relative to root of data store (where we do aggregation)
343
+ const newPath = shouldCompress ? key : `${path}/${key}`;
344
+ switch (obj.type) {
345
+ case SummaryType.Tree:
346
+ // If client created empty tree, keep it as is
347
+ // Also do not package search blobs - they are part of storage contract
348
+ if (obj.tree !== {} && key !== "__search") {
349
+ const tree = await this.compressSmallBlobs(
350
+ obj,
351
+ newPath,
352
+ level + 1,
353
+ aggregator,
354
+ );
355
+ newSummary.tree[key] = tree;
356
+ if (tree.tree === {}) {
357
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
358
+ delete newSummary.tree[key];
359
+ }
360
+ }
361
+ break;
362
+ case SummaryType.Blob:
363
+ if (
364
+ aggregator &&
365
+ typeof obj.content == "string" &&
366
+ obj.content.length < this.blobCutOffSize
367
+ ) {
368
+ aggregator.addBlob(newPath, obj.content);
369
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
370
+ delete newSummary.tree[key];
371
+ }
372
+ break;
373
+ case SummaryType.Handle: {
374
+ // Would be nice to:
375
+ // Trees: expand the tree
376
+ // Blobs: parse handle and ensure it points to real blob, not virtual blob.
377
+ // We can avoid it for now given data store is the granularity of incremental summaries.
378
+ let handlePath = obj.handle;
379
+ if (handlePath.startsWith("/")) {
380
+ handlePath = handlePath.substr(1);
381
+ }
382
+ // Ensure only whole data stores can be reused, no reusing at deeper level!
383
+ assert(level === 0, 0x0fc /* "tree reuse at lower level" */);
384
+ assert(
385
+ !handlePath.includes("/"),
386
+ 0x0fd /* "data stores are writing incremental summaries!" */,
387
+ );
388
+ break;
389
+ }
390
+ case SummaryType.Attachment:
391
+ assert(
392
+ this.isRealStorageId(obj.id),
393
+ 0x0fe /* "attachment is aggregate blob" */,
394
+ );
395
+ break;
396
+ default:
397
+ unreachableCase(obj, `Unknown type: ${(obj as any).type}`);
398
+ }
399
+ }
400
+
401
+ assert(
402
+ newSummary.tree[this.aggregatedBlobName] === undefined,
403
+ 0x0ff /* "duplicate aggregate blob" */,
404
+ );
405
+ if (shouldCompress) {
406
+ // Note: It would be great to add code here to unpack aggregate blob back to normal blobs
407
+ // If only one blob made it into aggregate. Currently that does not happen as we always have
408
+ // at least one .component blob and at least one DDS that has .attributes blob, so it's not an issue.
409
+ // But it's possible that in future that would be great addition!
410
+ // Good news - it's backward compatible change.
411
+ assert(aggregator !== undefined, 0x100 /* "logic error" */);
412
+ const content = aggregator.getAggregatedBlobContent();
413
+ if (content !== undefined) {
414
+ newSummary.tree[this.aggregatedBlobName] = {
415
+ type: SummaryType.Blob,
416
+ content,
417
+ };
418
+ }
419
+ }
420
+ return newSummary;
421
+ }
422
+
423
+ protected isRealStorageId(id: string): boolean {
424
+ return !id.startsWith(this.virtualIdPrefix);
425
+ }
374
426
  }