5df38bad2d
Closes TG-4
152 lines
5.3 KiB
JavaScript
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)));
|
|
})());
|
|
}
|
|
});
|