feat(pwa): ✨ basic offline mode and sw fetch strategies
This commit is contained in:
parent
c780fcfab6
commit
5669489dc9
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,30 +1,71 @@
|
|||
/// <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
|
||||
|
||||
export async function main(origin: string) {
|
||||
const cacheConfig = await fetch(
|
||||
new URL('/api/serviceworker/precache', origin),
|
||||
).then((response) => response.json() as Promise<PrecacheResponse>)
|
||||
const cacheName = cacheConfig.version
|
||||
// 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.cache.urls')
|
||||
if (raw === null) {
|
||||
await openCache()
|
||||
return getPreCachedUrls()
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
async function getCache(): Promise<Cache> {
|
||||
const version = await swStorage.getItem<string>('$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<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.cache.version', version)
|
||||
await swStorage.setItem('$sw.cache.urls', preCachedUrls)
|
||||
|
||||
return { cache, version, preCachedUrls }
|
||||
}
|
||||
|
||||
export function main() {
|
||||
console.assert()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue