Initial import: WebAisMap
Closes TG-4 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
// 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)));
|
||||
})());
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user