diff --git a/packages/app/index.html b/packages/app/index.html index 8fad7efb3a45..dfcb1e32bbad 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -13,6 +13,7 @@ + diff --git a/packages/app/public/sw.js b/packages/app/public/sw.js new file mode 100644 index 000000000000..f1ca81a4f466 --- /dev/null +++ b/packages/app/public/sw.js @@ -0,0 +1,90 @@ +/// +const CACHE_NAME = "opencode-v1" + +/** @param {string} url */ +function isHashedAsset(url) { + return /\/assets\/[^/]+[-.][\da-f]{8,}\.\w+$/.test(url) +} + +/** @param {string} url */ +function isStaticAsset(url) { + return /\.(?:js|css|woff2?|png|svg|ico|webmanifest)(?:\?|$)/.test(url) +} + +/** @param {string} url */ +function isAPIorEvent(url) { + const path = new URL(url).pathname + return path.startsWith("/api/") || path.endsWith("/event") || path.endsWith("/prompt_async") +} + +self.addEventListener("install", () => { + self.skipWaiting() +}) + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), + ), + ) + self.clients.claim() +}) + +self.addEventListener("fetch", (event) => { + const { request } = event + if (request.method !== "GET") return + if (isAPIorEvent(request.url)) return + + // Navigation requests: stale-while-revalidate for instant page loads + if (request.mode === "navigate") { + event.respondWith( + caches.open(CACHE_NAME).then((cache) => + cache.match("/index.html").then((cached) => { + const fresh = fetch(request) + .then((response) => { + if (response.ok) cache.put("/index.html", response.clone()) + return response + }) + .catch(() => cached) + return cached || fresh + }), + ), + ) + return + } + + // Hashed assets: cache-first (immutable filenames) + if (isHashedAsset(request.url)) { + event.respondWith( + caches.open(CACHE_NAME).then((cache) => + cache.match(request).then( + (cached) => + cached || + fetch(request).then((response) => { + if (response.ok) cache.put(request, response.clone()) + return response + }), + ), + ), + ) + return + } + + // Other static assets: stale-while-revalidate + if (isStaticAsset(request.url)) { + event.respondWith( + caches.open(CACHE_NAME).then((cache) => + cache.match(request).then((cached) => { + const fresh = fetch(request) + .then((response) => { + if (response.ok) cache.put(request, response.clone()) + return response + }) + .catch(() => cached) + return cached || fresh + }), + ), + ) + return + } +}) diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 5115f0348ad4..55901a68d457 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -153,6 +153,10 @@ if (import.meta.env.VITE_SENTRY_DSN) { }) } +if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/sw.js").catch(() => {}) +} + if (root instanceof HTMLElement) { const auth = authFromToken(new URLSearchParams(location.search).get("auth_token")) clearAuthToken() diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 8db576dd8342..b30d41279d20 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -5,6 +5,7 @@ src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2"); font-weight: normal; font-style: normal; + font-display: swap; } @layer components {