clone

a daily shikaku puzzle for phones: one seeded puzzle per date, solved by dragging rectangles

Improve offline PWA support

Dami committed 0da5b0c
commit
0da5b0c12fecd85e9d51198db15fc8d9e94bddd6
parent
7d27a9813c3ae3bad0c83d8c4de2b11ea0eec7c9
author
Dami <dami.etoile@gmail.com>
10 files changed, +209 -1
Massets/styles.css18+++++-
Aislands/OfflineBadge.tsx23+++++++
Alib/site/network.ts3+
Alib/site/pwa.ts14++++
Mroutes/_app.tsx9+++
Mroutes/index.tsx2+
Astatic/register-sw.js10+++
Astatic/sw.js100++++++++++++++++++++++++++++++
Atests/lib/site/network_test.ts8++
Atests/lib/site/pwa_test.ts23+++++++
M assets/styles.css +17 -1
A islands/OfflineBadge.tsx +23 -0
A lib/site/network.ts +3 -0
A lib/site/pwa.ts +14 -0
M routes/_app.tsx +9 -0
M routes/index.tsx +2 -0
A static/register-sw.js +10 -0
A static/sw.js +100 -0
@@ -0,0 +1,100 @@
1+const CORE_CACHE = "rect-run-core-v2";2+const RUNTIME_CACHE = "rect-run-runtime-v2";3+const CORE_PATHS = [4+ "/",5+ "/d",6+ "/site.webmanifest",7+ "/icon.svg",8+ "/icon-192.png",9+ "/icon-512.png",10+ "/apple-touch-icon.png",11+ "/register-sw.js",12+ "/sw.js",13+];14+15+self.addEventListener("install", (event) => {16+ event.waitUntil(17+ caches.open(CORE_CACHE).then((cache) => cache.addAll(CORE_PATHS)).then(18+ () => {19+ self.skipWaiting();20+ },21+ ),22+ );23+});24+25+self.addEventListener("activate", (event) => {26+ event.waitUntil((async () => {27+ const names = await caches.keys();28+ await Promise.all(29+ names.filter((name) => name !== CORE_CACHE && name !== RUNTIME_CACHE).map(30+ (name) => caches.delete(name),31+ ),32+ );33+ await self.clients.claim();34+ })());35+});36+37+self.addEventListener("fetch", (event) => {38+ const { request } = event;39+ if (request.method !== "GET") return;40+41+ const url = new URL(request.url);42+ if (url.origin !== self.location.origin) return;43+44+ if (request.mode === "navigate") {45+ event.respondWith(handleNavigation(request));46+ return;47+ }48+49+ if (url.pathname.startsWith("/api/puzzles/")) {50+ event.respondWith(networkFirst(request));51+ return;52+ }53+54+ event.respondWith(staleWhileRevalidate(request));55+});56+57+async function handleNavigation(request) {58+ try {59+ const response = await fetch(request);60+ const cache = await caches.open(RUNTIME_CACHE);61+ cache.put(request, response.clone());62+ return response;63+ } catch {64+ const cached = await caches.match(request);65+ if (cached) return cached;66+ return caches.match("/");67+ }68+}69+70+async function networkFirst(request) {71+ const cache = await caches.open(RUNTIME_CACHE);72+ try {73+ const response = await fetch(request);74+ cache.put(request, response.clone());75+ return response;76+ } catch {77+ const cached = await cache.match(request);78+ if (cached) return cached;79+ throw errorResponse();80+ }81+}82+83+async function staleWhileRevalidate(request) {84+ const cache = await caches.open(RUNTIME_CACHE);85+ const cached = await cache.match(request);86+ const networkPromise = fetch(request).then((response) => {87+ cache.put(request, response.clone());88+ return response;89+ }).catch(() => null);90+91+ return cached ?? await networkPromise ?? errorResponse();92+}93+94+function errorResponse() {95+ return new Response("Offline", {96+ status: 503,97+ statusText: "Offline",98+ headers: { "content-type": "text/plain; charset=utf-8" },99+ });100+}
A tests/lib/site/network_test.ts +8 -0
A tests/lib/site/pwa_test.ts +23 -0