diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 8bb9ffc..79922e6 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -21,8 +21,8 @@ Open `http://localhost:5173` in Chrome or Edge. Firefox and Safari work too, the
- `src/lib/git` wraps isomorphic-git. `handle-fs.ts` is the file system adapter that lets it run on the project folder, `engine.ts` the operations, `errors.ts` turns its errors into something readable. The panel lives in `src/lib/ui/git`, one file per tab.
- `src/lib/project` holds the open files, the project tree and everything the toolbar calls.
- `src/lib/ui` are the Svelte components, `src/routes/editor` is the editor page itself.
-- `static/sw.js` is the service worker that resolves TeX Live files on demand: cache first, then the bundled subset, then the jsDelivr mirror, then the fallback server.
-- `static/texlive/cache` is the bundled package subset. If you change anything in there, bump `BUNDLE_VERSION` in `static/sw.js` and `WARMED_VERSION` in `src/lib/compiler/offline-cache.ts`, otherwise users keep the old files in their cache.
+- `src/service-worker.ts` is the service worker. It keeps every file of the current build in a cache of its own, so a page that was open before a deploy still gets its chunks, and it resolves TeX Live files on demand: cache first, then the bundled subset, then the jsDelivr mirror, then the fallback server.
+- `static/texlive/cache` is the bundled package subset. If you change anything in there, bump `BUNDLE_VERSION` in `src/service-worker.ts` and `WARMED_VERSION` in `src/lib/compiler/offline-cache.ts`, otherwise users keep the old files in their cache.
- `static/swiftlatex` is the engine. It is a build artifact, not something to edit by hand.
## What helps most
diff --git a/src/lib/compiler/offline-cache.ts b/src/lib/compiler/offline-cache.ts
index 6dd33ad..a86c46e 100644
--- a/src/lib/compiler/offline-cache.ts
+++ b/src/lib/compiler/offline-cache.ts
@@ -6,7 +6,7 @@ import { base } from '$app/paths';
// down (the cache writes compete with compile i/o otherwise).
const FLAG = 'texbrain-texlive-warmed';
-// matches BUNDLE_VERSION in sw.js: when the bundle changes, the warm set
+// matches BUNDLE_VERSION in service-worker.ts: when the bundle changes, the warm set
// is evicted and needs one fresh run
const WARMED_VERSION = '4';
const CONCURRENCY = 2;
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 3050e7b..70881b9 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -3,14 +3,25 @@
import Toast from '$lib/ui/Toast.svelte';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
+ import { beforeNavigate } from '$app/navigation';
+ import { updated } from '$app/state';
import { preferences } from '$lib/stores/preferences';
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
+ // after a deploy the chunks this tab knows about are gone from the server.
+ // a full page load on the next navigation picks up the new build instead
+ // of failing on a missing file
+ beforeNavigate(({ willUnload, to }) => {
+ if (updated.current && !willUnload && to?.url) {
+ location.href = to.url.href;
+ }
+ });
+
onMount(() => {
if (browser && 'serviceWorker' in navigator) {
- navigator.serviceWorker.register('/sw.js').catch(() => {});
+ navigator.serviceWorker.register('/service-worker.js').catch(() => {});
// keep the service worker's texlive mirror config in sync
const unsubscribe = preferences.subscribe((prefs) => {
diff --git a/static/sw.js b/src/service-worker.ts
similarity index 53%
rename from static/sw.js
rename to src/service-worker.ts
index 2a036a8..69e0a58 100644
--- a/static/sw.js
+++ b/src/service-worker.ts
@@ -1,3 +1,20 @@
+///
+///
+///
+///
+
+// two jobs: keep the app's own files around so a page and its chunks always
+// come from the same build (even after a deploy replaced everything on the
+// server), and resolve tex live files on demand for the engine
+
+import { build, files, prerendered, version } from '$service-worker';
+
+const sw = self as unknown as ServiceWorkerGlobalScope;
+
+// one cache per deploy for the app itself, dropped when the next one arrives
+const APP_CACHE = `texbrain-app-${version}`;
+// these outlive deploys: the offline warmer's copies, the tex live files
+// fetched so far, and the mirror setting
const CACHE_NAME = 'texbrain-v1';
const TEXLIVE_CACHE = 'texbrain-texlive-v1';
const CONFIG_CACHE = 'texbrain-config-v1';
@@ -21,106 +38,120 @@ const LSR_URL = STATIC_MIRROR + '/ls-R';
// texliveMirror preference.
const DEFAULT_MIRROR = 'https://texlive.texlyre.org/';
-const STATIC_ASSETS = [
- '/',
- '/editor',
- '/favicon.svg',
- '/manifest.json'
+// the app's chunks, the pages, and the handful of small static files. the
+// bundled tex subset and the engine are big and fetched on demand instead
+const PRECACHE = [
+ ...build,
+ ...prerendered,
+ ...files.filter(f => /^\/(favicon\.svg|manifest\.json|texbrain-logo-readme\.svg)$/.test(f))
];
// extensions kpathsea probes when a request comes without one,
// same order as the engine uses for its local lookups
const TEX_EXTENSIONS = ['', '.tfm', '.pfb', '.vf', '.sty', '.cls', '.fd', '.def', '.cfg', '.clo', '.tex', '.ltx', '.map', '.enc', '.dfu', '.ldf'];
-let mirrorOverride = null; // null = not set, '' = disabled, string = url
-let manifestPromise = null;
-let lsrPromise = null;
-
-// cache static assets on install
-self.addEventListener('install', (event) => {
- event.waitUntil(
- caches.open(CACHE_NAME).then((cache) => {
- return cache.addAll(STATIC_ASSETS);
- })
- );
- self.skipWaiting();
+let mirrorOverride: string | null = null; // null = not set, '' = disabled, string = url
+let manifestPromise: Promise> | null = null;
+let lsrPromise: Promise