Files
odysseus/static/sw.js
T

278 lines
11 KiB
JavaScript

// static/sw.js — Odysseus PWA Service Worker
// Strategy:
// - HTML (navigation): network-first, cache fallback. Code graph updates
// need the current app shell on the reload the user actually performs.
// - JS/CSS (/static/*.js|.css): network-first, cache fallback for offline.
// (So code/style edits show up on a normal reload, no manual cache clear.)
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
// - API / non-GET: never cached.
// Bump CACHE_NAME whenever the precache list or SW logic changes.
const CACHE_NAME = 'odysseus-v636-shell-toggle-authority';
// KaTeX resolves these from its own stylesheet, so caching the CSS without them
// gives offline math fallback glyphs instead of proper typesetting.
const KATEX_FONTS = [
'AMS-Regular', 'Caligraphic-Bold', 'Caligraphic-Regular',
'Fraktur-Bold', 'Fraktur-Regular',
'Main-Bold', 'Main-BoldItalic', 'Main-Italic', 'Main-Regular',
'Math-BoldItalic', 'Math-Italic',
'SansSerif-Bold', 'SansSerif-Italic', 'SansSerif-Regular',
'Script-Regular',
'Size1-Regular', 'Size2-Regular', 'Size3-Regular', 'Size4-Regular',
'Typewriter-Regular',
].map(name => `/static/lib/katex/fonts/KaTeX_${name}.woff2`);
// Two lists, two jobs — they are no longer the same set and must not be
// "resynced" back into one:
//
// PRECACHE = the app shell. Mirrors the <script type="module"> tags
// and <link rel="stylesheet"> in index.html — i.e. what
// loads before first paint.
// PANEL_PRECACHE = modules that index.html deliberately does NOT load,
// because js/panels.js imports them on first use. They are
// off the critical path, not out of the offline manifest:
// without them here, a panel the user never opened while
// online could not open offline at all.
//
// Both are fetched at install time, in the background. Entries must match the
// exact URL the browser requests, query string included.
const PRECACHE = [
'/',
'/static/style.css?v=20260910researchmobilebuttons41',
'/static/app.js?v=20260910shelltoggle3',
'/static/js/storage.js',
'/static/js/appConfig.js',
'/static/js/ui.js?v=20260908weekhoverfix1',
'/static/js/markdown.js',
'/static/js/dragSort.js',
'/static/js/sessions.js',
'/static/js/memory.js',
'/static/js/skills.js?v=20260909kebabconsistency1',
'/static/js/skillsMetrics.js?v=20260908autonomousskills1',
'/static/js/tourHints.js',
'/static/js/fileHandler.js?v=20260909mobileattachmentedit1',
'/static/js/voiceRecorder.js',
'/static/js/actionMenuOrder.js',
'/static/js/models.js',
'/static/js/rag.js',
'/static/js/presets.js?v=20260908personaname1',
'/static/js/search.js',
'/static/js/spinner.js',
'/static/js/tts-ai.js',
'/static/js/document.js?v=20260910minimizedcontext1',
'/static/js/gallery.js?v=20260910promptcopy1',
'/static/js/chatRenderer.js?v=20260910streamlinks2',
'/static/js/codeRunner.js?v=20260831richtexttools91',
'/static/js/chatStream.js?v=20260909cardlayout1',
'/static/js/chat.js?v=20260910shelltoggle1',
'/static/js/cookbook.js',
'/static/js/search-chat.js',
'/static/js/compare/index.js?v=20260909mobilepaneaddscroll1',
'/static/js/compare/vote.js?v=20260828resendcaldrag1',
'/static/js/colorPicker.js?v=20260910eyedropper1',
'/static/js/panels.js?v=20260909movepicklayer1',
'/static/js/theme.js?v=20260909effectspeed1',
'/static/js/censor.js',
'/static/js/settings.js?v=20260909defaultmodelfix1',
'/static/js/admin.js?v=20260908notificationcopy1',
'/static/js/init.js?v=20260829chatstyle12',
'/static/js/slashCommands.js?v=20260902tuiharness1',
'/static/js/research/jobs.js?v=20260910researcherrorpersist1',
'/static/js/emailInbox.js?v=20260903emailsend2',
'/static/js/emailLibrary/utils.js',
'/static/js/emailLibrary/signatureFold.js',
'/static/js/emailLibrary/state.js',
'/static/js/notes.js?v=20260910drawmerge2',
'/static/js/tasks.js?v=20260910tasksortpicker1',
'/static/js/calendar.js?v=20260903weekscrollstable1',
'/static/js/calendar/utils.js',
'/static/js/calendar/reminders.js',
'/static/js/group.js',
'/static/js/keyboard-shortcuts.js?v=20260829chatstyle12',
'/static/js/sidebar-layout.js?v=20260910sidebarbounce1',
'/static/js/tileManager.js?v=20260910responsivebounds1',
'/static/js/section-management.js',
'/static/lib/highlight.min.js',
// Math turns up in ordinary answers and KaTeX is small, so precaching it and
// its fonts keeps formulas typeset offline. Mermaid is deliberately NOT
// precached: at 3.5 MB it would re-download on every CACHE_NAME bump, a poor
// trade for a library most sessions never touch. The cache-first rule below
// picks it up the first time a diagram renders, which is also when it starts
// mattering offline.
'/static/lib/katex/katex.min.js',
'/static/lib/katex/katex.min.css',
...KATEX_FONTS,
];
// Lazily-imported panel modules (js/panels.js). Not in index.html by design;
// precached so the panel still opens with no network.
const PANEL_PRECACHE = [
// Image editor — galleryEditor.js and its js/editor/ graph.
'/static/js/galleryEditor.js?v=20260909movepicklayer1',
'/static/js/editor/ai-inpaint.js?v=20260708match1',
'/static/js/editor/ai-models.js',
'/static/js/editor/ai-rembg.js',
'/static/js/editor/ai-tool-runner.js',
'/static/js/editor/ai-tools-misc.js',
'/static/js/editor/build/controls.js?v=20260830editor2',
'/static/js/editor/build/popups.js',
'/static/js/editor/build/right-panel.js',
'/static/js/editor/build/toolbar.js?v=20260830editor2',
'/static/js/editor/build/topbar.js',
'/static/js/editor/build/transform-popup.js',
'/static/js/editor/canvas-coords.js',
'/static/js/editor/canvas-events.js',
'/static/js/editor/canvas-navigation.js',
'/static/js/editor/canvas-transforms.js',
'/static/js/editor/brush-engine.js',
'/static/js/editor/brush-presets.js',
'/static/js/editor/checkerboard.js',
'/static/js/editor/clipboard-and-drop.js',
'/static/js/editor/composite-helpers.js',
'/static/js/editor/document-codec.js',
'/static/js/editor/adjustment-layer.js',
'/static/js/editor/adjustments-worker.js',
'/static/js/editor/thumbnail-worker.js',
'/static/js/editor/serialization-worker.js',
'/static/js/editor/effects.js',
'/static/js/editor/gradient-stops.js',
'/static/js/editor/render-cancellation.js',
'/static/js/editor/effects-worker.js',
'/static/js/editor/document-geometry.js',
'/static/js/editor/export-dialog.js',
'/static/js/editor/selection-mask.js',
'/static/js/editor/filters/blur.js',
'/static/js/editor/filters/edge-feather.js',
'/static/js/editor/fx/adj-popup.js',
'/static/js/editor/fx/filter-string.js',
'/static/js/editor/fx/histogram.js',
'/static/js/editor/fx/pixel-pass.js',
'/static/js/editor/harmonize-masks.js',
'/static/js/editor/history-panel.js',
'/static/js/editor/history-budget.js',
'/static/js/editor/keyboard-shortcuts.js',
'/static/js/editor/layer-helpers.js',
'/static/js/editor/layer-groups.js',
'/static/js/editor/layer-clipping.js',
'/static/js/editor/multi-transform.js',
'/static/js/editor/direct-manipulation-session.js',
'/static/js/editor/placed-layer.js',
'/static/js/editor/layer-selection.js',
'/static/js/editor/layer-geometry.js',
'/static/js/editor/precision-guides.js',
'/static/js/editor/layer-panel.js',
'/static/js/editor/mask-utils.js',
'/static/js/editor/shortcuts-popover.js',
'/static/js/editor/slider-ux.js',
'/static/js/editor/snap.js',
'/static/js/editor/state.js',
'/static/js/editor/stroke-pipeline.js',
'/static/js/editor/stroke-tool-sliders.js',
'/static/js/editor/text-layer.js',
'/static/js/editor/text-edit-overlay.js',
'/static/js/editor/shape-layer.js',
'/static/js/editor/transform-frame-geometry.js',
'/static/js/editor/tools/clone.js',
'/static/js/editor/tools/eyedropper.js',
'/static/js/editor/tools/crop.js',
'/static/js/editor/tools/flood-fill.js',
'/static/js/editor/tools/gradient.js',
'/static/js/editor/tools/lasso-mask.js',
'/static/js/editor/tools/lasso.js',
'/static/js/editor/tools/marquee.js',
'/static/js/editor/tools/move.js',
'/static/js/editor/tools/stroke.js',
'/static/js/editor/tools/transform-drag.js',
'/static/js/editor/tools/transform-handles.js',
'/static/js/editor/tools/transform-session.js',
'/static/js/editor/tools/wand.js',
'/static/js/editor/wire-import.js',
'/static/js/editor/wire-inpaint-controls.js?v=20260708match1',
'/static/js/editor/wire-merge-buttons.js',
'/static/js/editor/wire-selection-controls.js',
'/static/js/editor/wire-topbar-menus.js',
'/static/js/editor/wire-topbar-overflow.js',
'/static/js/editor/wire-topbar.js',
'/static/js/editor/wire-view-menu.js',
];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_NAME).then(cache =>
// addAll is atomic — if any item fails, none are cached. Use individual
// puts so a single 404 can't block the whole install.
Promise.all(
[...PRECACHE, ...PANEL_PRECACHE].map(url =>
fetch(url, { cache: 'reload' })
.then(res => res.ok ? cache.put(url, res) : null)
.catch(() => null)
)
)
)
);
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
// Never touch API calls or non-GET.
if (url.pathname.startsWith('/api/') || e.request.method !== 'GET') return;
// HTML navigation: network-first app shell — but ONLY for the SPA root.
// Other navigations (e.g. a deep-linked /static/*.html page) must go to the
// network/static handlers below; otherwise every navigation was served the
// app index, replacing the page the user actually asked for.
if (e.request.mode === 'navigate' && url.pathname === '/') {
e.respondWith(
caches.open(CACHE_NAME).then(async cache => {
const cached = await cache.match('/');
return fetch(e.request).then(res => {
if (res && res.ok) cache.put('/', res.clone());
return res;
}).catch(() => cached);
})
);
return;
}
// JS/CSS: network-first — always try the network so code/style edits show up
// on a normal reload; fall back to cache only when offline.
if (url.pathname.startsWith('/static/') && /\.(js|css)(\?|$)/.test(url.pathname + url.search)) {
e.respondWith(
fetch(e.request).then(res => {
if (res && res.ok) {
const copy = res.clone();
caches.open(CACHE_NAME).then(cache => cache.put(e.request, copy));
}
return res;
}).catch(() => caches.match(e.request))
);
return;
}
// Other static assets (images, fonts, libs): cache-first with background refresh.
if (url.pathname.startsWith('/static/')) {
e.respondWith(
caches.open(CACHE_NAME).then(async cache => {
const cached = await cache.match(e.request);
const fetching = fetch(e.request).then(res => {
if (res && res.ok) cache.put(e.request, res.clone());
return res;
}).catch(() => cached);
return cached || fetching;
})
);
return;
}
});