You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
94 lines
3.9 KiB
JavaScript
94 lines
3.9 KiB
JavaScript
// Service worker with TWO jobs:
|
|
//
|
|
// 1. ngrok interstitial bypass (navigations) — add the `ngrok-skip-browser-warning`
|
|
// header to top-level navigation requests so ngrok's free-tier browser
|
|
// interstitial ("You are about to visit …") doesn't appear on repeat visits /
|
|
// PWA launches. ngrok skips the warning whenever that header is present. The very
|
|
// first visit on a fresh browser still shows it once (the SW can't exist before
|
|
// that initial load); ngrok's own cookie also suppresses it after one click.
|
|
//
|
|
// 2. Versioned asset cache (subresources) — serve /js, /css, /fonts and the manifest
|
|
// from a Cache Storage entry instead of re-fetching them through the tunnel on
|
|
// every load. The server sends `Cache-Control: no-store` on everything (see
|
|
// src/server/app.ts), so the browser HTTP cache is disabled and every page open
|
|
// otherwise re-downloads ~20 assets + ~5MB of fonts through ngrok. ngrok's free
|
|
// tier allows only 20k HTTP requests/month (ERR_NGROK_727 when exceeded), so this
|
|
// is the difference between staying under the cap and blowing it in days.
|
|
//
|
|
// Safe against stale assets: every asset URL carries a ?v=N that index.html bumps
|
|
// whenever that file changes (the project's existing cache-busting convention). A
|
|
// changed asset is therefore a NEW url → guaranteed cache miss → fresh fetch. We
|
|
// never serve old bytes for a current url. Bump CACHE_VERSION to wipe everything
|
|
// (e.g. to clear the few KB of dead old-?v= entries, or to refresh the unversioned
|
|
// fonts/manifest if they ever change).
|
|
|
|
const CACHE_VERSION = 'cmux-assets-v1';
|
|
|
|
// Same-origin GET requests under these paths are cache-first. Everything else
|
|
// (/api, navigations, POST, WebSocket upgrades) is left completely untouched.
|
|
function isCacheableAsset(pathname) {
|
|
return (
|
|
pathname.startsWith('/js/') ||
|
|
pathname.startsWith('/css/') ||
|
|
pathname.startsWith('/fonts/') ||
|
|
pathname === '/manifest.json'
|
|
);
|
|
}
|
|
|
|
self.addEventListener('install', () => self.skipWaiting());
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil((async () => {
|
|
// Drop every cache that isn't the current version (old caching SWs included).
|
|
const keys = await caches.keys();
|
|
await Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k)));
|
|
await self.clients.claim();
|
|
})());
|
|
});
|
|
|
|
async function cacheFirst(request) {
|
|
const cache = await caches.open(CACHE_VERSION);
|
|
const hit = await cache.match(request);
|
|
if (hit) return hit;
|
|
|
|
const response = await fetch(request);
|
|
// Only store complete, same-origin 200s. The server's `no-store` header is
|
|
// intentionally ignored — the Cache API stores what we put regardless, and the
|
|
// ?v= URL versioning (not HTTP caching) is what guards against staleness here.
|
|
if (response && response.status === 200 && response.type === 'basic') {
|
|
cache.put(request, response.clone());
|
|
}
|
|
return response;
|
|
}
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request;
|
|
|
|
// Top-level navigations: network-first, with the ngrok interstitial-bypass header.
|
|
// Never cached — HTML must stay fresh so new ?v= asset versions are picked up.
|
|
if (req.mode === 'navigate') {
|
|
const headers = new Headers(req.headers);
|
|
headers.set('ngrok-skip-browser-warning', 'true');
|
|
event.respondWith(
|
|
fetch(
|
|
new Request(req.url, {
|
|
method: 'GET',
|
|
headers,
|
|
credentials: req.credentials,
|
|
redirect: 'manual',
|
|
})
|
|
).catch(() => fetch(req))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Versioned static assets: cache-first, keeping them off the tunnel entirely.
|
|
const url = new URL(req.url);
|
|
if (req.method === 'GET' && url.origin === self.location.origin && isCacheableAsset(url.pathname)) {
|
|
event.respondWith(cacheFirst(req));
|
|
return;
|
|
}
|
|
|
|
// Everything else: untouched — straight to network, no rewrite, no caching.
|
|
});
|