123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- "use strict";
- const OFFLINE_DATA_FILE = "offline.js";
- const CACHE_NAME_PREFIX = "c2offline";
- const BROADCASTCHANNEL_NAME = "offline";
- const CONSOLE_PREFIX = "[SW] ";
- const LAZYLOAD_KEYNAME = "";
- const broadcastChannel = (typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(BROADCASTCHANNEL_NAME));
- function PostBroadcastMessage(o)
- {
- if (!broadcastChannel)
- return;
-
-
-
-
-
- setTimeout(() => broadcastChannel.postMessage(o), 3000);
- };
- function Broadcast(type)
- {
- PostBroadcastMessage({
- "type": type
- });
- };
- function BroadcastDownloadingUpdate(version)
- {
- PostBroadcastMessage({
- "type": "downloading-update",
- "version": version
- });
- }
- function BroadcastUpdateReady(version)
- {
- PostBroadcastMessage({
- "type": "update-ready",
- "version": version
- });
- }
- function IsUrlInLazyLoadList(url, lazyLoadList)
- {
- if (!lazyLoadList)
- return false;
-
- try {
- for (const lazyLoadRegex of lazyLoadList)
- {
- if (new RegExp(lazyLoadRegex).test(url))
- return true;
- }
- }
- catch (err)
- {
- console.error(CONSOLE_PREFIX + "Error matching in lazy-load list: ", err);
- }
-
- return false;
- };
- function WriteLazyLoadListToStorage(lazyLoadList)
- {
- if (typeof localforage === "undefined")
- return Promise.resolve();
- else
- return localforage.setItem(LAZYLOAD_KEYNAME, lazyLoadList)
- };
- function ReadLazyLoadListFromStorage()
- {
- if (typeof localforage === "undefined")
- return Promise.resolve([]);
- else
- return localforage.getItem(LAZYLOAD_KEYNAME);
- };
- function GetCacheBaseName()
- {
-
-
- return CACHE_NAME_PREFIX + "-" + self.registration.scope;
- };
- function GetCacheVersionName(version)
- {
-
-
- return GetCacheBaseName() + "-v" + version;
- };
- async function GetAvailableCacheNames()
- {
- const cacheNames = await caches.keys();
- const cacheBaseName = GetCacheBaseName();
- return cacheNames.filter(n => n.startsWith(cacheBaseName));
- };
- async function IsUpdatePending()
- {
- const availableCacheNames = await GetAvailableCacheNames();
- return (availableCacheNames.length >= 2);
- };
- async function GetMainPageUrl()
- {
- const allClients = await clients.matchAll({
- includeUncontrolled: true,
- type: "window"
- });
-
- for (const c of allClients)
- {
-
- let url = c.url;
- if (url.startsWith(self.registration.scope))
- url = url.substring(self.registration.scope.length);
-
- if (url && url !== "/")
- {
-
-
- if (url.startsWith("?"))
- url = "/" + url;
-
- return url;
- }
- }
-
- return "";
- };
- function fetchWithBypass(request, bypassCache)
- {
- if (typeof request === "string")
- request = new Request(request);
-
- if (bypassCache)
- {
-
- const url = new URL(request.url);
- url.search += Math.floor(Math.random() * 1000000);
- return fetch(url, {
- headers: request.headers,
- mode: request.mode,
- credentials: request.credentials,
- redirect: request.redirect,
- cache: "no-store"
- });
- }
- else
- {
-
- return fetch(request);
- }
- };
- async function CreateCacheFromFileList(cacheName, fileList, bypassCache)
- {
-
- const responses = await Promise.all(fileList.map(url => fetchWithBypass(url, bypassCache)));
-
-
-
- let allOk = true;
-
- for (const response of responses)
- {
- if (!response.ok)
- {
- allOk = false;
- console.error(CONSOLE_PREFIX + "Error fetching '" + response.url + "' (" + response.status + " " + response.statusText + ")");
- }
- }
-
- if (!allOk)
- throw new Error("not all resources were fetched successfully");
-
-
-
-
-
- const cache = await caches.open(cacheName);
-
- try {
- return await Promise.all(responses.map(
- (response, i) => cache.put(fileList[i], response)
- ));
- }
- catch (err)
- {
-
-
- console.error(CONSOLE_PREFIX + "Error writing cache entries: ", err);
- caches.delete(cacheName);
- throw err;
- }
- };
- async function UpdateCheck(isFirst)
- {
- try {
-
- const response = await fetchWithBypass(OFFLINE_DATA_FILE, true);
-
- if (!response.ok)
- throw new Error(OFFLINE_DATA_FILE + " responded with " + response.status + " " + response.statusText);
-
- const data = await response.json();
-
- const version = data.version;
- const fileList = data.fileList;
- const lazyLoadList = data.lazyLoad;
- const currentCacheName = GetCacheVersionName(version);
-
- const cacheExists = await caches.has(currentCacheName);
-
- if (cacheExists)
- {
-
- const isUpdatePending = await IsUpdatePending();
- if (isUpdatePending)
- {
- console.log(CONSOLE_PREFIX + "Update pending");
- Broadcast("update-pending");
- }
- else
- {
- console.log(CONSOLE_PREFIX + "Up to date");
- Broadcast("up-to-date");
- }
- return;
- }
-
-
- const mainPageUrl = await GetMainPageUrl();
-
-
-
- fileList.unshift("./");
-
- if (mainPageUrl && fileList.indexOf(mainPageUrl) === -1)
- fileList.unshift(mainPageUrl);
-
- console.log(CONSOLE_PREFIX + "Caching " + fileList.length + " files for offline use");
-
- if (isFirst)
- Broadcast("downloading");
- else
- BroadcastDownloadingUpdate(version);
-
-
-
-
-
- if (lazyLoadList)
- await WriteLazyLoadListToStorage(lazyLoadList);
-
- await CreateCacheFromFileList(currentCacheName, fileList, !isFirst);
- const isUpdatePending = await IsUpdatePending();
-
- if (isUpdatePending)
- {
- console.log(CONSOLE_PREFIX + "All resources saved, update ready");
- BroadcastUpdateReady(version);
- }
- else
- {
- console.log(CONSOLE_PREFIX + "All resources saved, offline support ready");
- Broadcast("offline-ready");
- }
- }
- catch (err)
- {
-
- console.warn(CONSOLE_PREFIX + "Update check failed: ", err);
- }
- };
- self.addEventListener("install", event =>
- {
-
-
-
- event.waitUntil(
- UpdateCheck(true)
- .catch(() => null)
- );
- });
- async function GetCacheNameToUse(availableCacheNames, doUpdateCheck)
- {
-
-
-
- if (availableCacheNames.length === 1 || !doUpdateCheck)
- return availableCacheNames[0];
-
-
- const allClients = await clients.matchAll();
-
-
-
- if (allClients.length > 1)
- return availableCacheNames[0];
-
-
- const latestCacheName = availableCacheNames[availableCacheNames.length - 1];
- console.log(CONSOLE_PREFIX + "Updating to new version");
-
- await Promise.all(
- availableCacheNames.slice(0, -1)
- .map(c => caches.delete(c))
- );
-
- return latestCacheName;
- };
- async function HandleFetch(event, doUpdateCheck)
- {
- const availableCacheNames = await GetAvailableCacheNames();
-
-
- if (!availableCacheNames.length)
- return fetch(event.request);
-
- const useCacheName = await GetCacheNameToUse(availableCacheNames, doUpdateCheck);
- const cache = await caches.open(useCacheName);
- const cachedResponse = await cache.match(event.request);
-
- if (cachedResponse)
- return cachedResponse;
-
-
-
- const result = await Promise.all([fetch(event.request), ReadLazyLoadListFromStorage()]);
- const fetchResponse = result[0];
- const lazyLoadList = result[1];
-
- if (IsUrlInLazyLoadList(event.request.url, lazyLoadList))
- {
-
-
-
- try {
-
- await cache.put(event.request, fetchResponse.clone());
- }
- catch (err)
- {
- console.warn(CONSOLE_PREFIX + "Error caching '" + event.request.url + "': ", err);
- }
- }
-
- return fetchResponse;
- };
- self.addEventListener("fetch", event =>
- {
-
- if (new URL(event.request.url).origin !== location.origin)
- return;
-
-
- const doUpdateCheck = (event.request.mode === "navigate");
-
- const responsePromise = HandleFetch(event, doUpdateCheck);
- if (doUpdateCheck)
- {
-
- event.waitUntil(
- responsePromise
- .then(() => UpdateCheck(false))
- );
- }
- event.respondWith(responsePromise);
- });
|