fighting-konoha/preload.js

67 lines
4.0 KiB
JavaScript

/* First-visit loader. Before the menu opens, every runtime file in precache.js (built by tools/build_dist.mjs) is
downloaded once behind a progress bar, so later screens never wait for pictures.
- Secure pages (https, localhost): files go into Cache Storage ('aether-assets') with their content hash. A later visit
only downloads files whose hash changed, and sw.js serves the pictures straight from that cache.
- Plain http on a LAN IP (no Cache Storage / service worker there): the download still fills the browser's HTTP cache.
- file:// or no list: nothing to do, the game boots as before.
The game and the menu wait for window.AETHER_PRELOAD before they request their own images (no double download). */
(() => {
'use strict';
const list = self.AETHER_PRECACHE, boot = document.getElementById('boot');
const finish = () => { if (!boot) return; boot.classList.add('done'); setTimeout(() => boot.remove(), 500); };
if (!list || !boot || location.protocol === 'file:' || typeof fetch !== 'function') { window.AETHER_PRELOAD = Promise.resolve(); finish(); return; }
const bar = boot.querySelector('.boot-bar i'), status = boot.querySelector('.boot-status');
const mb = n => (n / 1e6).toFixed(1).replace('.', ',');
let got = 0, downloading = false;
function paint() {
const p = Math.min(1, got / Math.max(1, list.total));
bar.style.transform = `scaleX(${p})`;
status.textContent = downloading ? `Mengunduh aset game · ${Math.floor(p * 100)}% · ${mb(got)} / ${mb(list.total)} MB` : 'Memuat…';
}
const secure = window.isSecureContext && 'caches' in window;
if (secure && 'serviceWorker' in navigator) navigator.serviceWorker.register('sw.js').catch(() => {});
const INDEX = '__aether-index';
async function run() {
const cache = secure ? await caches.open('aether-assets').catch(() => null) : null;
let index = {};
if (cache) { try { const hit = await cache.match(INDEX); if (hit) index = await hit.json(); } catch (_) { index = {}; } }
const todo = [];
for (const [url, bytes, hash] of list.files) {
if (cache && index[url] === hash && await cache.match(url)) got += bytes; else todo.push([url, bytes, hash]);
}
downloading = todo.length > 0; paint();
let next = 0;
async function worker() {
while (next < todo.length) {
const [url, bytes, hash] = todo[next++];
let seen = 0;
try {
const res = await fetch(url, { cache: 'no-cache' });
if (!res.ok) throw new Error(String(res.status));
const chunks = [], reader = res.body && res.body.getReader ? res.body.getReader() : null;
if (reader) for (;;) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); seen += value.byteLength; got += value.byteLength; paint(); }
else { const buf = new Uint8Array(await res.arrayBuffer()); chunks.push(buf); seen = buf.byteLength; got += seen; }
if (cache) {
const type = res.headers.get('Content-Type') || '';
await cache.put(url, new Response(new Blob(chunks, { type }), { headers: { 'Content-Type': type } }));
index[url] = hash;
}
} catch (_) { /* a missing file must not block the game; it simply loads normally later */ }
got += Math.max(0, bytes - seen); paint();
}
}
await Promise.all(Array.from({ length: 6 }, worker));
if (cache) {
// Forget files from older deploys, then store the hash index for the next visit.
const keep = new Set(list.files.map(f => new URL(f[0], location.href).href));
keep.add(new URL(INDEX, location.href).href);
for (const req of await cache.keys()) if (!keep.has(req.url)) await cache.delete(req);
for (const url of Object.keys(index)) if (!list.files.some(f => f[0] === url)) delete index[url];
await cache.put(INDEX, new Response(JSON.stringify(index), { headers: { 'Content-Type': 'application/json' } }));
}
downloading = false; got = list.total; paint();
}
window.AETHER_PRELOAD = run().catch(() => {}).then(finish);
})();