Compare commits

..

No commits in common. "5669489dc9284af55d8fd7bdd1726b442d12fac1" and "2ded36b38bd8b33941b4a102a8f650a9b70364b2" have entirely different histories.

4 changed files with 26 additions and 200 deletions

4
dev.ts
View file

@ -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'
) )
}), })
}, },
}) })

View file

@ -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(':islands/StartServiceWorker.tsx').then(async (mod) => { import('./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() {
// }) // })
// }) // })
async function getSubscription() { const subscription = await (async () => {
const currentSubscription = await registration.pushManager const currentSubscription = await registration.pushManager
.getSubscription() .getSubscription()
if (currentSubscription) return currentSubscription if (currentSubscription) return currentSubscription
@ -31,14 +31,9 @@ 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 })
}
}) })
} }

View file

@ -1,51 +0,0 @@
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('*', ''),
'/',
)
}

View file

@ -1,71 +1,26 @@
/// <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
// Pseudo storage API for SW const cacheName = 'v1' //TODO dynamique cache key
const swStorage = { const _preCachedPaths = ['/', '/css/*', '/assets/*'] //TODO pre-cache these paths
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) { export function main() {
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) => {
// Assign global cache and pre-cached-urls //TODO handle installation
event.waitUntil(openCache()) event.waitUntil(
addToCache([]),
)
}) })
self.addEventListener('activate', () => { self.addEventListener('activate', () => {
//TODO handle activation //TODO handle activation
}) })
self.addEventListener('fetch', (event) => { self.addEventListener('fetch', (_event) => {
const url = new URL(event.request.url) //TODO add fetch strategies
// Don't handle 3rd party request
if (url.origin !== location.origin) return
event.respondWith(fetchHandler(event))
}) })
self.addEventListener('push', (event) => { self.addEventListener('push', (event) => {
@ -80,82 +35,9 @@ if (IS_SW) {
} }
}) })
} }
async function addToCache(ressources: string[]) {
const cache = await caches.open(cacheName) //TODO dynamique cache key
await cache.addAll(ressources)
async function fetchHandler(event: FetchEvent) { //TODO list statics
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()
} }