@indiekit/frontend
Version:
Frontend components for Indiekit
211 lines (180 loc) • 6.19 kB
JavaScript
const assetCacheName = "assets-APP_VERSION";
const pagesCacheName = "pages";
const imageCacheName = "images";
const maxPages = 50; // Maximum number of pages to cache
const maxImages = 100; // Maximum number of images to cache
const timeout = 5000; // Number of milliseconds before timing out
const cacheList = new Set([assetCacheName, pagesCacheName, imageCacheName]);
const placeholderImage = `<svg xmlns="http://www.w3.org/2000/svg"><defs><path id="icon" fill="#AAA" d="M24 32a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm-6.9-11.9 4.1 4.1a17 17 0 0 0-9.7 5.3L8 26a22 22 0 0 1 9-6Zm22.5 5.4L36 29l-.8-.8L26 19a22 22 0 0 1 13.5 6.4ZM8.2 11.2l3.7 3.7a24.7 24.7 0 0 0-8.4 6.6l-3.6-3.6c2.4-2.7 5.2-5 8.3-6.7ZM24 7a32 32 0 0 1 23.4 10.2l-3.5 3.6a27 27 0 0 0-24.5-8.4l-4.2-4.2A32 32 0 0 1 24 7ZM2 5l3-3 41 41-3 3L2 5Z" opacity=".7"/>
</defs><rect fill="#000" width="100%" height="100%" opacity="0.075"/><use href="#icon" x="50%" y="50%" transform="translate(-24 -24)"/></svg>`;
/**
* Update asset cache
* @returns {Promise<Cache>} - Updated asset cache
*/
async function updateAssetCache() {
try {
const assetCache = await caches.open(assetCacheName);
// These items won’t block the installation of the service worker
assetCache.addAll(["/app.webmanifest"]);
// These items must be cached for service worker to complete installation
await assetCache.addAll(["APP_CSS_PATH", "APP_JS_PATH", "/offline"]);
return assetCache;
} catch (error) {
console.error("Error updating asset cache", error);
}
}
/**
* Cache the page(s) that initiate the service worker
* @returns {Promise<Cache>} - Updated page cache
*/
async function cacheClients() {
const pages = [];
try {
const allClients = await clients.matchAll({ includeUncontrolled: true });
for (const client of allClients) {
pages.push(client.url);
}
const pagesCache = await caches.open(pagesCacheName);
await pagesCache.addAll(pages);
return pagesCache;
} catch (error) {
console.error("Error updating client cache", error);
}
}
/**
* Remove caches whose name is no longer valid
*/
async function clearOldCaches() {
try {
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => !cacheList.has(key))
.map((key) => caches.delete(key)),
);
} catch (error) {
console.error("Error clearing old caches", error);
}
}
/**
* Trim cache
* @param {string} cacheName - Name of cache
* @param {number} maxItems - Maximum number of items to keep in cache
*/
async function trimCache(cacheName, maxItems) {
try {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxItems) {
await cache.delete(keys[0]);
await trimCache(cacheName, maxItems);
}
} catch (error) {
console.error(`Error trimming ${cacheName} cache`, error);
}
}
self.addEventListener("install", async (event) => {
event.waitUntil(
(async () => {
await updateAssetCache();
await cacheClients();
globalThis.skipWaiting();
})(),
);
});
self.addEventListener("activate", async (event) => {
event.waitUntil(
(async () => {
await clearOldCaches();
await clients.claim();
})(),
);
});
if (registration.navigationPreload) {
self.addEventListener("activate", (event) => {
event.waitUntil(registration.navigationPreload.enable());
});
}
self.addEventListener("message", (event) => {
if (event.data.command == "trimCaches") {
trimCache(pagesCacheName, maxPages);
trimCache(imageCacheName, maxImages);
}
});
self.addEventListener("fetch", (event) => {
const request = event.request;
// Ignore non-GET requests
if (request.method !== "GET") {
return;
}
const retrieveFromCache = caches.match(request);
// For HTML requests, try network, fall back to cache, else show offline page
if (
request.mode === "navigate" ||
request.headers.get("Accept").includes("text/html")
) {
event.respondWith(
(async () => {
// CHECK CACHE
const timer = setTimeout(async () => {
const responseFromCache = await retrieveFromCache;
if (responseFromCache) {
return responseFromCache;
}
}, timeout);
try {
const preloadResponse = await Promise.resolve(event.preloadResponse);
const responseFromPreloadOrFetch =
preloadResponse || (await fetch(request));
// NETWORK
// Save a copy of page to pages cache
clearTimeout(timer);
const copy = responseFromPreloadOrFetch.clone();
const pagesCache = await caches.open(pagesCacheName);
await pagesCache.put(request, copy);
return responseFromPreloadOrFetch;
} catch (error) {
console.error(error, request);
// CACHE or OFFLINE PAGE
clearTimeout(timer);
const responseFromCache = await retrieveFromCache;
return responseFromCache || caches.match("/offline");
}
})(),
);
return;
}
// For non-HTML requests, look in cache first, fall back to network
event.respondWith(
(async () => {
try {
const responseFromCache = await retrieveFromCache;
if (responseFromCache) {
// CACHE
return responseFromCache;
} else {
const responseFromFetch = await fetch(request);
// NETWORK
// If request is for an image, save a copy to images cache
if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) {
const copy = responseFromFetch.clone();
const imagesCache = await caches.open(imageCacheName);
await imagesCache.put(request, copy);
}
return responseFromFetch;
}
} catch (error) {
console.error(error);
// OFFLINE IMAGE
if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) {
return new Response(placeholderImage, {
headers: {
"Content-Type": "image/svg+xml",
"Cache-Control": "no-store",
},
});
}
}
})(),
);
});