-
Notifications
You must be signed in to change notification settings - Fork 140
/
Copy pathWebHelper.ts
222 lines (194 loc) · 8.59 KB
/
WebHelper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import log from './log';
import Asset, {AssetData, AssetId} from './Asset';
import Helper from './Helper';
import ProxyTool from './ProxyTool';
import {ScratchGetRequest, ScratchSendRequest, Tool} from './Tool';
import {AssetType} from './AssetType';
import {DataFormat} from './DataFormat';
const ensureRequestConfig = reqConfig => {
if (typeof reqConfig === 'string') {
return {
url: reqConfig
};
}
return reqConfig;
};
/**
* @typedef {function} UrlFunction - A function which computes a URL from asset information.
* @param {Asset} - The asset for which the URL should be computed.
* @returns {(string|object)} - A string representing the URL for the asset request OR an object with configuration for
* the underlying fetch call (necessary for configuring e.g. authentication)
*/
export type UrlFunction = (asset: Asset) => string | ScratchGetRequest | ScratchSendRequest;
interface StoreRecord {
types: string[],
get: UrlFunction,
create?: UrlFunction,
update?: UrlFunction
}
export default class WebHelper extends Helper {
public stores: StoreRecord[];
public assetTool: Tool;
public projectTool: Tool;
constructor (parent) {
super(parent);
/**
* @type {Array.<StoreRecord>}
* @typedef {object} StoreRecord
* @property {Array.<string>} types - The types of asset provided by this store, from AssetType's name field.
* @property {UrlFunction} getFunction - A function which computes a URL from an Asset.
* @property {UrlFunction} createFunction - A function which computes a URL from an Asset.
* @property {UrlFunction} updateFunction - A function which computes a URL from an Asset.
*/
this.stores = [];
/**
* Set of tools to best load many assets in parallel. If one tool
* cannot be used, it will use the next.
* @type {ProxyTool}
*/
this.assetTool = new ProxyTool();
/**
* Set of tools to best load project data in parallel with assets. This
* tool set prefers tools that are immediately ready. Some tools have
* to initialize before they can load files.
* @type {ProxyTool}
*/
this.projectTool = new ProxyTool(ProxyTool.TOOL_FILTER.READY);
}
/**
* Register a web-based source for assets. Sources will be checked in order of registration.
* @deprecated Please use addStore
* @param {Array.<AssetType>} types - The types of asset provided by this source.
* @param {UrlFunction} urlFunction - A function which computes a URL from an Asset.
*/
addSource (types: AssetType[], urlFunction: UrlFunction): void {
log.warn('Deprecation: WebHelper.addSource has been replaced with WebHelper.addStore.');
this.addStore(types, urlFunction);
}
/**
* Register a web-based store for assets. Sources will be checked in order of registration.
* @param {Array.<AssetType>} types - The types of asset provided by this store.
* @param {UrlFunction} getFunction - A function which computes a GET URL for an Asset
* @param {UrlFunction} createFunction - A function which computes a POST URL for an Asset
* @param {UrlFunction} updateFunction - A function which computes a PUT URL for an Asset
*/
addStore (
types: AssetType[],
getFunction: UrlFunction,
createFunction?: UrlFunction,
updateFunction?: UrlFunction
): void {
this.stores.push({
types: types.map(assetType => assetType.name),
get: getFunction,
create: createFunction,
update: updateFunction
});
}
/**
* Fetch an asset but don't process dependencies.
* @param {AssetType} assetType - The type of asset to fetch.
* @param {string} assetId - The ID of the asset to fetch: a project ID, MD5, etc.
* @param {DataFormat} dataFormat - The file format / file extension of the asset to fetch: PNG, JPG, etc.
* @return {Promise.<Asset>} A promise for the contents of the asset.
*/
load (assetType: AssetType, assetId: AssetId, dataFormat: DataFormat): Promise<Asset | null> {
/** @type {Array.<{url:string, result:*}>} List of URLs attempted & errors encountered. */
const errors: unknown[] = [];
const stores = this.stores.slice()
.filter(store => store.types.indexOf(assetType.name) >= 0);
// New empty asset but it doesn't have data yet
const asset = new Asset(assetType, assetId, dataFormat);
let tool = this.assetTool;
if (assetType.name === 'Project') {
tool = this.projectTool;
}
let storeIndex = 0;
const tryNextSource = (err?: unknown): Promise<Asset | null> => {
if (err) {
errors.push(err);
}
const store = stores[storeIndex++];
/** @type {UrlFunction} */
const reqConfigFunction = store && store.get;
if (reqConfigFunction) {
const reqConfig = ensureRequestConfig(reqConfigFunction(asset));
if (reqConfig === false) {
return tryNextSource();
}
return tool.get(reqConfig)
.then(body => {
if (body) {
asset.setData(body, dataFormat);
return asset;
}
return tryNextSource();
})
.catch(tryNextSource);
} else if (errors.length > 0) {
return Promise.reject(errors);
}
// no stores matching asset
return Promise.resolve(null);
};
return tryNextSource();
}
/**
* Create or update an asset with provided data. The create function is called if no asset id is provided
* @param {AssetType} assetType - The type of asset to create or update.
* @param {?DataFormat} dataFormat - DataFormat of the data for the stored asset.
* @param {Buffer} data - The data for the cached asset.
* @param {?string} assetId - The ID of the asset to fetch: a project ID, MD5, etc.
* @return {Promise.<object>} A promise for the response from the create or update request
*/
store (
assetType: AssetType,
dataFormat: DataFormat | undefined,
data: AssetData,
assetId?: AssetId
): Promise<string | {id: string}> {
const asset = new Asset(assetType, assetId, dataFormat);
// If we have an asset id, we should update, otherwise create to get an id
const create = assetId === '' || assetId === null || typeof assetId === 'undefined';
// Use the first store with the appropriate asset type and url function
const store = this.stores.filter(s =>
// Only use stores for the incoming asset type
s.types.indexOf(assetType.name) !== -1 && (
// Only use stores that have a create function if this is a create request
// or an update function if this is an update request
(create && s.create) || s.update
)
)[0];
const method = create ? 'post' : 'put';
if (!store) return Promise.reject(new Error('No appropriate stores'));
let tool = this.assetTool;
if (assetType.name === 'Project') {
tool = this.projectTool;
}
const reqConfig = ensureRequestConfig(
// The non-nullability of this gets checked above while looking up the store.
// Making TS understand that is going to require code refactoring which we currently don't
// feel safe to do.
create ? store.create!(asset) : store.update!(asset)
);
const reqBodyConfig = Object.assign({body: data, method}, reqConfig);
return tool.send(reqBodyConfig)
.then(body => {
// xhr makes it difficult to both send FormData and
// automatically parse a JSON response. So try to parse
// everything as JSON.
if (typeof body === 'string') {
try {
body = JSON.parse(body);
} catch (parseError) {
// If it's not parseable, then we can't add the id even
// if we want to, so stop here
return body;
}
}
return Object.assign({
id: body['content-name'] || assetId
}, body);
});
}
}