84 lines
3.0 KiB
JavaScript
84 lines
3.0 KiB
JavaScript
const BUILD = '3b3c3c52da91'
|
|
// The service worker of the BM student app. The Vite plugin bm-service-worker
|
|
// (vite.config.ts) emits this file as /sw.js and replaces the build id above,
|
|
// so every build that changes an asset gets a fresh cache and an update prompt.
|
|
//
|
|
// It is deliberately minimal: hashed /assets/* files are cached, navigations
|
|
// go to the network with an offline fallback page, and everything else is
|
|
// left to the browser untouched.
|
|
|
|
const CACHE = 'bm-' + BUILD
|
|
const OFFLINE_URL = '/offline.html'
|
|
|
|
// API, realtime, upload and admin paths are never intercepted. The speaker
|
|
// deck (/_presentations) holds notes and text meant only for its owner, and
|
|
// no response from these prefixes may ever land in a device cache. The list
|
|
// mirrors reservedPrefixes in plugins/jz/bm/static.go plus /ws and the admin.
|
|
const SKIP_PREFIXES = ['/_user', '/_bm', '/_quizzes', '/_presentations', '/api', '/ws', '/storage', '/bmadmin']
|
|
|
|
// swRoute classifies a request: 'skip' leaves it to the browser, 'page' is a
|
|
// network-only navigation with an offline fallback, 'asset' is cache-first.
|
|
function swRoute(method, href, mode, origin) {
|
|
if (method !== 'GET') return 'skip'
|
|
let url
|
|
try {
|
|
url = new URL(href)
|
|
} catch {
|
|
return 'skip'
|
|
}
|
|
if (url.origin !== origin) return 'skip'
|
|
const path = url.pathname
|
|
// The reserved check runs before the navigation check, so even a top-level
|
|
// navigation to /storage/... or /bmadmin is never touched.
|
|
for (const prefix of SKIP_PREFIXES) {
|
|
if (path === prefix || path.startsWith(prefix + '/')) return 'skip'
|
|
}
|
|
if (mode === 'navigate') return 'page'
|
|
if (path.startsWith('/assets/')) return 'asset'
|
|
return 'skip'
|
|
}
|
|
|
|
self.addEventListener('install', (event) => {
|
|
// No skipWaiting here: a new worker waits until the user asks to refresh.
|
|
event.waitUntil(caches.open(CACHE).then((cache) => cache.add(new Request(OFFLINE_URL, { cache: 'reload' }))))
|
|
})
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((keys) => Promise.all(keys.filter((key) => key.startsWith('bm-') && key !== CACHE).map((key) => caches.delete(key))))
|
|
.then(() => self.clients.claim()),
|
|
)
|
|
})
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request
|
|
const route = swRoute(req.method, req.url, req.mode, self.location.origin)
|
|
if (route === 'skip') return
|
|
|
|
if (route === 'page') {
|
|
// Navigations are never stored: an HTML response could carry user state.
|
|
event.respondWith(
|
|
fetch(req).catch(() => caches.match(OFFLINE_URL).then((res) => res || Response.error())),
|
|
)
|
|
return
|
|
}
|
|
|
|
event.respondWith(
|
|
caches.open(CACHE).then((cache) =>
|
|
cache.match(req).then((hit) => {
|
|
if (hit) return hit
|
|
return fetch(req).then((res) => {
|
|
if (res.status === 200 && res.type === 'basic') cache.put(req, res.clone())
|
|
return res
|
|
})
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting()
|
|
})
|