AnthoLume/assets/sw.js

266 lines
8.1 KiB
JavaScript

// Misc Consts
const SW_VERSION = 1;
const SW_CACHE_NAME = "OFFLINE_V1";
// Message Types
const PURGE_SW_CACHE = "PURGE_SW_CACHE";
const DEL_SW_CACHE = "DEL_SW_CACHE";
const GET_SW_CACHE = "GET_SW_CACHE";
const GET_SW_VERSION = "GET_SW_VERSION";
// Cache Types
const CACHE_ONLY = "CACHE_ONLY";
const CACHE_NEVER = "CACHE_NEVER";
const CACHE_UPDATE_SYNC = "CACHE_UPDATE_SYNC";
const CACHE_UPDATE_ASYNC = "CACHE_UPDATE_ASYNC";
/**
* Define routes and their directives. Takes `routes`, `type`, and `fallback`.
*
* Routes (Required):
* Either a string of the exact request, or a RegExp. Order precedence.
*
* Fallback (Optional):
* A fallback function. If the request fails, this function is executed and
* its return value is returned as the result.
*
* Types (Required):
* - CACHE_ONLY
* Cache once & never refresh.
* - CACHE_NEVER
* Never cache & always perform a request.
* - CACHE_UPDATE_SYNC
* Update cache & return result.
* - CACHE_UPDATE_ASYNC
* Return cache if exists & update cache in background.
**/
const ROUTES = [
{ route: "/local", type: CACHE_UPDATE_ASYNC },
{ route: "/reader", type: CACHE_UPDATE_ASYNC },
{ route: "/manifest.json", type: CACHE_UPDATE_ASYNC },
{ route: /^\/assets\/reader\/fonts\//, type: CACHE_ONLY },
{ route: /^\/assets\//, type: CACHE_UPDATE_ASYNC },
{
route: /^\/documents\/[a-zA-Z0-9]{32}\/(cover|file)$/,
type: CACHE_UPDATE_ASYNC,
},
{
route: /^\/reader\/progress\/[a-zA-Z0-9]{32}$/,
type: CACHE_UPDATE_SYNC,
},
{
route: /.*/,
type: CACHE_NEVER,
fallback: (event) => caches.match("/local"),
},
];
/**
* These are assets that are cached on initial service worker installation.
**/
const PRECACHE_ASSETS = [
// Offline & Reader Assets
"/local",
"/reader",
"/assets/local/index.js",
"/assets/reader/index.js",
"/assets/reader/fonts.css",
"/assets/reader/themes.css",
"/assets/icons/icon512.png",
"/assets/images/no-cover.jpg",
// Main App Assets
"/manifest.json",
"/assets/index.js",
"/assets/style.css",
"/assets/common.js",
// Library Assets
"/assets/lib/jszip.min.js",
"/assets/lib/epub.min.js",
"/assets/lib/no-sleep.min.js",
"/assets/lib/idb-keyval.min.js",
// Fonts
"/assets/reader/fonts/arbutus-slab-v16-latin_latin-ext-regular.woff2",
"/assets/reader/fonts/lato-v24-latin_latin-ext-100.woff2",
"/assets/reader/fonts/lato-v24-latin_latin-ext-100italic.woff2",
"/assets/reader/fonts/lato-v24-latin_latin-ext-700.woff2",
"/assets/reader/fonts/lato-v24-latin_latin-ext-700italic.woff2",
"/assets/reader/fonts/lato-v24-latin_latin-ext-italic.woff2",
"/assets/reader/fonts/lato-v24-latin_latin-ext-regular.woff2",
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-700.woff2",
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-700italic.woff2",
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-italic.woff2",
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-regular.woff2",
];
// ------------------------------------------------------- //
// ----------------------- Helpers ----------------------- //
// ------------------------------------------------------- //
function purgeCache() {
console.log("[purgeCache] Purging Cache");
return caches.keys().then(function (names) {
for (let name of names) caches.delete(name);
});
}
async function updateCache(request) {
let url = request.url ? new URL(request.url).pathname : request;
console.log("[updateCache] Updating Cache:", url);
let cache = await caches.open(SW_CACHE_NAME);
return fetch(request)
.then((response) => {
const resClone = response.clone();
if (response.status < 400) cache.put(request, resClone);
return response;
})
.catch((e) => {
console.log("[updateCache] Updating Cache Failed:", url);
throw e;
});
}
// ------------------------------------------------------- //
// ------------------- Event Listeners ------------------- //
// ------------------------------------------------------- //
async function handleFetch(event) {
// Get Path
let url = new URL(event.request.url).pathname;
// Find Directive
const directive = ROUTES.find(
(item) =>
(item.route instanceof RegExp && url.match(item.route)) ||
url == item.route
) || { type: CACHE_NEVER };
// Get Fallback
const fallbackFunc = (event) => {
console.log("[handleFetch] Fallback:", { url, directive });
if (directive.fallback) return directive.fallback(event);
};
console.log("[handleFetch] Processing:", { url, directive });
// Get Current Cache
let currentCache = await caches.match(event.request);
// Perform Caching Method
switch (directive.type) {
case CACHE_NEVER:
return fetch(event.request).catch((e) => fallbackFunc(event));
case CACHE_ONLY:
return (
currentCache ||
updateCache(event.request).catch((e) => fallbackFunc(event))
);
case CACHE_UPDATE_SYNC:
return updateCache(event.request).catch(
(e) => currentCache || fallbackFunc(event)
);
case CACHE_UPDATE_ASYNC:
let newResponse = updateCache(event.request).catch((e) =>
fallbackFunc(event)
);
return currentCache || newResponse;
}
}
function handleMessage(event) {
console.log("[handleMessage] Received Message:", event.data);
let { id, data } = event.data;
if (data.type === GET_SW_VERSION) {
event.source.postMessage({ id, data: SW_VERSION });
} else if (data.type === PURGE_SW_CACHE) {
purgeCache()
.then(() => event.source.postMessage({ id, data: "SUCCESS" }))
.catch(() => event.source.postMessage({ id, data: "FAILURE" }));
} else if (data.type === GET_SW_CACHE) {
caches.open(SW_CACHE_NAME).then(async (cache) => {
let allKeys = await cache.keys();
// Get Cached Resources
let docResources = allKeys
.map((item) => new URL(item.url).pathname)
.filter(
(item) =>
item.startsWith("/documents/") ||
item.startsWith("/reader/progress/")
);
// Derive Unique IDs
let documentIDs = Array.from(
new Set(
docResources
.filter((item) => item.startsWith("/documents/"))
.map((item) => item.split("/")[2])
)
);
/**
* Filter for cached items only. Attempt to fetch updated result. If
* failure, return cached version. This ensures we return the most up to
* date version possible.
**/
let cachedDocuments = await Promise.all(
documentIDs
.filter(
(id) =>
docResources.includes("/documents/" + id + "/file") &&
docResources.includes("/reader/progress/" + id)
)
.map(async (id) => {
let url = "/reader/progress/" + id;
let currentCache = await caches.match(url);
let resp = await updateCache(url).catch((e) => currentCache);
return resp.json();
})
);
event.source.postMessage({ id, data: cachedDocuments });
});
} else if (data.type === DEL_SW_CACHE) {
caches
.open(SW_CACHE_NAME)
.then((cache) =>
Promise.all([
cache.delete("/documents/" + data.id + "/file"),
cache.delete("/reader/progress/" + data.id),
])
)
.then(() => event.source.postMessage({ id, data: "SUCCESS" }))
.catch(() => event.source.postMessage({ id, data: "FAILURE" }));
} else {
event.source.postMessage({ id, data: { pong: 1 } });
}
}
async function handleInstall(event) {
let cache = await caches.open(SW_CACHE_NAME);
return cache.addAll(PRECACHE_ASSETS);
}
self.addEventListener("message", handleMessage);
self.addEventListener("install", function (event) {
event.waitUntil(handleInstall(event));
});
self.addEventListener("fetch", (event) => {
/**
* Weird things happen when a service worker attempts to handle a request
* when the server responds with chunked transfer encoding. Right now we only
* use chunked encoding on POSTs. So this is to avoid processing those.
**/
if (event.request.method != "GET") return;
return event.respondWith(handleFetch(event));
});