aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/public/pwa-worker.js
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-08-06 21:52:21 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-08-06 21:52:21 +0200
commitebfb7c1c8bc0a9ec50bde72eb9a0859c6e5dcee5 (patch)
tree35353c15726d7d036907df731b00d390c1d1f538 /src/frontend/public/pwa-worker.js
parent5cc27f852b02446659e0ab85305916c9f5e5a5f0 (diff)
Fix this fucking pile of steaming garbage
Diffstat (limited to 'src/frontend/public/pwa-worker.js')
-rw-r--r--src/frontend/public/pwa-worker.js145
1 files changed, 145 insertions, 0 deletions
diff --git a/src/frontend/public/pwa-worker.js b/src/frontend/public/pwa-worker.js
new file mode 100644
index 0000000..f47ede0
--- /dev/null
+++ b/src/frontend/public/pwa-worker.js
@@ -0,0 +1,145 @@
+const CACHE_VERSION = "2025-0806a";
+const API_CACHE_NAME = `api-cache-${CACHE_VERSION}`;
+const STATIC_CACHE_NAME = `static-cache-${CACHE_VERSION}`;
+
+const API_URL_PATTERN = /\/api\/(GetStopList|GetStopEstimates|GetStopTimetable)/;
+const API_MAX_AGE = 24 * 60 * 60 * 1000; // 24h
+const ESTIMATES_MIN_AGE = 15 * 1000;
+const ESTIMATES_MAX_AGE = 30 * 1000;
+
+self.addEventListener("install", (event) => {
+ console.log("SW: Installing new version");
+ event.waitUntil(
+ caches.open(STATIC_CACHE_NAME).then((cache) =>
+ cache.addAll([
+ "/favicon.ico",
+ "/logo-256.png",
+ "/logo-512.jpg",
+ "/stops.json"
+ ])
+ ).then(() => self.skipWaiting())
+ );
+});
+
+self.addEventListener("activate", (event) => {
+ console.log("SW: Activating new version");
+ event.waitUntil(
+ (async () => {
+ const cacheNames = await caches.keys();
+ await Promise.all(
+ cacheNames.map((name) => {
+ if (name !== API_CACHE_NAME && name !== STATIC_CACHE_NAME) {
+ console.log("SW: Deleting old cache:", name);
+ return caches.delete(name);
+ }
+ })
+ );
+ await self.clients.claim();
+ const clients = await self.clients.matchAll();
+ clients.forEach((client) =>
+ client.postMessage({ type: "SW_UPDATED" })
+ );
+ })()
+ );
+});
+
+self.addEventListener("fetch", (event) => {
+ const request = event.request;
+ const url = new URL(request.url);
+
+ // Ignore requests with unsupported schemes
+ if (!url.protocol.startsWith('http')) {
+ return;
+ }
+
+ // API
+ if (request.method === "GET" && API_URL_PATTERN.test(url.pathname)) {
+ event.respondWith(handleApiRequest(request));
+ return;
+ }
+
+ // Navegación -> network first
+ if (request.mode === "navigate") {
+ event.respondWith(handleNavigationRequest(request));
+ return;
+ }
+
+ // Estáticos -> cache first
+ if (request.method === "GET") {
+ event.respondWith(handleStaticRequest(request));
+ }
+});
+
+async function handleApiRequest(request) {
+ const url = new URL(request.url);
+ const isEstimates = url.pathname.includes("GetStopEstimates");
+ const maxAge = isEstimates
+ ? ESTIMATES_MIN_AGE + Math.random() * (ESTIMATES_MAX_AGE - ESTIMATES_MIN_AGE)
+ : API_MAX_AGE;
+
+ const cache = await caches.open(API_CACHE_NAME);
+ const cachedResponse = await cache.match(request);
+
+ if (cachedResponse) {
+ const dateHeader = cachedResponse.headers.get("date");
+ const age = dateHeader ? Date.now() - new Date(dateHeader).getTime() : 0;
+ if (age && age < maxAge) {
+ console.debug("SW: Cache HIT", request.url);
+ return cachedResponse;
+ }
+ cache.delete(request);
+ }
+
+ try {
+ const netResponse = await fetch(request);
+ if (netResponse.ok) cache.put(request, netResponse.clone());
+ return netResponse;
+ } catch (error) {
+ if (cachedResponse) return cachedResponse;
+ throw error;
+ }
+}
+
+async function handleStaticRequest(request) {
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ const cachedResponse = await cache.match(request);
+ if (cachedResponse) return cachedResponse;
+
+ try {
+ const netResponse = await fetch(request);
+ if (netResponse.ok) cache.put(request, netResponse.clone());
+ return netResponse;
+ } catch (err) {
+ return new Response("Offline asset not available", {
+ status: 503,
+ headers: { "Content-Type": "text/plain" }
+ });
+ }
+}
+
+async function handleNavigationRequest(request) {
+ try {
+ const netResponse = await fetch(request);
+ return netResponse;
+ } catch (err) {
+ // Si no hay red, intenta fallback con caché estática
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ const offline = await cache.match("/stops.json");
+ return (
+ offline ||
+ new Response("App offline", {
+ status: 503,
+ headers: { "Content-Type": "text/plain" }
+ })
+ );
+ }
+}
+
+self.addEventListener("message", (event) => {
+ if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
+ if (event.data?.type === "CLEAR_CACHE") {
+ event.waitUntil(
+ caches.keys().then((names) => Promise.all(names.map((n) => caches.delete(n))))
+ );
+ }
+});