-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgetLocalizeResources.js
393 lines (280 loc) · 8.27 KB
/
getLocalizeResources.js
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
/* eslint no-console: 0 */
import { getDocumentLocaleSettings } from '@brightspace-ui/intl/lib/common.js';
const CacheName = 'd2l-oslo';
const ContentTypeHeader = 'Content-Type';
const ContentTypeJson = 'application/json';
const DebounceTime = 150;
const ETagHeader = 'ETag';
const StateFetching = 2;
const StateIdle = 1;
const BatchFailedReason = new Error('Failed to fetch batch overrides.');
const SingleFailedReason = new Error('Failed to fetch overrides.');
const blobs = new Map();
let cache = undefined;
let cachePromise = undefined;
let documentLocaleSettings = undefined;
let queue = [];
let state = StateIdle;
let timer = 0;
let debug = false;
async function publish(request, response) {
if (response.ok) {
const overridesJson = await response.json();
request.resolve(overridesJson);
} else {
request.reject(SingleFailedReason);
}
}
async function flushQueue() {
timer = 0;
state = StateFetching;
if (queue.length <= 0) {
state = StateIdle;
return;
}
const requests = queue;
queue = [];
const resources = requests.map(item => item.resource);
const bodyObject = { resources };
const bodyText = JSON.stringify(bodyObject);
const res = await fetch(documentLocaleSettings.oslo.batch, {
method: 'POST',
body: bodyText,
headers: { [ContentTypeHeader]: ContentTypeJson }
});
if (res.ok) {
const responses = (await res.json()).resources;
const tasks = [];
for (let i = 0; i < responses.length; ++i) {
const response = responses[i];
const request = requests[i];
const responseValue = new Response(response.body, {
status: response.status,
headers: response.headers
});
// New version might be available since the page loaded, so make a
// record of it.
const nextVersion = responseValue.headers.get(ETagHeader);
if (nextVersion) {
setVersion(nextVersion);
}
const cacheKey = new Request(formatCacheKey(request.resource));
const cacheValue = responseValue.clone();
if (cache === undefined) {
if (cachePromise === undefined) {
cachePromise = caches.open(CacheName);
}
cache = await cachePromise;
}
debug && console.log(`[Oslo] cache prime: ${request.resource}`);
tasks.push(cache.put(cacheKey, cacheValue));
tasks.push(publish(request, responseValue));
}
await Promise.all(tasks);
} else {
for (const request of requests) {
request.reject(BatchFailedReason);
}
}
if (queue.length > 0) {
setTimeout(flushQueue, 0);
} else {
state = StateIdle;
}
}
function debounceQueue() {
if (state !== StateIdle) {
return;
}
if (timer > 0) {
clearTimeout(timer);
}
timer = setTimeout(flushQueue, DebounceTime);
}
async function fetchCollection(url) {
if (blobs.has(url)) {
return Promise.resolve(blobs.get(url));
}
const res = await fetch(url, { method: 'GET' });
if (res.ok) {
const resJson = await res.json();
blobs.set(url, resJson);
return Promise.resolve(resJson);
} else {
return Promise.reject(SingleFailedReason);
}
}
function fetchWithQueuing(resource) {
const promise = new Promise((resolve, reject) => {
queue.push({ resource, resolve, reject });
});
debounceQueue();
return promise;
}
function formatCacheKey(resource) {
return formatOsloRequest(documentLocaleSettings.oslo.collection, resource);
}
async function fetchWithCaching(resource) {
if (cache === undefined) {
if (cachePromise === undefined) {
cachePromise = caches.open(CacheName);
}
cache = await cachePromise;
}
const cacheKey = new Request(formatCacheKey(resource));
const cacheValue = await cache.match(cacheKey);
if (cacheValue === undefined) {
debug && console.log(`[Oslo] cache miss: ${resource}`);
return fetchWithQueuing(resource);
}
debug && console.log(`[Oslo] cache hit: ${resource}`);
if (!cacheValue.ok) {
fetchWithQueuing(resource).then(url => URL.revokeObjectURL(url));
throw SingleFailedReason;
}
// Check if the cache response is stale based on either the document init or
// any requests we've made to the LMS since init. We'll still serve stale
// from cache for this page, but we'll update it in the background for the
// next page.
// We rely on the ETag header to identify if the cache needs to be updated.
// The LMS will provide it in the format: [release].[build].[langModifiedVersion]
// So for example, an ETag in the 20.20.10 release could be: 20.20.10.24605.55520
const currentVersion = getVersion();
if (currentVersion) {
const previousVersion = cacheValue.headers.get(ETagHeader);
if (previousVersion !== currentVersion) {
debug && console.log(`[Oslo] cache stale: ${resource}`);
fetchWithQueuing(resource).then(url => URL.revokeObjectURL(url));
}
}
return await cacheValue.json();
}
function fetchWithPooling(resource) {
// At most one request per resource.
let promise = blobs.get(resource);
if (promise === undefined) {
promise = fetchWithCaching(resource);
blobs.set(resource, promise);
}
return promise;
}
async function shouldUseBatchFetch() {
if (documentLocaleSettings === undefined) {
documentLocaleSettings = getDocumentLocaleSettings();
}
if (!documentLocaleSettings.oslo) {
return false;
}
try {
// try opening CacheStorage, if the session is in a private browser in firefox this throws an exception
await caches.open(CacheName);
// Only batch if we can do client-side caching, otherwise it's worse on each
// subsequent page navigation.
return Boolean(documentLocaleSettings.oslo.batch) && 'CacheStorage' in window;
} catch (err) {
return false;
}
}
function shouldUseCollectionFetch() {
if (documentLocaleSettings === undefined) {
documentLocaleSettings = getDocumentLocaleSettings();
}
if (!documentLocaleSettings.oslo) {
return false;
}
return Boolean(documentLocaleSettings.oslo.collection);
}
function setVersion(version) {
if (documentLocaleSettings === undefined) {
documentLocaleSettings = getDocumentLocaleSettings();
}
if (!documentLocaleSettings.oslo) {
return;
}
documentLocaleSettings.oslo.version = version;
}
function getVersion() {
if (documentLocaleSettings === undefined) {
documentLocaleSettings = getDocumentLocaleSettings();
}
const shouldReturnVersion =
documentLocaleSettings.oslo &&
documentLocaleSettings.oslo.version;
if (!shouldReturnVersion) {
return null;
}
return documentLocaleSettings.oslo.version;
}
async function shouldFetchOverrides() {
const isOsloAvailable =
await shouldUseBatchFetch() ||
shouldUseCollectionFetch();
return isOsloAvailable;
}
async function fetchOverride(formatFunc) {
let resource, res, requestURL;
if (await shouldUseBatchFetch()) {
// If batching is available, pool requests together.
resource = formatFunc();
res = fetchWithPooling(resource);
} else /* shouldUseCollectionFetch() == true */ {
// Otherwise, fetch it directly and let the LMS manage the cache.
resource = formatFunc();
requestURL = formatOsloRequest(documentLocaleSettings.oslo.collection, resource);
res = fetchCollection(requestURL);
}
res = res.catch(coalesceToNull);
return res;
}
function coalesceToNull() {
return null;
}
function formatOsloRequest(baseUrl, resource) {
return `${baseUrl}/${resource}`;
}
export function __clearWindowCache() {
// Used to reset state for tests.
blobs.clear();
cache = undefined;
cachePromise = undefined;
}
export function __enableDebugging() {
// Used to enable debug logging during development.
debug = true;
}
export async function getLocalizeOverrideResources(collection) {
if (await shouldFetchOverrides()) {
return fetchOverride(() => collection);
}
}
export async function getLocalizeResources(
possibleLanguages,
filterFunc,
formatFunc,
fetchFunc
) {
const promises = [];
let supportedLanguage;
if (await shouldFetchOverrides()) {
const overrides = await fetchOverride(formatFunc, fetchFunc);
promises.push(overrides);
}
for (const language of possibleLanguages) {
if (filterFunc(language)) {
if (supportedLanguage === undefined) {
supportedLanguage = language;
}
const translations = fetchFunc(formatFunc(language));
promises.push(translations);
break;
}
}
const results = await Promise.all(promises);
// We're fetching in best -> worst, so we'll assign worst -> best, so the
// best overwrite everything else.
results.reverse();
return {
language: supportedLanguage,
resources: Object.assign({}, ...results)
};
}