Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mamayaya1
GitHub Repository: mamayaya1/game
Path: blob/main/projects/btts/sw.js
4626 views
1
"use strict";
2
3
const OFFLINE_DATA_FILE = "offline.js";
4
const CACHE_NAME_PREFIX = "c2offline";
5
const BROADCASTCHANNEL_NAME = "offline";
6
const CONSOLE_PREFIX = "[SW] ";
7
8
// Create a BroadcastChannel if supported.
9
const broadcastChannel = typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(BROADCASTCHANNEL_NAME);
10
11
//////////////////////////////////////
12
// Utility methods
13
function PostBroadcastMessage(o) {
14
if (!broadcastChannel) return; // not supported
15
16
// Impose artificial (and arbitrary!) delay of 3 seconds to make sure client is listening by the time the message is sent.
17
// Note we could remove the delay on some messages, but then we create a race condition where sometimes messages can arrive
18
// in the wrong order (e.g. "update ready" arrives before "started downloading update"). So to keep the consistent ordering,
19
// delay all messages by the same amount.
20
setTimeout(() => broadcastChannel.postMessage(o), 3000);
21
}
22
23
function Broadcast(type) {
24
PostBroadcastMessage({
25
type: type,
26
});
27
}
28
29
function BroadcastDownloadingUpdate(version) {
30
PostBroadcastMessage({
31
type: "downloading-update",
32
version: version,
33
});
34
}
35
36
function BroadcastUpdateReady(version) {
37
PostBroadcastMessage({
38
type: "update-ready",
39
version: version,
40
});
41
}
42
43
function GetCacheBaseName() {
44
// Include the scope to avoid name collisions with any other SWs on the same origin.
45
// e.g. "c2offline-https://example.com/foo/" (won't collide with anything under bar/)
46
return CACHE_NAME_PREFIX + "-" + self.registration.scope;
47
}
48
49
function GetCacheVersionName(version) {
50
// Append the version number to the cache name.
51
// e.g. "c2offline-https://example.com/foo/-v2"
52
return GetCacheBaseName() + "-v" + version;
53
}
54
55
// Return caches.keys() filtered down to just caches we're interested in (with the right base name).
56
// This filters out caches from unrelated scopes.
57
function GetAvailableCacheNames() {
58
return caches.keys().then((cacheNames) => {
59
const cacheBaseName = GetCacheBaseName();
60
return cacheNames.filter((n) => n.startsWith(cacheBaseName));
61
});
62
}
63
64
// Identify if an update is pending, which is the case when we have 2 or more available caches.
65
// One must be an update that is waiting, since the next navigate that does an upgrade will
66
// delete all the old caches leaving just one currently-in-use cache.
67
function IsUpdatePending() {
68
return GetAvailableCacheNames().then((availableCacheNames) => availableCacheNames.length >= 2);
69
}
70
71
// Automatically deduce the main page URL (e.g. index.html or main.aspx) from the available browser windows.
72
// This prevents having to hard-code an index page in the file list, implicitly caching it like AppCache did.
73
function GetMainPageUrl() {
74
return clients
75
.matchAll({
76
includeUncontrolled: true,
77
type: "window",
78
})
79
.then((clients) => {
80
for (let c of clients) {
81
// Parse off the scope from the full client URL, e.g. https://example.com/index.html -> index.html
82
let url = c.url;
83
if (url.startsWith(self.registration.scope)) url = url.substring(self.registration.scope.length);
84
85
if (url && url !== "/") {
86
// ./ is also implicitly cached so don't bother returning that
87
// If the URL is solely a search string, prefix it with / to ensure it caches correctly.
88
// e.g. https://example.com/?foo=bar needs to cache as /?foo=bar, not just ?foo=bar.
89
if (url.startsWith("?")) url = "/" + url;
90
91
return url;
92
}
93
}
94
95
return ""; // no main page URL could be identified
96
});
97
}
98
99
// Hack to fetch optionally bypassing HTTP cache until fetch cache options are supported in Chrome (crbug.com/453190)
100
function fetchWithBypass(request, bypassCache) {
101
if (typeof request === "string") request = new Request(request);
102
103
if (bypassCache) {
104
// bypass enabled: add a random search parameter to avoid getting a stale HTTP cache result
105
const url = new URL(request.url);
106
url.search += Math.floor(Math.random() * 1000000);
107
108
return fetch(url, {
109
headers: request.headers,
110
mode: request.mode,
111
credentials: request.credentials,
112
redirect: request.redirect,
113
cache: "no-store",
114
});
115
} else {
116
// bypass disabled: perform normal fetch which is allowed to return from HTTP cache
117
return fetch(request);
118
}
119
}
120
121
// Effectively a cache.addAll() that only creates the cache on all requests being successful (as a weak attempt at making it atomic)
122
// and can optionally cache-bypass with fetchWithBypass in every request
123
function CreateCacheFromFileList(cacheName, fileList, bypassCache) {
124
// Kick off all requests and wait for them all to complete
125
return Promise.all(fileList.map((url) => fetchWithBypass(url, bypassCache))).then((responses) => {
126
// Check if any request failed. If so don't move on to opening the cache.
127
// This makes sure we only open a cache if all requests succeeded.
128
let allOk = true;
129
130
for (let response of responses) {
131
if (!response.ok) {
132
allOk = false;
133
console.error(CONSOLE_PREFIX + "Error fetching '" + originalUrl + "' (" + response.status + " " + response.statusText + ")");
134
}
135
}
136
137
if (!allOk) throw new Error("not all resources were fetched successfully");
138
139
// Can now assume all responses are OK. Open a cache and write all responses there.
140
// TODO: ideally we can do this transactionally to ensure a complete cache is written as one atomic operation.
141
// This needs either new transactional features in the spec, or at the very least a way to rename a cache
142
// (so we can write to a temporary name that won't be returned by GetAvailableCacheNames() and then rename it when ready).
143
return caches
144
.open(cacheName)
145
.then((cache) => {
146
return Promise.all(responses.map((response, i) => cache.put(fileList[i], response)));
147
})
148
.catch((err) => {
149
// Not sure why cache.put() would fail (maybe if storage quota exceeded?) but in case it does,
150
// clean up the cache to try to avoid leaving behind an incomplete cache.
151
console.error(CONSOLE_PREFIX + "Error writing cache entries: ", err);
152
caches.delete(cacheName);
153
throw err;
154
});
155
});
156
}
157
158
function UpdateCheck(isFirst) {
159
// Always bypass cache when requesting offline.js to make sure we find out about new versions.
160
return fetchWithBypass(OFFLINE_DATA_FILE, true)
161
.then((r) => r.json())
162
.then((data) => {
163
const version = data.version;
164
let fileList = data.fileList;
165
const currentCacheName = GetCacheVersionName(version);
166
167
return caches.has(currentCacheName).then((cacheExists) => {
168
// Don't recache if there is already a cache that exists for this version. Assume it is complete.
169
if (cacheExists) {
170
// Log whether we are up-to-date or pending an update.
171
return IsUpdatePending().then((isUpdatePending) => {
172
if (isUpdatePending) {
173
console.log(CONSOLE_PREFIX + "Update pending");
174
Broadcast("update-pending");
175
} else {
176
console.log(CONSOLE_PREFIX + "Up to date");
177
Broadcast("up-to-date");
178
}
179
});
180
}
181
182
// Implicitly add the main page URL to the file list, e.g. "index.html", so we don't have to assume a specific name.
183
return GetMainPageUrl().then((mainPageUrl) => {
184
// Prepend the main page URL to the file list if we found one and it is not already in the list.
185
// Also make sure we request the base / which should serve the main page.
186
fileList.unshift("./");
187
188
if (mainPageUrl && fileList.indexOf(mainPageUrl) === -1) fileList.unshift(mainPageUrl);
189
190
console.log(CONSOLE_PREFIX + "Caching " + fileList.length + " files for offline use");
191
192
if (isFirst) Broadcast("downloading");
193
else BroadcastDownloadingUpdate(version);
194
195
// Note we don't bypass the cache on the first update check. This is because SW installation and the following
196
// update check caching will race with the normal page load requests. For any normal loading fetches that have already
197
// completed or are in-flight, it is pointless and wasteful to cache-bust the request for offline caching, since that
198
// forces a second network request to be issued when a response from the browser HTTP cache would be fine.
199
return CreateCacheFromFileList(currentCacheName, fileList, !isFirst)
200
.then(IsUpdatePending)
201
.then((isUpdatePending) => {
202
if (isUpdatePending) {
203
console.log(CONSOLE_PREFIX + "All resources saved, update ready");
204
BroadcastUpdateReady(version);
205
} else {
206
console.log(CONSOLE_PREFIX + "All resources saved, offline support ready");
207
Broadcast("offline-ready");
208
}
209
});
210
});
211
});
212
})
213
.catch((err) => {
214
// Update check fetches fail when we're offline, but in case there's any other kind of problem with it, log a warning.
215
console.warn(CONSOLE_PREFIX + "Update check failed: ", err);
216
});
217
}
218
219
self.addEventListener("install", (event) => {
220
// On install kick off an update check to cache files on first use.
221
// If it fails we can still complete the install event and leave the SW running, we'll just
222
// retry on the next navigate.
223
event.waitUntil(
224
UpdateCheck(true) // first update
225
.catch(() => null)
226
);
227
});
228
229
self.addEventListener("fetch", (event) => {
230
const isNavigateRequest = event.request.mode === "navigate";
231
232
let responsePromise = GetAvailableCacheNames().then((availableCacheNames) => {
233
// No caches available: go to network
234
if (!availableCacheNames.length) return fetch(event.request);
235
236
// Resolve with the cache name to use.
237
return Promise.resolve()
238
.then(() => {
239
// Prefer the oldest cache available. This avoids mixed-version responses by ensuring that if a new cache
240
// is created and filled due to an update check while the page is running, we keep returning resources
241
// from the original (oldest) cache only.
242
if (availableCacheNames.length === 1 || !isNavigateRequest) return availableCacheNames[0];
243
244
// We are making a navigate request with more than one cache available. Check if we can expire any old ones.
245
return clients.matchAll().then((clients) => {
246
// If there are other clients open, don't expire anything yet. We don't want to delete any caches they
247
// might be using, which could cause mixed-version responses.
248
// TODO: verify client count is as expected in navigate requests.
249
// TODO: find a way to upgrade on reloading the only client. Chrome seems to think there are 2 clients in that case.
250
if (clients.length > 1) return availableCacheNames[0];
251
252
// Identify newest cache to use. Delete all the others.
253
let latestCacheName = availableCacheNames[availableCacheNames.length - 1];
254
console.log(CONSOLE_PREFIX + "Updating to new version");
255
256
return Promise.all(availableCacheNames.slice(0, -1).map((c) => caches.delete(c))).then(() => latestCacheName);
257
});
258
})
259
.then((useCacheName) => {
260
return caches
261
.open(useCacheName)
262
.then((c) => c.match(event.request))
263
.then((response) => response || fetch(event.request));
264
});
265
});
266
267
if (isNavigateRequest) {
268
// allow the main request to complete, then check for updates
269
event.waitUntil(responsePromise.then(() => UpdateCheck(false))); // not first check
270
}
271
272
event.respondWith(responsePromise);
273
});
274
275