193 lines
6.5 KiB
JavaScript
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.
|
|
});
|