// 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))); })()); } });