MyKindlebox/mykindlebox/client/public/sw.js

92 lines
3.5 KiB
JavaScript

const CACHE_NAME = 'mykindlebox-shell-v1'
const SHELL_URLS = ['/', '/favicon.svg', '/icon-192.png', '/manifest.json']
// Vendored Tesseract OCR runtime: the worker, the three feature-detect WASM core
// variants (relaxedSIMD / SIMD / plain LSTM — only one is loaded per browser, but
// we can't know which ahead of time, so all are cached), and the English language
// data. These are large (~16 MB total) but static and versioned with the app, so
// they're precached on install and served cache-first (see the fetch handler):
// once stored they never need re-fetching, and the pedigree scanner must work
// fully offline. See FEATURE_OCR_Pedigree_Scan.md Q7.
//
// UPGRADE CONTRACT: cache-first means installed clients serve these from cache
// forever. If tesseract.js is ever upgraded (package.json pins the exact version
// for this reason) the files under public/tesseract/ must be re-vendored AND
// CACHE_NAME must be bumped, or existing clients keep the old worker/WASM while
// the app bundle uses the new library.
const TESSERACT_URLS = [
'/tesseract/worker.min.js',
'/tesseract/tesseract-core-lstm.wasm.js',
'/tesseract/tesseract-core-simd-lstm.wasm.js',
'/tesseract/tesseract-core-relaxedsimd-lstm.wasm.js',
'/tesseract/eng.traineddata',
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) =>
Promise.all([
cache.addAll(SHELL_URLS),
// The ~16 MB Tesseract batch is best-effort: if it fails (flaky first
// load, missing file in a bad deploy), the shell must still install —
// the cache-first fetch branch below backfills these lazily the next
// time the scanner runs online.
cache.addAll(TESSERACT_URLS).catch(() => {}),
]),
),
)
})
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))))
.then(() => self.clients.claim()),
)
})
// Network-first with a cache fallback for the app shell (HTML/JS/CSS/static
// assets), so a previously-visited bundle still loads offline. API requests
// are left alone — they should fail fast so the UI can show its own inline
// "offline" state instead of serving stale data silently.
self.addEventListener('fetch', (event) => {
const { request } = event
if (request.method !== 'GET') return
const url = new URL(request.url)
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/uploads/')) return
// Tesseract assets are static and large — cache-first. Serve the stored copy
// outright when present (no network round-trip, works offline), and only hit
// the network on a cache miss, storing the result for next time.
if (url.pathname.startsWith('/tesseract/')) {
event.respondWith(
caches.match(request).then(
(cached) =>
cached ??
fetch(request).then((response) => {
if (response.ok) {
const copy = response.clone()
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy))
}
return response
}),
),
)
return
}
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const copy = response.clone()
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy))
}
return response
})
.catch(() => caches.match(request).then((cached) => cached ?? caches.match('/'))),
)
})