feat: service worker cache static app resources, and all entry points

This commit is contained in:
Min Idzelis 2025-05-02 23:08:46 +00:00
parent 15d431ba6a
commit 9032c7e021

View file

@ -2,7 +2,7 @@
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { version } from '$service-worker';
import { build, files, version } from '$service-worker';
const useCache = true;
const sw = globalThis as unknown as ServiceWorkerGlobalScope;
@ -11,8 +11,15 @@ const pendingLoads = new Map<string, AbortController>();
// Create a unique cache name for this deployment
const CACHE = `cache-${version}`;
const APP_RESOURCES = [
...build, // the app itself
...files, // everything in `static`
];
sw.addEventListener('install', (event) => {
event.waitUntil(sw.skipWaiting());
// Create a new cache and add all files to it
event.waitUntil(addFilesToCache());
});
sw.addEventListener('activate', (event) => {
@ -26,8 +33,16 @@ sw.addEventListener('fetch', (event) => {
return;
}
const url = new URL(event.request.url);
if (/^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(url.pathname)) {
if (APP_RESOURCES.includes(url.pathname)) {
event.respondWith(appResources(url, event));
} else if (/^\/api\/assets\/[a-f0-9-]+\/(original|thumbnail)/.test(url.pathname)) {
event.respondWith(immichAsset(url));
} else if (
/^(\/(link|auth|admin|albums|archive|buy|explore|favorites|folders|maps|memory|partners|people|photos|places|search|share|shared-links|sharing|tags|trash|user-settings|utilities))(\/.*)?$/.test(
url.pathname,
)
) {
event.respondWith(ssr(new URL(event.request.url).origin));
}
});
@ -39,6 +54,28 @@ async function deleteOldCaches() {
}
}
async function addFilesToCache() {
const cache = await caches.open(CACHE);
await cache.addAll(APP_RESOURCES);
}
async function ssr(origin: string) {
const cache = await caches.open(CACHE);
const url = new URL('/', origin);
let response = useCache ? await cache.match(url) : undefined;
if (response) {
return response;
}
response = await fetch(url);
if (!(response instanceof Response)) {
return Response.error();
}
if (response.status === 200) {
cache.put(url, response.clone());
}
return response;
}
async function immichAsset(url: URL) {
const cache = await caches.open(CACHE);
let response = useCache ? await cache.match(url) : undefined;
@ -66,6 +103,40 @@ async function immichAsset(url: URL) {
}
}
async function appResources(url: URL, event: FetchEvent) {
const cache = await caches.open(CACHE);
// `build`/`files` can always be served from the cache
if (APP_RESOURCES.includes(url.pathname)) {
const response = await cache.match(url.pathname);
if (response) {
return response;
}
}
// for everything else, try the network first, but
// fall back to the cache if we're offline
try {
const response = await fetch(event.request);
// if we're offline, fetch can return a value that is not a Response
// instead of throwing - and we can't pass this non-Response to respondWith
if (!(response instanceof Response)) {
throw new TypeError('invalid response from fetch');
}
if (response.status === 200) {
cache.put(event.request, response.clone());
}
return response;
} catch {
const response = await cache.match(event.request);
if (response) {
return response;
}
// if there's no cache, then just error out
return Response.error();
}
}
const broadcast = new BroadcastChannel('immich');
// eslint-disable-next-line unicorn/prefer-add-event-listener
broadcast.onmessage = (event) => {