mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-01-10 18:21:15 +00:00
Use Web Worker to download game assets
This commit is contained in:
parent
553a714591
commit
90a75d1d2e
40
app.js
40
app.js
@ -346,6 +346,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
document.getElementById('touch-section').style.display = 'none';
|
document.getElementById('touch-section').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let downloaderWorker = null;
|
||||||
|
let missingGameFiles = [];
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
configManager.init(),
|
configManager.init(),
|
||||||
@ -370,6 +373,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
|
||||||
await requestPersistentStorage();
|
await requestPersistentStorage();
|
||||||
|
|
||||||
|
if (downloaderWorker) downloaderWorker.terminate();
|
||||||
|
downloaderWorker = new Worker('/downloader.js');
|
||||||
|
downloaderWorker.onmessage = handleWorkerMessage;
|
||||||
|
|
||||||
const selectedLanguage = languageSelect.value;
|
const selectedLanguage = languageSelect.value;
|
||||||
installBtn.style.display = 'none';
|
installBtn.style.display = 'none';
|
||||||
uninstallBtn.style.display = 'none';
|
uninstallBtn.style.display = 'none';
|
||||||
@ -377,8 +384,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
progressCircular.textContent = '0%';
|
progressCircular.textContent = '0%';
|
||||||
progressCircular.style.background = 'conic-gradient(#FFD700 0deg, #333 0deg)';
|
progressCircular.style.background = 'conic-gradient(#FFD700 0deg, #333 0deg)';
|
||||||
|
|
||||||
navigator.serviceWorker.controller.postMessage({
|
downloaderWorker.postMessage({
|
||||||
action: 'install_language_pack',
|
action: 'install',
|
||||||
|
missingFiles: missingGameFiles,
|
||||||
language: selectedLanguage
|
language: selectedLanguage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -427,29 +435,39 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleServiceWorkerMessage(event) {
|
function handleServiceWorkerMessage(event) {
|
||||||
const { action, language, progress, exists, success } = event.data;
|
const { action, language, isInstalled, success } = event.data;
|
||||||
if (language !== languageSelect.value) return;
|
if (language && language !== languageSelect.value) return;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'cache_status':
|
case 'cache_status':
|
||||||
updateInstallUI(exists);
|
missingGameFiles = event.data.missingFiles;
|
||||||
|
updateInstallUI(isInstalled);
|
||||||
break;
|
break;
|
||||||
|
case 'uninstall_complete':
|
||||||
|
updateInstallUI(!success);
|
||||||
|
checkInitialCacheStatus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWorkerMessage(event) {
|
||||||
|
const { action, progress, success, error } = event.data;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
case 'install_progress':
|
case 'install_progress':
|
||||||
updateInstallUI(false, true);
|
updateInstallUI(false, true);
|
||||||
const angle = (progress / 100) * 360;
|
const angle = (progress / 100) * 360;
|
||||||
progressCircular.textContent = `${Math.round(progress)}%`;
|
progressCircular.textContent = `${Math.round(progress)}%`;
|
||||||
progressCircular.style.background =
|
progressCircular.style.background = `radial-gradient(#181818 60%, transparent 61%), conic-gradient(#FFD700 ${angle}deg, #333 ${angle}deg)`;
|
||||||
`radial-gradient(#181818 60%, transparent 61%), conic-gradient(#FFD700 ${angle}deg, #333 ${angle}deg)`;
|
|
||||||
break;
|
break;
|
||||||
case 'install_complete':
|
case 'install_complete':
|
||||||
updateInstallUI(success);
|
updateInstallUI(success);
|
||||||
break;
|
if (downloaderWorker) downloaderWorker.terminate();
|
||||||
case 'uninstall_complete':
|
|
||||||
updateInstallUI(!success);
|
|
||||||
break;
|
break;
|
||||||
case 'install_failed':
|
case 'install_failed':
|
||||||
alert('Download failed. Please check your internet connection and try again.');
|
alert(`Download failed: ${error}`);
|
||||||
updateInstallUI(false);
|
updateInstallUI(false);
|
||||||
|
if (downloaderWorker) downloaderWorker.terminate();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
downloader.js
Normal file
59
downloader.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
self.onmessage = async (event) => {
|
||||||
|
const { action, missingFiles, language } = event.data;
|
||||||
|
|
||||||
|
if (action === 'install') {
|
||||||
|
const cacheName = `game-assets-${language}`;
|
||||||
|
const THROTTLE_MS = 100;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileMetadataPromises = missingFiles.map(fileUrl =>
|
||||||
|
fetch(fileUrl, { method: 'HEAD', headers: { 'Accept-Language': language } })
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error(`Failed to HEAD ${fileUrl}`);
|
||||||
|
return { url: fileUrl, size: Number(response.headers.get('content-length')) || 0 };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const fileMetadata = await Promise.all(fileMetadataPromises);
|
||||||
|
const totalBytesToDownload = fileMetadata.reduce((sum, file) => sum + file.size, 0);
|
||||||
|
let bytesDownloaded = 0;
|
||||||
|
let lastProgressUpdate = 0;
|
||||||
|
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
|
||||||
|
for (const file of fileMetadata) {
|
||||||
|
const request = new Request(file.url, { headers: { 'Accept-Language': language } });
|
||||||
|
const response = await fetch(request);
|
||||||
|
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new Error(`Failed to fetch ${file.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [streamForCaching, streamForProgress] = response.body.tee();
|
||||||
|
const cachePromise = cache.put(request, new Response(streamForCaching, response));
|
||||||
|
|
||||||
|
const reader = streamForProgress.getReader();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
bytesDownloaded += value.length;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now - lastProgressUpdate > THROTTLE_MS) {
|
||||||
|
lastProgressUpdate = now;
|
||||||
|
self.postMessage({
|
||||||
|
action: 'install_progress',
|
||||||
|
progress: (bytesDownloaded / totalBytesToDownload) * 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await cachePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage({ action: 'install_complete', success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Download worker error:", error);
|
||||||
|
self.postMessage({ action: 'install_failed', error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
152
sw.js
152
sw.js
@ -16,7 +16,7 @@ const coreAppFiles = [
|
|||||||
'/island.webp', '/isle.js', '/isle.wasm', '/poster.pdf', '/read_me_off.webp',
|
'/island.webp', '/isle.js', '/isle.wasm', '/poster.pdf', '/read_me_off.webp',
|
||||||
'/read_me_on.webp', '/run_game_off.webp', '/run_game_on.webp', '/shark.webp',
|
'/read_me_on.webp', '/run_game_off.webp', '/run_game_on.webp', '/shark.webp',
|
||||||
'/uninstall_off.webp', '/uninstall_on.webp', '/app.js', '/style.css', '/manifest.json',
|
'/uninstall_off.webp', '/uninstall_on.webp', '/app.js', '/style.css', '/manifest.json',
|
||||||
'/install.webp', '/install.mp3'
|
'/install.webp', '/install.mp3', '/downloader.js'
|
||||||
];
|
];
|
||||||
|
|
||||||
const gameFiles = [
|
const gameFiles = [
|
||||||
@ -33,22 +33,6 @@ const gameFiles = [
|
|||||||
|
|
||||||
const STATIC_CACHE_NAME = 'static-assets-v1';
|
const STATIC_CACHE_NAME = 'static-assets-v1';
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
event.waitUntil(
|
|
||||||
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
|
||||||
return cache.addAll(coreAppFiles);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
self.skipWaiting();
|
|
||||||
});
|
|
||||||
|
|
||||||
registerRoute(
|
|
||||||
({ url }) => coreAppFiles.includes(url.pathname),
|
|
||||||
new StaleWhileRevalidate({
|
|
||||||
cacheName: STATIC_CACHE_NAME,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const rangeRequestsPlugin = new RangeRequestsPlugin();
|
const rangeRequestsPlugin = new RangeRequestsPlugin();
|
||||||
const normalizePathPlugin = {
|
const normalizePathPlugin = {
|
||||||
cacheKeyWillBeUsed: async ({ request }) => {
|
cacheKeyWillBeUsed: async ({ request }) => {
|
||||||
@ -82,95 +66,8 @@ class LegoCacheStrategy extends Strategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRoute(
|
|
||||||
({ url }) => url.pathname.startsWith('/LEGO/'),
|
|
||||||
new LegoCacheStrategy()
|
|
||||||
);
|
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.action) {
|
|
||||||
switch (event.data.action) {
|
|
||||||
case 'install_language_pack':
|
|
||||||
installLanguagePack(event.data.language, event.source);
|
|
||||||
break;
|
|
||||||
case 'uninstall_language_pack':
|
|
||||||
uninstallLanguagePack(event.data.language, event.source);
|
|
||||||
break;
|
|
||||||
case 'check_cache_status':
|
|
||||||
checkCacheStatus(event.data.language, event.source);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getLanguageCacheName = (language) => `game-assets-${language}`;
|
const getLanguageCacheName = (language) => `game-assets-${language}`;
|
||||||
|
|
||||||
async function installLanguagePack(language, client) {
|
|
||||||
const THROTTLE_MS = 100;
|
|
||||||
const cacheName = getLanguageCacheName(language);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileMetadataPromises = gameFiles.map(fileUrl =>
|
|
||||||
fetch(fileUrl, { method: 'HEAD', headers: { 'Accept-Language': language } })
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) throw new Error(`Failed to HEAD ${fileUrl}`);
|
|
||||||
return { url: fileUrl, size: Number(response.headers.get('content-length')) || 0 };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const fileMetadata = await Promise.all(fileMetadataPromises);
|
|
||||||
const totalBytesToDownload = fileMetadata.reduce((sum, file) => sum + file.size, 0);
|
|
||||||
let bytesDownloaded = 0;
|
|
||||||
let lastProgressUpdate = 0;
|
|
||||||
|
|
||||||
const cache = await caches.open(cacheName);
|
|
||||||
|
|
||||||
for (const file of fileMetadata) {
|
|
||||||
const request = new Request(file.url, { headers: { 'Accept-Language': language } });
|
|
||||||
const response = await fetch(request);
|
|
||||||
|
|
||||||
if (!response.ok || !response.body) {
|
|
||||||
throw new Error(`Failed to fetch ${file.url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [streamForCaching, streamForProgress] = response.body.tee();
|
|
||||||
|
|
||||||
const responseToCache = new Response(streamForCaching, response);
|
|
||||||
const cachePromise = cache.put(request, responseToCache);
|
|
||||||
|
|
||||||
const reader = streamForProgress.getReader();
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
bytesDownloaded += value.length;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (now - lastProgressUpdate > THROTTLE_MS) {
|
|
||||||
lastProgressUpdate = now;
|
|
||||||
client.postMessage({
|
|
||||||
action: 'install_progress',
|
|
||||||
progress: (bytesDownloaded / totalBytesToDownload) * 100,
|
|
||||||
language: language
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await cachePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.postMessage({
|
|
||||||
action: 'install_progress',
|
|
||||||
progress: 100,
|
|
||||||
language: language
|
|
||||||
});
|
|
||||||
client.postMessage({ action: 'install_complete', success: true, language: language });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Aborting installation due to an error:", error);
|
|
||||||
await caches.delete(cacheName);
|
|
||||||
client.postMessage({ action: 'install_failed', language: language });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uninstallLanguagePack(language, client) {
|
async function uninstallLanguagePack(language, client) {
|
||||||
const cacheName = getLanguageCacheName(language);
|
const cacheName = getLanguageCacheName(language);
|
||||||
try {
|
try {
|
||||||
@ -187,10 +84,40 @@ async function uninstallLanguagePack(language, client) {
|
|||||||
|
|
||||||
async function checkCacheStatus(language, client) {
|
async function checkCacheStatus(language, client) {
|
||||||
const cacheName = getLanguageCacheName(language);
|
const cacheName = getLanguageCacheName(language);
|
||||||
const hasCache = await caches.has(cacheName);
|
const cache = await caches.open(cacheName);
|
||||||
client.postMessage({ action: 'cache_status', exists: hasCache, language: language });
|
const requests = await cache.keys();
|
||||||
|
const cachedUrls = requests.map(req => new URL(req.url).pathname);
|
||||||
|
const missingFiles = gameFiles.filter(file => !cachedUrls.includes(file));
|
||||||
|
|
||||||
|
client.postMessage({
|
||||||
|
action: 'cache_status',
|
||||||
|
isInstalled: missingFiles.length === 0,
|
||||||
|
missingFiles: missingFiles,
|
||||||
|
language: language
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => coreAppFiles.includes(url.pathname),
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: STATIC_CACHE_NAME,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => url.pathname.startsWith('/LEGO/'),
|
||||||
|
new LegoCacheStrategy()
|
||||||
|
);
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
||||||
|
return cache.addAll(coreAppFiles);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.keys().then((cacheNames) => {
|
caches.keys().then((cacheNames) => {
|
||||||
@ -205,3 +132,16 @@ self.addEventListener('activate', (event) => {
|
|||||||
);
|
);
|
||||||
event.waitUntil(self.clients.claim());
|
event.waitUntil(self.clients.claim());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.action) {
|
||||||
|
switch (event.data.action) {
|
||||||
|
case 'uninstall_language_pack':
|
||||||
|
uninstallLanguagePack(event.data.language, event.source);
|
||||||
|
break;
|
||||||
|
case 'check_cache_status':
|
||||||
|
checkCacheStatus(event.data.language, event.source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user