fix(pwa): rewrite and update service worker related to fit recent refactors

This commit is contained in:
Julien Oculi 2025-04-22 16:50:42 +02:00
parent 09e4f401e9
commit 125e645ffd
9 changed files with 61 additions and 90 deletions

View file

@ -1,5 +0,0 @@
import RegisterServiceWorker from ':islands/RegisterServiceWorker.tsx'
export function ProgressiveWebApp() {
return <RegisterServiceWorker />
}

View file

@ -26,6 +26,7 @@
"packages/"
],
"imports": {
"@deno/emit": "jsr:@deno/emit@^0.46.0",
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",
"@std/fs": "jsr:@std/fs@^1.0.6",
"@std/path": "jsr:@std/path@^1.0.8",

View file

@ -1,47 +1,49 @@
import { requestApi } from ':src/utils.ts'
import { useEffect } from 'preact/hooks'
export default function RegisterServiceWorker() {
if ('serviceWorker' in navigator) {
import(':islands/StartServiceWorker.tsx').then(async (mod) => {
const href = mod.default()
const registration = await navigator.serviceWorker.register(href, {
scope: '/',
type: 'module',
})
async function register() {
const registration = await navigator.serviceWorker.register('/sw', {
scope: '/',
type: 'module',
})
// Notification.requestPermission().then((permission) => {
// if (permission !== 'granted') return
// Notification.requestPermission().then((permission) => {
// if (permission !== 'granted') return
// registration.showNotification('Notification permission granted', {
// body: 'Notification is ok.',
// })
// })
// registration.showNotification('Notification permission granted', {
// body: 'Notification is ok.',
// })
// })
async function getSubscription() {
const currentSubscription = await registration.pushManager
.getSubscription()
if (currentSubscription) return currentSubscription
async function getSubscription() {
const currentSubscription = await registration.pushManager
.getSubscription()
if (currentSubscription) return currentSubscription
const applicationServerKey = await requestApi<void, string>(
'webpush/vapid',
'GET',
)
const applicationServerKey = await requestApi<void, string>(
'webpush/vapid',
'GET',
)
return await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
})
}
try {
if (registration.active === null) return
const subscription = await getSubscription()
await requestApi('webpush/subscription', 'POST', subscription)
} catch (cause) {
console.error('Push subscription is not available', { cause })
}
return await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
})
}
return <></>
try {
if (registration.active === null) return
const subscription = await getSubscription()
await requestApi('webpush/subscription', 'POST', subscription)
} catch (cause) {
console.error('Push subscription is not available', { cause })
}
}
export default function RegisterServiceWorker() {
useEffect(() => {
if ('serviceWorker' in navigator) register()
}, [])
return null
}

View file

@ -1,10 +0,0 @@
import { main } from ':src/serviceworker/mod.ts'
const IS_SW = 'onpushsubscriptionchange' in self
export default function StartServiceWorker() {
if (IS_SW) {
main()
}
return new URL(import.meta.url).pathname
}

View file

@ -2,8 +2,8 @@ import { asset, Partial } from 'fresh/runtime'
import { type PageProps } from 'fresh'
import { Footer } from ':components/Footer.tsx'
import { Header } from ':components/Header.tsx'
import { ProgressiveWebApp } from ':components/ProgressiveWebApp.tsx'
import IsOnline from ':islands/IsOnline.tsx'
import RegisterServiceWorker from ':islands/RegisterServiceWorker.tsx'
export default function App(
{ Component, data }: PageProps<{ title?: string } | undefined>,
@ -54,7 +54,7 @@ export default function App(
<Component />
</Partial>
</main>
<ProgressiveWebApp />
<RegisterServiceWorker />
<Footer />
</body>
</html>

View file

@ -1,7 +1,6 @@
import { useCache } from ':src/cache/middleware.ts'
import { useCsp } from ':src/csp/middleware.ts'
import { useSecurityHeaders } from ':src/security_headers/middleware.ts'
import { useServiceworker } from ':src/serviceworker/middleware.ts'
import { useSession } from ':src/session/middleware.ts'
import { SessionStore } from ':src/session/mod.ts'
import { define } from '../utils.ts'
@ -23,7 +22,6 @@ export default define.middleware(async (ctx) => {
useSecurityHeaders(request, response, ctx)
await useCsp(request, response, ctx)
useSession(request, response, ctx)
useServiceworker(request, response, ctx)
useCache(request, response, ctx)
return response

View file

@ -1,37 +1,28 @@
import { expandGlob } from '@std/fs'
import { SessionHandlers } from ':src/session/mod.ts'
import { respondApi } from ':src/utils.ts'
import { BUILD_ID } from '$fresh/src/server/build_id.ts'
import { encodeBase64 } from '@std/encoding'
import { BUILD_ID } from '../../../utils.ts'
export type PrecacheResponse = { version: string; preCachedUrls: string[] }
async function getVersion() {
const versionBytes = new TextEncoder().encode(BUILD_ID)
const versionHash = await crypto.subtle.digest('SHA-256', versionBytes)
return encodeBase64(versionHash)
}
export const handler: SessionHandlers = {
async GET() {
try {
const preCachedUrls: string[] = ['/', '/imports/markdown_css']
const paths = ['/static/**', '/_fresh/static/**']
const routes = '/routes/*/index.tsx'
const version = await getVersion()
const version = BUILD_ID
//Pre-cache routes
for await (const route of expandGlob(routes, { root: '.' })) {
if (!route.isFile) continue
//@ts-expect-error parentPath is missing from type definition
const path = route.parentPath as string
preCachedUrls.push(path.replace('routes', '').replace('\\', '/'))
if ('isFile' in route && !route.isFile) continue
preCachedUrls.push(strip('/routes/**', route.path))
}
// Pre-cache files
for (const path of paths) {
for await (const entry of expandGlob(path, { root: '.' })) {
if (!entry.isFile) continue
if ('isFile' in entry && !entry.isFile) continue
preCachedUrls.push(strip(path, entry.path))
}
}
@ -47,12 +38,10 @@ export const handler: SessionHandlers = {
}
function strip(root: string, path: string) {
return path
// Force unix/web separator
.replaceAll('\\', '/')
.replace(
// Remove root slash and glob *
root.slice(1).replaceAll('*', ''),
'/',
)
// Remove root slash and glob *
const base = root.slice(1).replaceAll('*', '').replaceAll('\\', '/')
// Force unix/web separator
const pathname = path.replaceAll('\\', '/').replace('/index.tsx', '')
return `/${pathname.slice(base.length)}`
}

View file

@ -1,12 +0,0 @@
import { FreshContext } from 'fresh'
export function useServiceworker(
_request: Request,
response: Response,
ctx: FreshContext,
) {
// Allow service worker to serve root scope
if (ctx.url.pathname.endsWith('island-startserviceworker.js')) {
response.headers.set('Service-Worker-Allowed', '/')
}
}

View file

@ -4,7 +4,7 @@
import type { JsonValue } from '@std/json'
import { ApiPayload } from ':src/utils.ts'
import type { PrecacheResponse } from '../../routes/api/serviceworker/precache.tsx'
import { FetchStrategy } from './src/fetch_strategy.ts'
import { FetchStrategy } from ':src/serviceworker/src/fetch_strategy.ts'
// Force load service worker types
const self = globalThis as unknown as ServiceWorkerGlobalScope
@ -120,6 +120,14 @@ if (IS_SW) {
)
}
})
self.addEventListener('notificationclick', (event) => {
console.log('SW - NOT_CLICK', event)
})
self.addEventListener('notificationclose', (event) => {
console.log('SW - NOT_CLOSE', event)
})
}
async function fetchHandler(event: FetchEvent) {