diff --git a/islands/StartServiceWorker.tsx b/islands/StartServiceWorker.tsx
index 764f88f..ffcf7c2 100644
--- a/islands/StartServiceWorker.tsx
+++ b/islands/StartServiceWorker.tsx
@@ -4,7 +4,7 @@ const IS_SW = 'onpushsubscriptionchange' in self
export default function StartServiceWorker() {
if (IS_SW) {
- main(location.origin)
+ main()
}
return new URL(import.meta.url).pathname
}
diff --git a/src/serviceworker/mod.ts b/src/serviceworker/mod.ts
index 307b726..6929001 100644
--- a/src/serviceworker/mod.ts
+++ b/src/serviceworker/mod.ts
@@ -1,30 +1,71 @@
///
///
+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
-export async function main(origin: string) {
- const cacheConfig = await fetch(
- new URL('/api/serviceworker/precache', origin),
- ).then((response) => response.json() as Promise)
- const cacheName = cacheConfig.version
+// 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.cache.urls')
+ if (raw === null) {
+ await openCache()
+ return getPreCachedUrls()
+ }
+ return raw
+}
+
+async function getCache(): Promise {
+ const version = await swStorage.getItem('$sw.cache.version')
+ if (version === null) {
+ await openCache()
+ return getCache()
+ }
+ return caches.open(version)
+}
+
+const IS_SW = 'onpushsubscriptionchange' in self
+if (IS_SW) {
self.addEventListener('install', (event) => {
- event.waitUntil(
- //precache static files and routes index
- addToCache(cacheName, cacheConfig.files)
- )
+ // Assign global cache and pre-cached-urls
+ event.waitUntil(openCache())
})
self.addEventListener('activate', () => {
//TODO handle activation
})
- self.addEventListener('fetch', (_event) => {
- //TODO add fetch strategies
+ 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) => {
@@ -40,7 +81,81 @@ export async function main(origin: string) {
})
}
-async function addToCache(cacheName: string, ressources: string[]) {
- const cache = await caches.open(cacheName)
- await cache.addAll(ressources)
+async function fetchHandler(event: FetchEvent) {
+ const url = new URL(event.request.url)
+ const method = event.request.method
+ const preCachedUrls = await getPreCachedUrls()
+ const cache = await getCache()
+
+ // Cache first for "pre-cached-urls"
+ if (preCachedUrls.includes(url.pathname) && method === 'GET') {
+ const cached = await cache.match(event.request) ??
+ await fetch(event.request).catch(() => null) ??
+ await cache.match(event.request, { ignoreSearch: true })
+
+ if (cached === undefined) {
+ throw new Error(`no cache available for pre-cached-url "${url}"`)
+ }
+ return cached
+ }
+
+ // Cache first and refresh
+ if (url.origin === location.origin && method === 'GET') {
+ const cached = await cache.match(event.request)
+
+ const response = fetch(event.request)
+ .then((response) => {
+ // Update cache
+ cache.put(event.request, response.clone())
+ return response
+ })
+ .catch((cause) => {
+ // Try serve cache if no network
+ if (cached === undefined) {
+ throw new Error(`no cache available for "${url}"`, { cause })
+ }
+ return cached
+ })
+
+ if (cached === undefined) {
+ return response
+ }
+
+ return cached
+ }
+
+ // Network only
+ return fetch(event.request)
+}
+
+async function openCache() {
+ 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.cache.version', version)
+ await swStorage.setItem('$sw.cache.urls', preCachedUrls)
+
+ return { cache, version, preCachedUrls }
+}
+
+export function main() {
+ console.assert()
}