scratch-storage 6.1.11 → 6.2.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.
package/src/WebHelper.ts CHANGED
@@ -7,23 +7,43 @@ import {ScratchGetRequest, ScratchSendRequest, Tool} from './Tool';
7
7
  import {AssetType} from './AssetType';
8
8
  import {DataFormat} from './DataFormat';
9
9
 
10
- const ensureRequestConfig = reqConfig => {
10
+ /**
11
+ * The request configuration
12
+ */
13
+ type RequestConfig = ScratchGetRequest | ScratchSendRequest;
14
+
15
+ /**
16
+ * The result of a UrlFunction, which can be a string URL or a full request configuration
17
+ * object, or a promise for either of those.
18
+ *
19
+ * If set to null or undefined, the WebHelper will skip that store and move on to the
20
+ * next one. This allows stores to be registered that only provide a subset of their
21
+ * declared asset types at a given time.
22
+ */
23
+ type RequestFnResult = null | undefined | string | ScratchGetRequest | ScratchSendRequest;
24
+
25
+ /**
26
+ * Ensure that the provided request configuration is in object form, converting from
27
+ * string if necessary.
28
+ */
29
+ const ensureRequestConfig = async (
30
+ reqConfig: RequestFnResult | Promise<RequestFnResult>
31
+ ): Promise<RequestConfig | null | undefined> => {
32
+ reqConfig = await reqConfig;
33
+
11
34
  if (typeof reqConfig === 'string') {
12
35
  return {
13
36
  url: reqConfig
14
37
  };
15
38
  }
39
+
16
40
  return reqConfig;
17
41
  };
18
42
 
19
43
  /**
20
- * @typedef {function} UrlFunction - A function which computes a URL from asset information.
21
- * @param {Asset} - The asset for which the URL should be computed.
22
- * @returns {(string|object)} - A string representing the URL for the asset request OR an object with configuration for
23
- * the underlying fetch call (necessary for configuring e.g. authentication)
44
+ * A function which computes a URL from asset information.
24
45
  */
25
-
26
- export type UrlFunction = (asset: Asset) => string | ScratchGetRequest | ScratchSendRequest;
46
+ export type UrlFunction = (asset: Asset) => RequestFnResult | Promise<RequestFnResult>;
27
47
 
28
48
  interface StoreRecord {
29
49
  types: string[],
@@ -105,9 +125,9 @@ export default class WebHelper extends Helper {
105
125
  * @param {DataFormat} dataFormat - The file format / file extension of the asset to fetch: PNG, JPG, etc.
106
126
  * @returns {Promise.<Asset>} A promise for the contents of the asset.
107
127
  */
108
- load (assetType: AssetType, assetId: AssetId, dataFormat: DataFormat): Promise<Asset | null> {
128
+ async load (assetType: AssetType, assetId: AssetId, dataFormat: DataFormat): Promise<Asset | null> {
109
129
 
110
- /** @type {Array.<{url:string, result:*}>} List of URLs attempted & errors encountered. */
130
+ /** @type {unknown[]} List of errors encountered while attempting to load the asset. */
111
131
  const errors: unknown[] = [];
112
132
  const stores = this.stores.slice()
113
133
  .filter(store => store.types.indexOf(assetType.name) >= 0);
@@ -120,40 +140,33 @@ export default class WebHelper extends Helper {
120
140
  tool = this.projectTool;
121
141
  }
122
142
 
123
- let storeIndex = 0;
124
- const tryNextSource = (err?: unknown): Promise<Asset | null> => {
125
- if (err) {
126
- errors.push(err);
127
- }
128
- const store = stores[storeIndex++];
129
-
130
- /** @type {UrlFunction} */
143
+ for (const store of stores) {
131
144
  const reqConfigFunction = store && store.get;
132
145
 
133
146
  if (reqConfigFunction) {
134
- const reqConfig = ensureRequestConfig(reqConfigFunction(asset));
135
- if (reqConfig === false) {
136
- return tryNextSource();
137
- }
147
+ try {
148
+ const reqConfig = await ensureRequestConfig(reqConfigFunction(asset));
149
+ if (!reqConfig) {
150
+ continue;
151
+ }
138
152
 
139
- return tool.get(reqConfig)
140
- .then(body => {
141
- if (body) {
142
- asset.setData(body, dataFormat);
143
- return asset;
144
- }
145
- return tryNextSource();
146
- })
147
- .catch(tryNextSource);
148
- } else if (errors.length > 0) {
149
- return Promise.reject(errors);
153
+ const body = await tool.get(reqConfig);
154
+ if (body) {
155
+ asset.setData(body, dataFormat);
156
+ return asset;
157
+ }
158
+ } catch (err) {
159
+ errors.push(err);
160
+ }
150
161
  }
162
+ }
151
163
 
152
- // no stores matching asset
153
- return Promise.resolve(null);
154
- };
164
+ if (errors.length > 0) {
165
+ return Promise.reject(errors);
166
+ }
155
167
 
156
- return tryNextSource();
168
+ // no stores matching asset
169
+ return Promise.resolve(null);
157
170
  }
158
171
 
159
172
  /**
@@ -164,7 +177,7 @@ export default class WebHelper extends Helper {
164
177
  * @param {?string} assetId - The ID of the asset to fetch: a project ID, MD5, etc.
165
178
  * @returns {Promise.<object>} A promise for the response from the create or update request
166
179
  */
167
- store (
180
+ async store (
168
181
  assetType: AssetType,
169
182
  dataFormat: DataFormat | undefined,
170
183
  data: AssetData,
@@ -174,49 +187,60 @@ export default class WebHelper extends Helper {
174
187
  // If we have an asset id, we should update, otherwise create to get an id
175
188
  const create = assetId === '' || assetId === null || typeof assetId === 'undefined';
176
189
 
177
- // Use the first store with the appropriate asset type and url function
178
- const store = this.stores.filter(s =>
190
+ const candidateStores = this.stores.filter(s =>
179
191
  // Only use stores for the incoming asset type
180
192
  s.types.indexOf(assetType.name) !== -1 && (
181
193
  // Only use stores that have a create function if this is a create request
182
194
  // or an update function if this is an update request
183
195
  (create && s.create) || s.update
184
196
  )
185
- )[0];
197
+ );
186
198
 
187
199
  const method = create ? 'post' : 'put';
188
200
 
189
- if (!store) return Promise.reject(new Error('No appropriate stores'));
201
+ if (candidateStores.length === 0) {
202
+ return Promise.reject(new Error('No appropriate stores'));
203
+ }
190
204
 
191
205
  let tool = this.assetTool;
192
206
  if (assetType.name === 'Project') {
193
207
  tool = this.projectTool;
194
208
  }
195
209
 
196
- const reqConfig = ensureRequestConfig(
197
- // The non-nullability of this gets checked above while looking up the store.
198
- // Making TS understand that is going to require code refactoring which we currently don't
199
- // feel safe to do.
200
- create ? store.create!(asset) : store.update!(asset)
201
- );
202
- const reqBodyConfig = Object.assign({body: data, method}, reqConfig);
203
- return tool.send(reqBodyConfig)
204
- .then(body => {
205
- // xhr makes it difficult to both send FormData and
206
- // automatically parse a JSON response. So try to parse
207
- // everything as JSON.
208
- if (typeof body === 'string') {
209
- try {
210
- body = JSON.parse(body);
211
- } catch (parseError) { // eslint-disable-line @typescript-eslint/no-unused-vars
212
- // If it's not parseable, then we can't add the id even
213
- // if we want to, so stop here
214
- return body;
215
- }
210
+ for (const store of candidateStores) {
211
+ const reqConfig = await ensureRequestConfig(
212
+ // The non-nullability of this gets checked above while looking up the store.
213
+ // Making TS understand that is going to require code refactoring which we currently don't
214
+ // feel safe to do.
215
+ create ? store.create!(asset) : store.update!(asset)
216
+ );
217
+
218
+ if (!reqConfig) {
219
+ continue;
220
+ }
221
+
222
+ const reqBodyConfig = Object.assign({body: data, method}, reqConfig);
223
+
224
+ let body = await tool.send(reqBodyConfig);
225
+
226
+ // xhr makes it difficult to both send FormData and
227
+ // automatically parse a JSON response. So try to parse
228
+ // everything as JSON.
229
+ if (typeof body === 'string') {
230
+ try {
231
+ body = JSON.parse(body);
232
+ } catch (parseError) { // eslint-disable-line @typescript-eslint/no-unused-vars
233
+ // If it's not parseable, then we can't add the id even
234
+ // if we want to, so stop here
235
+ return body;
216
236
  }
217
- return Object.assign({
218
- id: body['content-name'] || assetId
219
- }, body);
220
- });
237
+ }
238
+
239
+ return Object.assign({
240
+ id: body['content-name'] || assetId
241
+ }, body);
242
+ }
243
+
244
+ return Promise.reject(new Error('No store could handle the request'));
221
245
  }
222
246
  }
@@ -140,3 +140,50 @@ test('load', () => {
140
140
 
141
141
  return Promise.all(assetChecks);
142
142
  });
143
+
144
+ test('load using async UrlFunction', () => {
145
+ const storage = new ScratchStorage();
146
+
147
+ // these `asset => ...` callbacks generate values specifically for the fetch mock
148
+ // in the real world they would generate proper URIs
149
+ storage.addWebStore(
150
+ [storage.AssetType.Project],
151
+ async asset => {
152
+ await new Promise(resolve => setTimeout(() => resolve(), 0));
153
+
154
+ return `http://example.com/${asset.assetId}`;
155
+ },
156
+ null,
157
+ null
158
+ );
159
+
160
+ storage.addWebStore(
161
+ [storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound],
162
+ async asset => {
163
+ await new Promise(resolve => setTimeout(() => resolve(), 0));
164
+
165
+ return `http://example.com/${asset.assetId}.${asset.dataFormat}`;
166
+ },
167
+ null,
168
+ null
169
+ );
170
+
171
+ const testAssets = getTestAssets(storage);
172
+ const assetChecks = testAssets.map(async assetInfo => {
173
+ const asset = await storage.load(assetInfo.type, assetInfo.id, assetInfo.ext);
174
+
175
+ expect(asset).toBeInstanceOf(storage.Asset);
176
+ expect(asset.assetId).toBe(assetInfo.id);
177
+ expect(asset.assetType).toBe(assetInfo.type);
178
+ expect(asset.data.length).toBeGreaterThan(0);
179
+
180
+ // Web assets should come back as clean
181
+ expect(asset.clean).toBeTruthy();
182
+
183
+ if (assetInfo.md5) {
184
+ expect(md5(asset.data)).toBe(assetInfo.md5);
185
+ }
186
+ });
187
+
188
+ return Promise.all(assetChecks);
189
+ });