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/dist/node/scratch-storage.js +46 -39
- package/dist/node/scratch-storage.js.map +1 -1
- package/dist/types/WebHelper.d.ts +11 -5
- package/dist/web/scratch-storage.js +46 -39
- package/dist/web/scratch-storage.js.map +1 -1
- package/dist/web/scratch-storage.min.js +1 -1
- package/package.json +10 -10
- package/src/WebHelper.ts +89 -65
- package/test/integration/download-known-assets.test.js +47 -0
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
|
-
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
try {
|
|
148
|
+
const reqConfig = await ensureRequestConfig(reqConfigFunction(asset));
|
|
149
|
+
if (!reqConfig) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
138
152
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
153
|
-
return Promise.
|
|
154
|
-
}
|
|
164
|
+
if (errors.length > 0) {
|
|
165
|
+
return Promise.reject(errors);
|
|
166
|
+
}
|
|
155
167
|
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
197
|
+
);
|
|
186
198
|
|
|
187
199
|
const method = create ? 'post' : 'put';
|
|
188
200
|
|
|
189
|
-
if (
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
});
|