/// /// import { ApiPayload } from ':src/utils.ts' import type { PrecacheResponse } from '../../routes/api/serviceworker/precache.tsx' import type { JsonValue } from '$std/json/common.ts' // Force load service worker types const self = globalThis as unknown as ServiceWorkerGlobalScope // Pseudo storage API for SW const swStorage = { async getItem(key: string): Promise { const cache = await caches.open('$SW_STORAGE') const request = new Request(`http://null/${encodeURIComponent(key)}`) const response = await cache.match(request) if (response !== undefined) { return response.json() } return null }, async setItem(key: string, item: T): Promise { const cache = await caches.open('$SW_STORAGE') const request = new Request(`http://null/${encodeURIComponent(key)}`) const response = Response.json(item) return cache.put(request, response) }, } async function getPreCachedUrls(): Promise { const raw = await swStorage.getItem('$sw.pre-cache.urls') if (raw === null) { await openPreCache() return getPreCachedUrls() } return raw } async function getPreCache(): Promise { const version = await swStorage.getItem('$sw.pre-cache.version') // Get cache from server if (version === null) { await openPreCache() return getPreCache() } return caches.open(version) } const IS_SW = 'onpushsubscriptionchange' in self if (IS_SW) { self.addEventListener('install', (event) => { // Assign global cache and pre-cached-urls event.waitUntil(openPreCache()) }) self.addEventListener('activate', () => { //TODO handle activation }) self.addEventListener('fetch', (event) => { const url = new URL(event.request.url) // Don't handle 3rd party request if (url.origin !== location.origin) return event.respondWith(fetchHandler(event)) }) self.addEventListener('push', (event) => { console.log('push') const { title, options } = (event.data?.json() ?? {}) as { title?: string options?: Partial } if (title) { event.waitUntil( self.registration.showNotification(title, options), ) } }) } async function fetchHandler(event: FetchEvent) { const url = new URL(event.request.url) const method = event.request.method const preCachedUrls = await getPreCachedUrls() // Cache first for "pre-cached-urls" if (preCachedUrls.includes(url.pathname) && method === 'GET') { const preCache = await getPreCache() // Cache request update on each request if version change updatePreCache() // TODO handle search params const cached = await preCache.match(event.request) ?? await fetch(event.request).then((response) => response.ok ? response : null ).catch(() => null) ?? await preCache.match(event.request, { ignoreSearch: true }) if (cached === undefined) { throw new Error(`no cache available for pre-cached-url "${url}"`) } return cached } // Fastest and cache refresh if (url.origin === location.origin && method === 'GET') { const cache = await getDynCache() // Get cache or throw const cachedOrError = cache.match(event.request).then((response) => { if (response) return response throw new Error(`no cache available for "${url}"`) }) // Fetch and cache const fetchedAndCached = fetch(event.request).then((response) => { cache.put(event.request, response.clone()) return response }) // Get fastest return Promise.race([cachedOrError, fetchedAndCached]) } // Network only return fetch(event.request) } async function getDynCache(): Promise { const version = await swStorage.getItem('$sw.dyn-cache.version') // Create cache if (version === null) { await swStorage.setItem('$sw.dyn-cache.version', now + lifeTime) return getDynCache() } // Clean outdated cache if (version < Date.now()) { await swStorage.setItem('$sw.dyn-cache.version', now + lifeTime) await caches.delete(version.toString()) return getDynCache() } return cache } // Network only return fetch(event.request) } const serverVersion = await getServerPreCacheVersion() const clientVersion = await swStorage.getItem('$sw.pre-cache.version') if (clientVersion === null) return if (serverVersion === undefined) return if (clientVersion === serverVersion) return // Open new pre-cache await openPreCache() // Delete old pre-cache caches.delete(clientVersion) } async function getServerPreCacheVersion(): Promise { const response = await fetch('/api/serviceworker/precache').then( (response) => response.json() as Promise>, ) if (response.kind === 'success') { return response.data.version } } async function openPreCache() { const response = await fetch('/api/serviceworker/precache').then( (response) => response.json() as Promise>, ) if (response.kind === 'error') { throw new Error('unable to get pre-cached resources from server', { cause: new Error(response.error), }) } const { version, preCachedUrls } = response.data const cache = await caches.open(version) // Pre-cache static files and routes index const addList: Promise[] = [] // Prevent bunch error by splitting cache.addAll for (const url of preCachedUrls) { addList.push(cache.add(url)) } await Promise.allSettled(addList) await swStorage.setItem('$sw.pre-cache.version', version) await swStorage.setItem('$sw.pre-cache.urls', preCachedUrls) return { cache, version, preCachedUrls } } export function main() { console.assert() }