107 lines
4.1 KiB
JavaScript
107 lines
4.1 KiB
JavaScript
// Service Worker для GrechkaCraft.
|
||
//
|
||
// Раньше на любую сетевую ошибку отдавался `new Response('Offline', {status: 503})`.
|
||
// Из-за этого обычный обрыв сети (моргнувший Wi-Fi, потерянный пакет) выглядел
|
||
// как «сервер упал»: страница грузилась, но весь JS и мир приходили как 503,
|
||
// и пользователь видел либо пустой экран, либо сообщение об ошибке.
|
||
//
|
||
// Теперь:
|
||
// - статика (HTML/JS/CSS/ассеты) кэшируется и отдаётся из кэша при сбое;
|
||
// - навигация при сбое отдаёт закэшированную копию страницы;
|
||
// - если кэша нет — отдаём 200 с минимальной страницей-заглушкой, а НЕ 503,
|
||
// потому что 503 для пользователя неотличим от «сервис умер».
|
||
|
||
const CACHE = 'grechka-v3';
|
||
|
||
const PRECACHE_URLS = [
|
||
'./',
|
||
'./index.html',
|
||
'./index-v2.html',
|
||
'./game.js',
|
||
'./game3d.js',
|
||
'./style.css',
|
||
'./manifest.json',
|
||
];
|
||
|
||
self.addEventListener('install', (e) => {
|
||
self.skipWaiting();
|
||
e.waitUntil(
|
||
caches.open(CACHE)
|
||
// addAll падает целиком, если хоть один URL недоступен — кэшируем по одному.
|
||
.then((c) => Promise.all(
|
||
PRECACHE_URLS.map((u) => c.add(u).catch(() => null))
|
||
))
|
||
.then(() => self.clients.claim())
|
||
);
|
||
});
|
||
|
||
self.addEventListener('activate', (e) => {
|
||
e.waitUntil(
|
||
caches.keys()
|
||
.then((keys) => Promise.all(
|
||
keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))
|
||
))
|
||
.then(() => self.clients.claim())
|
||
);
|
||
});
|
||
|
||
const OFFLINE_PAGE = `<!doctype html><html lang="ru"><head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Гречка — нет связи</title>
|
||
<style>
|
||
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
|
||
background:#0f1720;color:#e8f0f8;font:16px/1.5 system-ui,sans-serif;text-align:center;padding:24px}
|
||
.b{max-width:340px}
|
||
h1{font-size:20px;margin:0 0 10px}
|
||
p{color:#9fb0c4;margin:0 0 18px}
|
||
button{font:inherit;padding:11px 22px;border:0;border-radius:9px;background:#5a8f3c;
|
||
color:#fff;cursor:pointer}
|
||
</style></head><body><div class="b">
|
||
<h1>Нет связи с сервером</h1>
|
||
<p>Игра не смогла загрузиться. Проверь интернет и нажми «Попробовать снова».</p>
|
||
<button onclick="location.reload()">Попробовать снова</button>
|
||
</div></body></html>`;
|
||
|
||
self.addEventListener('fetch', (e) => {
|
||
const req = e.request;
|
||
if (req.method !== 'GET') return;
|
||
|
||
const url = new URL(req.url);
|
||
|
||
// Кросс-доменные запросы (socket.io CDN, apigrech, telegram) не трогаем.
|
||
if (url.origin !== self.location.origin) return;
|
||
|
||
// Навигация: сначала сеть, при сбое — кэш страницы, иначе заглушка.
|
||
if (req.mode === 'navigate') {
|
||
e.respondWith(
|
||
fetch(req)
|
||
.then((res) => {
|
||
const copy = res.clone();
|
||
caches.open(CACHE).then((c) => c.put(req, copy));
|
||
return res;
|
||
})
|
||
.catch(() => caches.match(req, { ignoreSearch: true })
|
||
.then((r) => r || caches.match('./index.html'))
|
||
.then((r) => r || new Response(OFFLINE_PAGE, {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||
})))
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Статика: сначала сеть, кэшируем свежее; при сбое — из кэша.
|
||
e.respondWith(
|
||
fetch(req)
|
||
.then((res) => {
|
||
if (res && res.ok && res.type === 'basic') {
|
||
const copy = res.clone();
|
||
caches.open(CACHE).then((c) => c.put(req, copy));
|
||
}
|
||
return res;
|
||
})
|
||
.catch(() => caches.match(req, { ignoreSearch: true })
|
||
.then((r) => r || new Response('', { status: 200 })))
|
||
);
|
||
});
|