Compare commits
4 commits
2ded36b38b
...
5669489dc9
Author | SHA1 | Date | |
---|---|---|---|
Julien Oculi | 5669489dc9 | ||
Julien Oculi | c780fcfab6 | ||
Julien Oculi | 0ee41a9e5a | ||
Julien Oculi | f2b39d0288 |
4
dev.ts
4
dev.ts
|
@ -17,8 +17,8 @@ await dev(import.meta.url, './main.ts', {
|
||||||
`\n\t%c Server started %c %chttps://${hostname}:${port}\n`,
|
`\n\t%c Server started %c %chttps://${hostname}:${port}\n`,
|
||||||
'font-weight: bold; background-color: blue',
|
'font-weight: bold; background-color: blue',
|
||||||
'',
|
'',
|
||||||
'color: blue; text-decoration: underline'
|
'color: blue; text-decoration: underline',
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { requestApi } from ':src/utils.ts'
|
||||||
|
|
||||||
export default function RegisterServiceWorker() {
|
export default function RegisterServiceWorker() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
import('./StartServiceWorker.tsx').then(async (mod) => {
|
import(':islands/StartServiceWorker.tsx').then(async (mod) => {
|
||||||
const href = mod.default()
|
const href = mod.default()
|
||||||
const registration = await navigator.serviceWorker.register(href, {
|
const registration = await navigator.serviceWorker.register(href, {
|
||||||
scope: '/',
|
scope: '/',
|
||||||
|
@ -17,7 +17,7 @@ export default function RegisterServiceWorker() {
|
||||||
// })
|
// })
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const subscription = await (async () => {
|
async function getSubscription() {
|
||||||
const currentSubscription = await registration.pushManager
|
const currentSubscription = await registration.pushManager
|
||||||
.getSubscription()
|
.getSubscription()
|
||||||
if (currentSubscription) return currentSubscription
|
if (currentSubscription) return currentSubscription
|
||||||
|
@ -31,9 +31,14 @@ export default function RegisterServiceWorker() {
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey,
|
applicationServerKey,
|
||||||
})
|
})
|
||||||
})()
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscription = await getSubscription()
|
||||||
await requestApi('webpush/subscription', 'POST', subscription)
|
await requestApi('webpush/subscription', 'POST', subscription)
|
||||||
|
} catch (cause) {
|
||||||
|
console.error('Push subscription is not available', { cause })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
51
routes/api/serviceworker/precache.tsx
Normal file
51
routes/api/serviceworker/precache.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { expandGlob } from '$std/fs/mod.ts'
|
||||||
|
import { SessionHandlers } from ':src/session/mod.ts'
|
||||||
|
import { respondApi } from ':src/utils.ts'
|
||||||
|
|
||||||
|
export type PrecacheResponse = { version: string; preCachedUrls: string[] }
|
||||||
|
|
||||||
|
// Updated only at server start
|
||||||
|
const version = crypto.randomUUID()
|
||||||
|
|
||||||
|
export const handler: SessionHandlers = {
|
||||||
|
async GET() {
|
||||||
|
try {
|
||||||
|
const preCachedUrls: string[] = ['/', '/imports/markdown_css']
|
||||||
|
const paths = ['/static/**', '/_fresh/static/**']
|
||||||
|
const routes = '/routes/*/index.tsx'
|
||||||
|
|
||||||
|
//Pre-cache routes
|
||||||
|
for await (const route of expandGlob(routes, { root: '.' })) {
|
||||||
|
if (!route.isFile) continue
|
||||||
|
//@ts-expect-error parentPath is missing from type definition
|
||||||
|
preCachedUrls.push(strip(routes, route.parentPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-cache files
|
||||||
|
for (const path of paths) {
|
||||||
|
for await (const entry of expandGlob(path, { root: '.' })) {
|
||||||
|
if (!entry.isFile) continue
|
||||||
|
preCachedUrls.push(strip(path, entry.path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return respondApi<'success', PrecacheResponse>('success', {
|
||||||
|
version,
|
||||||
|
preCachedUrls,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return respondApi('error', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function strip(root: string, path: string) {
|
||||||
|
return path
|
||||||
|
// Force unix/web separator
|
||||||
|
.replaceAll('\\', '/')
|
||||||
|
.replace(
|
||||||
|
// Remove root slash and glob *
|
||||||
|
root.slice(1).replaceAll('*', ''),
|
||||||
|
'/',
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,26 +1,71 @@
|
||||||
/// <reference no-default-lib="true"/>
|
/// <reference no-default-lib="true"/>
|
||||||
/// <reference lib="webworker" />
|
/// <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
|
// Force load service worker types
|
||||||
const self = globalThis as unknown as ServiceWorkerGlobalScope
|
const self = globalThis as unknown as ServiceWorkerGlobalScope
|
||||||
|
|
||||||
const cacheName = 'v1' //TODO dynamique cache key
|
// Pseudo storage API for SW
|
||||||
const _preCachedPaths = ['/', '/css/*', '/assets/*'] //TODO pre-cache these paths
|
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)
|
||||||
|
|
||||||
export function main() {
|
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) => {
|
self.addEventListener('install', (event) => {
|
||||||
//TODO handle installation
|
// Assign global cache and pre-cached-urls
|
||||||
event.waitUntil(
|
event.waitUntil(openCache())
|
||||||
addToCache([]),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener('activate', () => {
|
self.addEventListener('activate', () => {
|
||||||
//TODO handle activation
|
//TODO handle activation
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener('fetch', (_event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
//TODO add fetch strategies
|
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) => {
|
self.addEventListener('push', (event) => {
|
||||||
|
@ -35,9 +80,82 @@ export function main() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
async function addToCache(ressources: string[]) {
|
|
||||||
const cache = await caches.open(cacheName) //TODO dynamique cache key
|
|
||||||
await cache.addAll(ressources)
|
|
||||||
|
|
||||||
//TODO list statics
|
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