Files
WebAisMap/static/sw.js
T
2026-05-04 08:06:34 +03:00

152 lines
5.3 KiB
JavaScript

// AIS Map Service Worker — кеширование тайлов и ассетов.
// Цели: моментальная отрисовка карты после первой загрузки, офлайн-доступ к ранее
// просмотренным тайлам, минимум сетевых запросов в slow-networks и при pan/zoom.
const CACHE_VERSION = 'v2';
const TILE_CACHE = 'aismap-tiles-' + CACHE_VERSION;
const ASSET_CACHE = 'aismap-assets-' + CACHE_VERSION;
const APP_CACHE = 'aismap-app-' + CACHE_VERSION;
// Ограничиваем, сколько тайлов держим на устройстве, чтобы не забить storage на телефоне.
const TILE_CACHE_MAX = 4000;
self.addEventListener('install', (event) => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((k) => ![TILE_CACHE, ASSET_CACHE, APP_CACHE].includes(k))
.map((k) => caches.delete(k))
);
await self.clients.claim();
})());
});
function isTileRequest(url) {
return url.pathname.startsWith('/tiles/') || url.pathname.startsWith('/vtiles/');
}
function isLongLivedAsset(url) {
if (url.pathname.startsWith('/static/leaflet/')) return true;
if (url.pathname.startsWith('/static/xterm/')) return true;
if (url.pathname.startsWith('/svg/')) return true;
return false;
}
function isAppShell(url) {
if (url.pathname === '/' || url.pathname === '/cert') return true;
if (url.pathname.startsWith('/static/js/')) return true;
if (url.pathname.startsWith('/static/css/')) return true;
return false;
}
function isApiRequest(url) {
return url.pathname.startsWith('/api/') || url.pathname === '/ws' || url.pathname.startsWith('/ws/');
}
async function trimCache(cacheName, max) {
try {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length <= max) return;
const excess = keys.length - max;
for (let i = 0; i < excess; i += 1) {
await cache.delete(keys[i]);
}
} catch (e) {}
}
// Cache-first с фоновым обновлением: отдаём из кеша если есть, в фоне освежаем.
async function cacheFirstWithRefresh(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const fetchAndUpdate = fetch(request)
.then((resp) => {
if (resp && resp.ok) {
cache.put(request, resp.clone()).catch(() => {});
if (cacheName === TILE_CACHE) {
trimCache(TILE_CACHE, TILE_CACHE_MAX);
}
}
return resp;
})
.catch(() => null);
if (cached) {
// Обновление в фоне, клиенту отдаём кеш немедленно.
fetchAndUpdate.catch(() => {});
return cached;
}
const fresh = await fetchAndUpdate;
if (fresh) return fresh;
return new Response('', { status: 504, statusText: 'Gateway Timeout (offline)' });
}
// Stale-while-revalidate: мгновенно из кеша, параллельно обновляем кеш.
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const networkFetch = fetch(request)
.then((resp) => {
if (resp && resp.ok) {
cache.put(request, resp.clone()).catch(() => {});
}
return resp;
})
.catch(() => null);
if (cached) {
networkFetch.catch(() => {});
return cached;
}
const fresh = await networkFetch;
if (fresh) return fresh;
return new Response('', { status: 504, statusText: 'Gateway Timeout (offline)' });
}
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return;
let url;
try {
url = new URL(req.url);
} catch (e) {
return;
}
// Не трогаем кросс-оригинальные запросы (OSM, CARTO и т.п.) — пусть идут напрямую.
if (url.origin !== self.location.origin) return;
// API и WebSocket — без кеша. WebSocket вообще не проходит через fetch, но на всякий случай.
if (isApiRequest(url)) return;
if (isTileRequest(url)) {
event.respondWith(cacheFirstWithRefresh(req, TILE_CACHE));
return;
}
if (isLongLivedAsset(url)) {
event.respondWith(cacheFirstWithRefresh(req, ASSET_CACHE));
return;
}
if (isAppShell(url)) {
event.respondWith(staleWhileRevalidate(req, APP_CACHE));
return;
}
});
// Принудительный сброс кешей по команде со страницы.
self.addEventListener('message', (event) => {
if (!event.data || typeof event.data !== 'object') return;
if (event.data.type === 'aismap:clear-caches') {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.map((k) => caches.delete(k)));
})());
}
});