Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mamayaya1
GitHub Repository: mamayaya1/game
Path: blob/main/projects/chrome-dino/sw.js
4626 views
1
"use strict";
2
3
const OFFLINE_DATA_FILE = "offline.js";
4
const CACHE_NAME_PREFIX = "chrome-dino";
5
const BROADCASTCHANNEL_NAME = "offline";
6
const CONSOLE_PREFIX = "[SW] ";
7
const LAZYLOAD_KEYNAME = "";
8
9
// Create a BroadcastChannel if supported.
10
const broadcastChannel = (typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(BROADCASTCHANNEL_NAME));
11
12
//////////////////////////////////////
13
// Utility methods
14
function PostBroadcastMessage(o)
15
{
16
if (!broadcastChannel)
17
return; // not supported
18
19
// Impose artificial (and arbitrary!) delay of 3 seconds to make sure client is listening by the time the message is sent.
20
// Note we could remove the delay on some messages, but then we create a race condition where sometimes messages can arrive
21
// in the wrong order (e.g. "update ready" arrives before "started downloading update"). So to keep the consistent ordering,
22
// delay all messages by the same amount.
23
setTimeout(() => broadcastChannel.postMessage(o), 3000);
24
};
25
26
function Broadcast(type)
27
{
28
PostBroadcastMessage({
29
"type": type
30
});
31
};
32
33
function BroadcastDownloadingUpdate(version)
34
{
35
PostBroadcastMessage({
36
"type": "downloading-update",
37
"version": version
38
});
39
}
40
41
function BroadcastUpdateReady(version)
42
{
43
PostBroadcastMessage({
44
"type": "update-ready",
45
"version": version
46
});
47
}
48
49
function IsUrlInLazyLoadList(url, lazyLoadList)
50
{
51
if (!lazyLoadList)
52
return false; // presumably lazy load list failed to load
53
54
try {
55
for (const lazyLoadRegex of lazyLoadList)
56
{
57
if (new RegExp(lazyLoadRegex).test(url))
58
return true;
59
}
60
}
61
catch (err)
62
{
63
console.error(CONSOLE_PREFIX + "Error matching in lazy-load list: ", err);
64
}
65
66
return false;
67
};
68
69
function WriteLazyLoadListToStorage(lazyLoadList)
70
{
71
if (typeof localforage === "undefined")
72
return Promise.resolve(); // bypass if localforage not imported
73
else
74
return localforage.setItem(LAZYLOAD_KEYNAME, lazyLoadList)
75
};
76
77
function ReadLazyLoadListFromStorage()
78
{
79
if (typeof localforage === "undefined")
80
return Promise.resolve([]); // bypass if localforage not imported
81
else
82
return localforage.getItem(LAZYLOAD_KEYNAME);
83
};
84
85
function GetCacheBaseName()
86
{
87
// Include the scope to avoid name collisions with any other SWs on the same origin.
88
// e.g. "c3offline-https://example.com/foo/" (won't collide with anything under bar/)
89
return CACHE_NAME_PREFIX + "-" + self.registration.scope;
90
};
91
92
function GetCacheVersionName(version)
93
{
94
// Append the version number to the cache name.
95
// e.g. "c3offline-https://example.com/foo/-v2"
96
return GetCacheBaseName() + "-v" + version;
97
};
98
99
// Return caches.keys() filtered down to just caches we're interested in (with the right base name).
100
// This filters out caches from unrelated scopes.
101
async function GetAvailableCacheNames()
102
{
103
const cacheNames = await caches.keys();
104
const cacheBaseName = GetCacheBaseName();
105
return cacheNames.filter(n => n.startsWith(cacheBaseName));
106
};
107
108
// Identify if an update is pending, which is the case when we have 2 or more available caches.
109
// One must be an update that is waiting, since the next navigate that does an upgrade will
110
// delete all the old caches leaving just one currently-in-use cache.
111
async function IsUpdatePending()
112
{
113
const availableCacheNames = await GetAvailableCacheNames();
114
return (availableCacheNames.length >= 2);
115
};
116
117
// Automatically deduce the main page URL (e.g. index.html or main.aspx) from the available browser windows.
118
// This prevents having to hard-code an index page in the file list, implicitly caching it like AppCache did.
119
async function GetMainPageUrl()
120
{
121
const allClients = await clients.matchAll({
122
includeUncontrolled: true,
123
type: "window"
124
});
125
126
for (const c of allClients)
127
{
128
// Parse off the scope from the full client URL, e.g. https://example.com/index.html -> index.html
129
let url = c.url;
130
if (url.startsWith(self.registration.scope))
131
url = url.substring(self.registration.scope.length);
132
133
if (url && url !== "/") // ./ is also implicitly cached so don't bother returning that
134
{
135
// If the URL is solely a search string, prefix it with / to ensure it caches correctly.
136
// e.g. https://example.com/?foo=bar needs to cache as /?foo=bar, not just ?foo=bar.
137
if (url.startsWith("?"))
138
url = "/" + url;
139
140
return url;
141
}
142
}
143
144
return ""; // no main page URL could be identified
145
};
146
147
// Fetch optionally bypassing HTTP cache using fetch cache options
148
function fetchWithBypass(request, bypassCache)
149
{
150
if (typeof request === "string")
151
request = new Request(request);
152
153
if (bypassCache)
154
{
155
return fetch(request.url, {
156
headers: request.headers,
157
mode: request.mode,
158
credentials: request.credentials,
159
redirect: request.redirect,
160
cache: "no-store"
161
});
162
}
163
else
164
{
165
// bypass disabled: perform normal fetch which is allowed to return from HTTP cache
166
return fetch(request);
167
}
168
};
169
170
// Effectively a cache.addAll() that only creates the cache on all requests being successful (as a weak attempt at making it atomic)
171
// and can optionally cache-bypass with fetchWithBypass in every request
172
async function CreateCacheFromFileList(cacheName, fileList, bypassCache)
173
{
174
// Kick off all requests and wait for them all to complete
175
const responses = await Promise.all(fileList.map(url => fetchWithBypass(url, bypassCache)));
176
177
// Check if any request failed. If so don't move on to opening the cache.
178
// This makes sure we only open a cache if all requests succeeded.
179
let allOk = true;
180
181
for (const response of responses)
182
{
183
if (!response.ok)
184
{
185
allOk = false;
186
console.error(CONSOLE_PREFIX + "Error fetching '" + response.url + "' (" + response.status + " " + response.statusText + ")");
187
}
188
}
189
190
if (!allOk)
191
throw new Error("not all resources were fetched successfully");
192
193
// Can now assume all responses are OK. Open a cache and write all responses there.
194
// TODO: ideally we can do this transactionally to ensure a complete cache is written as one atomic operation.
195
// This needs either new transactional features in the spec, or at the very least a way to rename a cache
196
// (so we can write to a temporary name that won't be returned by GetAvailableCacheNames() and then rename it when ready).
197
const cache = await caches.open(cacheName);
198
199
try {
200
return await Promise.all(responses.map(
201
(response, i) => cache.put(fileList[i], response)
202
));
203
}
204
catch (err)
205
{
206
// Not sure why cache.put() would fail (maybe if storage quota exceeded?) but in case it does,
207
// clean up the cache to try to avoid leaving behind an incomplete cache.
208
console.error(CONSOLE_PREFIX + "Error writing cache entries: ", err);
209
caches.delete(cacheName);
210
throw err;
211
}
212
};
213
214
async function UpdateCheck(isFirst)
215
{
216
try {
217
// Always bypass cache when requesting offline.js to make sure we find out about new versions.
218
const response = await fetchWithBypass(OFFLINE_DATA_FILE, true);
219
220
if (!response.ok)
221
throw new Error(OFFLINE_DATA_FILE + " responded with " + response.status + " " + response.statusText);
222
223
const data = await response.json();
224
225
const version = data.version;
226
const fileList = data.fileList;
227
const lazyLoadList = data.lazyLoad;
228
const currentCacheName = GetCacheVersionName(version);
229
230
const cacheExists = await caches.has(currentCacheName);
231
232
// Don't recache if there is already a cache that exists for this version. Assume it is complete.
233
if (cacheExists)
234
{
235
// Log whether we are up-to-date or pending an update.
236
const isUpdatePending = await IsUpdatePending();
237
if (isUpdatePending)
238
{
239
console.log(CONSOLE_PREFIX + "Update pending");
240
Broadcast("update-pending");
241
}
242
else
243
{
244
console.log(CONSOLE_PREFIX + "Up to date");
245
Broadcast("up-to-date");
246
}
247
return;
248
}
249
250
// Implicitly add the main page URL to the file list, e.g. "index.html", so we don't have to assume a specific name.
251
const mainPageUrl = await GetMainPageUrl();
252
253
// Prepend the main page URL to the file list if we found one and it is not already in the list.
254
// Also make sure we request the base / which should serve the main page.
255
fileList.unshift("./");
256
257
if (mainPageUrl && fileList.indexOf(mainPageUrl) === -1)
258
fileList.unshift(mainPageUrl);
259
260
console.log(CONSOLE_PREFIX + "Caching " + fileList.length + " files for offline use");
261
262
if (isFirst)
263
Broadcast("downloading");
264
else
265
BroadcastDownloadingUpdate(version);
266
267
// Note we don't bypass the cache on the first update check. This is because SW installation and the following
268
// update check caching will race with the normal page load requests. For any normal loading fetches that have already
269
// completed or are in-flight, it is pointless and wasteful to cache-bust the request for offline caching, since that
270
// forces a second network request to be issued when a response from the browser HTTP cache would be fine.
271
if (lazyLoadList)
272
await WriteLazyLoadListToStorage(lazyLoadList); // dump lazy load list to local storage#
273
274
await CreateCacheFromFileList(currentCacheName, fileList, !isFirst);
275
const isUpdatePending = await IsUpdatePending();
276
277
if (isUpdatePending)
278
{
279
console.log(CONSOLE_PREFIX + "All resources saved, update ready");
280
BroadcastUpdateReady(version);
281
}
282
else
283
{
284
console.log(CONSOLE_PREFIX + "All resources saved, offline support ready");
285
Broadcast("offline-ready");
286
}
287
}
288
catch (err)
289
{
290
// Update check fetches fail when we're offline, but in case there's any other kind of problem with it, log a warning.
291
console.warn(CONSOLE_PREFIX + "Update check failed: ", err);
292
}
293
};
294
295
self.addEventListener("install", event =>
296
{
297
// On install kick off an update check to cache files on first use.
298
// If it fails we can still complete the install event and leave the SW running, we'll just
299
// retry on the next navigate.
300
event.waitUntil(
301
UpdateCheck(true) // first update
302
.catch(() => null)
303
);
304
});
305
306
async function GetCacheNameToUse(availableCacheNames, doUpdateCheck)
307
{
308
// Prefer the oldest cache available. This avoids mixed-version responses by ensuring that if a new cache
309
// is created and filled due to an update check while the page is running, we keep returning resources
310
// from the original (oldest) cache only.
311
if (availableCacheNames.length === 1 || !doUpdateCheck)
312
return availableCacheNames[0];
313
314
// We are making a navigate request with more than one cache available. Check if we can expire any old ones.
315
const allClients = await clients.matchAll();
316
317
// If there are other clients open, don't expire anything yet. We don't want to delete any caches they
318
// might be using, which could cause mixed-version responses.
319
if (allClients.length > 1)
320
return availableCacheNames[0];
321
322
// Identify newest cache to use. Delete all the others.
323
const latestCacheName = availableCacheNames[availableCacheNames.length - 1];
324
console.log(CONSOLE_PREFIX + "Updating to new version");
325
326
await Promise.all(
327
availableCacheNames.slice(0, -1)
328
.map(c => caches.delete(c))
329
);
330
331
return latestCacheName;
332
};
333
334
async function HandleFetch(event, doUpdateCheck)
335
{
336
const availableCacheNames = await GetAvailableCacheNames();
337
338
// No caches available: go to network
339
if (!availableCacheNames.length)
340
return fetch(event.request);
341
342
const useCacheName = await GetCacheNameToUse(availableCacheNames, doUpdateCheck);
343
const cache = await caches.open(useCacheName);
344
const cachedResponse = await cache.match(event.request);
345
346
if (cachedResponse)
347
return cachedResponse; // use cached response
348
349
// We need to check if this request is to be lazy-cached. Send the request and load the lazy-load list
350
// from storage simultaneously.
351
const result = await Promise.all([fetch(event.request), ReadLazyLoadListFromStorage()]);
352
const fetchResponse = result[0];
353
const lazyLoadList = result[1];
354
355
if (IsUrlInLazyLoadList(event.request.url, lazyLoadList))
356
{
357
// Handle failure writing to the cache. This can happen if the storage quota is exceeded, which is particularly
358
// likely in Safari 11.1, which appears to have very tight storage limits. Make sure even in the event of an error
359
// we continue to return the response from the fetch.
360
try {
361
// Note clone response since we also respond with it
362
await cache.put(event.request, fetchResponse.clone());
363
}
364
catch (err)
365
{
366
console.warn(CONSOLE_PREFIX + "Error caching '" + event.request.url + "': ", err);
367
}
368
}
369
370
return fetchResponse;
371
};
372
373
self.addEventListener("fetch", event =>
374
{
375
/** NOTE (iain)
376
* This check is to prevent a bug with XMLHttpRequest where if its
377
* proxied with "FetchEvent.prototype.respondWith" no upload progress
378
* events are triggered. By returning we allow the default action to
379
* occur instead. Currently all cross-origin requests fall back to default.
380
*/
381
if (new URL(event.request.url).origin !== location.origin)
382
return;
383
384
// Check for an update on navigate requests
385
const doUpdateCheck = (event.request.mode === "navigate");
386
387
const responsePromise = HandleFetch(event, doUpdateCheck);
388
389
if (doUpdateCheck)
390
{
391
// allow the main request to complete, then check for updates
392
event.waitUntil(
393
responsePromise
394
.then(() => UpdateCheck(false)) // not first check
395
);
396
}
397
398
event.respondWith(responsePromise);
399
});
400