Files
sm-bm-plugin/public/sw.js
Jakub Zych ebd9117f8c feat: lecturer on class sessions and in the session course ref
The schedule showed a lecturer only where the session notes named one
(Globalizacja), so the student plan had nobody under the other courses.
The session JSON now carries the course lecturer in its course ref and an
optional per-session lecturer (new nullable column, admin field, seed key)
for courses shared between two people. The student app shows the
session's own lecturer or, failing that, the course's.
2026-10-17 11:20:11 +02:00

84 lines
3.0 KiB
JavaScript

const BUILD = '3394fd5feb39'
// 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()
})