210 lines
5.6 KiB
TypeScript
210 lines
5.6 KiB
TypeScript
/// <reference no-default-lib="true"/>
|
|
/// <reference lib="webworker" />
|
|
|
|
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<T extends JsonValue>(key: string): Promise<T | null> {
|
|
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<T extends JsonValue>(key: string, item: T): Promise<void> {
|
|
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<string[]> {
|
|
const raw = await swStorage.getItem<string[]>('$sw.pre-cache.urls')
|
|
|
|
if (raw === null) {
|
|
await openPreCache()
|
|
return getPreCachedUrls()
|
|
}
|
|
return raw
|
|
}
|
|
|
|
async function getPreCache(): Promise<Cache> {
|
|
const version = await swStorage.getItem<string>('$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<NotificationOptions>
|
|
}
|
|
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<Cache> {
|
|
const version = await swStorage.getItem<number>('$sw.dyn-cache.version')
|
|
// Create cache
|
|
if (version === null) {
|
|
await swStorage.setItem<number>('$sw.dyn-cache.version', now + lifeTime)
|
|
return getDynCache()
|
|
}
|
|
|
|
// Clean outdated cache
|
|
if (version < Date.now()) {
|
|
await swStorage.setItem<number>('$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<string>('$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<string | undefined> {
|
|
const response = await fetch('/api/serviceworker/precache').then(
|
|
(response) => response.json() as Promise<ApiPayload<PrecacheResponse>>,
|
|
)
|
|
|
|
if (response.kind === 'success') {
|
|
return response.data.version
|
|
}
|
|
}
|
|
|
|
async function openPreCache() {
|
|
const response = await fetch('/api/serviceworker/precache').then(
|
|
(response) => response.json() as Promise<ApiPayload<PrecacheResponse>>,
|
|
)
|
|
|
|
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<void>[] = []
|
|
// 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()
|
|
}
|