Files
SKEEN-Proyecto/frontend/public/sw.js

193 lines
6.5 KiB
JavaScript

/* =====================================================================
Service Worker del Panel Clínico (SKEEN)
---------------------------------------------------------------------
Sin dependencias ni Workbox. Estrategia:
- App shell (HTML, manifest, iconos, JS/CSS con hash): precache en
install, versionado por build.
- Navegación (HTML): network-first. Un deploy nuevo se ve al primer
refresh (nginx sirve index.html con Cache-Control: no-cache) y, si
no hay red, se devuelve la última copia del shell o /offline.html.
- Assets con hash (/assets/*): cache-first (son inmutables).
- Otras GET del mismo origen (logos de marca, svg): stale-while-revalidate.
- API (/api/*), peticiones con Authorization y cualquier método distinto
de GET: NUNCA se cachean. Se dejan pasar tal cual al navegador.
La versión de caché y la lista de assets se inyectan en el build
(vite.config.ts); si no se inyectan, el SW sigue siendo funcional.
===================================================================== */
var VERSION = '__BUILD_VERSION__';
// Lista de assets con hash generados por el build (JSON inyectado).
var PRECACHE_ASSETS = [];
try { PRECACHE_ASSETS = JSON.parse('__PRECACHE_ASSETS__'); } catch (e) { PRECACHE_ASSETS = []; }
var PRECACHE = 'skeen-precache-' + VERSION;
var RUNTIME = 'skeen-runtime-' + VERSION;
var CACHE_PREFIX = 'skeen-';
var PRECACHE_URLS = [
'/',
'/index.html',
'/offline.html',
'/manifest.webmanifest',
'/favicon.svg',
'/icons/icon.svg',
'/icons/icon-32.png',
'/icons/icon-192.png',
'/icons/icon-512.png',
'/icons/icon-192-maskable.png',
'/icons/icon-512-maskable.png',
'/icons/apple-touch-icon.png',
// Brand neutro por defecto: permite pintar el login sin red.
'/brand/favicon.svg',
'/brand/logo-blanco.svg',
'/brand/logo-negro.svg',
].concat(PRECACHE_ASSETS);
// --------------------------------------------------------------------
// install / activate
// --------------------------------------------------------------------
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(PRECACHE).then(function (cache) {
// add() individual: un recurso que falle no aborta toda la instalación.
return Promise.all(
PRECACHE_URLS.map(function (url) {
return cache.add(new Request(url, { cache: 'reload' })).catch(function () { /* ignore */ });
})
);
}).then(function () { return self.skipWaiting(); })
);
});
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (keys) {
return Promise.all(
keys.map(function (key) {
// Borra cachés de builds anteriores (mismo prefijo, otra versión).
if (key.indexOf(CACHE_PREFIX) === 0 && key !== PRECACHE && key !== RUNTIME) {
return caches.delete(key);
}
return undefined;
})
);
}).then(function () { return self.clients.claim(); })
);
});
// Permite forzar la activación desde la página si se desea.
self.addEventListener('message', function (event) {
if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
});
// --------------------------------------------------------------------
// Estrategias
// --------------------------------------------------------------------
function isSameOrigin(url) {
return url.origin === self.location.origin;
}
function isApiRequest(url) {
return url.pathname.indexOf('/api/') === 0;
}
function isHashedAsset(url) {
return isSameOrigin(url) && url.pathname.indexOf('/assets/') === 0;
}
async function networkFirstNavigation(request) {
try {
var fresh = await fetch(request);
var type = fresh.headers.get('content-type') || '';
if (fresh && fresh.ok && type.indexOf('text/html') !== -1) {
var cache = await caches.open(PRECACHE);
// Se guarda una única copia del shell bajo /index.html.
cache.put('/index.html', fresh.clone()).catch(function () { /* ignore */ });
}
return fresh;
} catch (e) {
var shell = await caches.open(PRECACHE);
var cached = (await shell.match('/index.html')) || (await shell.match('/'));
if (cached) return cached;
var offline = await shell.match('/offline.html');
if (offline) return offline;
return new Response('Sin conexión', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
}
async function cacheFirst(request, cacheName) {
var cache = await caches.open(cacheName);
var hit = await cache.match(request);
if (hit) return hit;
var res = await fetch(request);
if (res && res.ok && res.type === 'basic') {
cache.put(request, res.clone()).catch(function () { /* ignore */ });
}
return res;
}
async function staleWhileRevalidate(request, cacheName) {
var cache = await caches.open(cacheName);
var hit = await cache.match(request);
var network = fetch(request).then(function (res) {
if (res && res.ok && res.type === 'basic') {
cache.put(request, res.clone()).catch(function () { /* ignore */ });
}
return res;
}).catch(function () { return undefined; });
if (hit) return hit;
var res = await network;
if (res) return res;
return new Response('', { status: 504, statusText: 'Sin conexión' });
}
// --------------------------------------------------------------------
// fetch
// --------------------------------------------------------------------
self.addEventListener('fetch', function (event) {
var request = event.request;
var url;
try {
url = new URL(request.url);
} catch (e) {
return;
}
// 1) Nunca interceptar lo que no debe cachearse:
// - métodos distintos de GET
// - peticiones con Authorization (JWT / datos clínicos)
// - API del backend (/api/odoo/... y /api/v1/...)
// - backend Odoo directo (/web/)
if (request.method !== 'GET') return;
if (request.headers.get('authorization')) return;
if (isApiRequest(url)) return;
if (url.pathname.indexOf('/web/') === 0) return;
// 2) Navegación HTML: network-first con fallback al shell cacheado.
if (request.mode === 'navigate') {
event.respondWith(networkFirstNavigation(request));
return;
}
// 3) Assets con hash: cache-first (inmutables).
if (isHashedAsset(url)) {
event.respondWith(cacheFirst(request, RUNTIME));
return;
}
// 4) Otras GET del mismo origen (logos, svg): SWR.
if (isSameOrigin(url)) {
event.respondWith(staleWhileRevalidate(request, RUNTIME));
return;
}
// 5) Cross-origin (p. ej. Google Fonts): sin interceptar.
});