121 lines
3.6 KiB
JavaScript
121 lines
3.6 KiB
JavaScript
/**
|
|
* Network policy for the `/control/` application shell.
|
|
*
|
|
* `__PWA_BUILD_ID__` is replaced by Vite before emission. Navigations use a
|
|
* network-first strategy so fresh HTML can reference the newest hashed bundle;
|
|
* known static assets use cache-first. The worker never handles API, WebSocket
|
|
* or OBS routes because they are outside both this fetch policy and its scope.
|
|
*/
|
|
const buildId = '__PWA_BUILD_ID__'
|
|
const cacheName = `lxc-control-v2-shell-${buildId}`
|
|
const shellPath = '/control/'
|
|
const shellRoutes = [
|
|
'/control/',
|
|
'/control/invitations',
|
|
'/control/login',
|
|
'/control/register',
|
|
'/control/setup',
|
|
]
|
|
const stableAssets = [
|
|
'/control/manifest.webmanifest',
|
|
'/pwa/icon-192.png',
|
|
'/pwa/icon-512.png',
|
|
'/pwa/icon-maskable-512.png',
|
|
'/pwa/apple-touch-icon.png',
|
|
]
|
|
|
|
async function precacheShell() {
|
|
const cache = await caches.open(cacheName)
|
|
const response = await fetch(shellPath, { cache: 'reload', credentials: 'same-origin' })
|
|
if (!response.ok) throw new Error(`Cannot precache control shell: ${response.status}`)
|
|
|
|
const html = await response.clone().text()
|
|
await Promise.all(shellRoutes.map(route => cache.put(route, response.clone())))
|
|
const generatedAssets = [...html.matchAll(/(?:src|href)="(\/assets\/[^"?#]+)"/g)].map(
|
|
match => match[1],
|
|
)
|
|
await cache.addAll([...new Set([...stableAssets, ...generatedAssets])])
|
|
}
|
|
|
|
self.addEventListener('install', event => {
|
|
event.waitUntil(precacheShell())
|
|
})
|
|
|
|
self.addEventListener('activate', event => {
|
|
event.waitUntil(
|
|
(async () => {
|
|
const names = await caches.keys()
|
|
await Promise.all(
|
|
names
|
|
.filter(name => name.startsWith('lxc-control-v2-shell-') && name !== cacheName)
|
|
.map(name => caches.delete(name)),
|
|
)
|
|
await self.clients.claim()
|
|
})(),
|
|
)
|
|
})
|
|
|
|
self.addEventListener('message', event => {
|
|
if (event.data?.type === 'SKIP_WAITING') event.waitUntil(self.skipWaiting())
|
|
})
|
|
|
|
function isControlShell(pathname) {
|
|
return pathname.startsWith('/control/')
|
|
}
|
|
|
|
function isStaticAsset(pathname) {
|
|
return (
|
|
pathname.startsWith('/assets/') ||
|
|
pathname.startsWith('/pwa/') ||
|
|
pathname === '/control/manifest.webmanifest'
|
|
)
|
|
}
|
|
|
|
async function navigationResponse(request) {
|
|
const cache = await caches.open(cacheName)
|
|
const pathname = new URL(request.url).pathname
|
|
try {
|
|
const response = await fetch(request)
|
|
if (
|
|
shellRoutes.includes(pathname) &&
|
|
response.ok &&
|
|
response.headers.get('content-type')?.includes('text/html')
|
|
) {
|
|
await cache.put(pathname, response.clone())
|
|
}
|
|
return response
|
|
} catch {
|
|
return (
|
|
(await cache.match(request, { ignoreSearch: true })) ||
|
|
(await cache.match(shellPath)) ||
|
|
Response.error()
|
|
)
|
|
}
|
|
}
|
|
|
|
async function staticResponse(request) {
|
|
const cached = await caches.match(request, { ignoreSearch: false })
|
|
if (cached) return cached
|
|
const response = await fetch(request)
|
|
if (response.ok) {
|
|
const cache = await caches.open(cacheName)
|
|
await cache.put(request, response.clone())
|
|
}
|
|
return response
|
|
}
|
|
|
|
self.addEventListener('fetch', event => {
|
|
const request = event.request
|
|
if (request.method !== 'GET') return
|
|
const url = new URL(request.url)
|
|
if (url.origin !== self.location.origin) return
|
|
|
|
// Authentication, component data, WebSockets and OBS are outside this
|
|
// worker's scope and are never cached. Keep the path check as defence in depth.
|
|
if (request.mode === 'navigate' && isControlShell(url.pathname)) {
|
|
event.respondWith(navigationResponse(request))
|
|
return
|
|
}
|
|
if (isStaticAsset(url.pathname)) event.respondWith(staticResponse(request))
|
|
})
|