From 946d8d695c038e565fb2e8466e6935d1fe147cb4 Mon Sep 17 00:00:00 2001 From: fynks <75840152+fynks@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:52:36 +0500 Subject: [PATCH] feat: implement fetch with timeout and fallback for improved data loading reliability --- dist/js/app.js | 45 +++++++++++++++++++++++++++++++-------- scripts/replace-assets.js | 34 ++++++++++++++++++++++++++--- workbox-config.js | 20 ++++++----------- 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/dist/js/app.js b/dist/js/app.js index bc1250f..d4f411b 100644 --- a/dist/js/app.js +++ b/dist/js/app.js @@ -1083,12 +1083,38 @@ document.addEventListener('DOMContentLoaded', async () => { loadingManager.show('#pricing-table-container', 'Loading pricing data...') ]; - const dataUrls = ['./json/file-hosts-optimized.json', './json/adult-hosts-optimized.json', './json/pricing.json']; - const fetchPromises = dataUrls.map(url => - fetch(url).then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status} for ${url}`))) - ); + // Helper: fetch with timeout and graceful fallbacks + const fetchWithTimeout = (url, { timeout = 8000 } = {}) => { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + return fetch(url, { signal: controller.signal }) + .finally(() => clearTimeout(id)); + }; - const results = await Promise.allSettled(fetchPromises); + const fetchJsonFallback = async (urls, timeout = 8000) => { + let lastErr; + for (const url of urls) { + try { + const res = await fetchWithTimeout(url, { timeout }); + if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`); + return await res.json(); + } catch (err) { + lastErr = err; + continue; + } + } + throw lastErr || new Error('All fetch fallbacks failed'); + }; + + // Prefer optimized JSON, fall back to original if missing + const datasets = [ + ['./json/file-hosts-optimized.json', './json/file-hosts.json'], + ['./json/adult-hosts-optimized.json', './json/adult-hosts.json'], + ['./json/pricing.json'] + ]; + + const fetchPromises = datasets.map(list => fetchJsonFallback(list, 8000)); + const results = await Promise.allSettled(fetchPromises); results.forEach((result, index) => { const containers = ['#file-hosts-table-container', '#adult-hosts-table-container', '#pricing-table-container']; @@ -1111,9 +1137,9 @@ document.addEventListener('DOMContentLoaded', async () => { } } } else { - console.error(`Error loading data (${dataUrls[index]}):`, result.reason); + console.error('Error loading data set:', result.reason); const el = document.querySelector(containers[index]); - if (el) el.innerHTML = `

Failed to load data: ${dataUrls[index]}

`; + if (el) el.innerHTML = `

Failed to load data. Please check your connection and refresh.

`; } }); @@ -1147,8 +1173,9 @@ window.addEventListener('unhandledrejection', e => { }); if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js').catch(err => { + window.addEventListener('load', () => { + // Use relative path to work under subdirectories too + navigator.serviceWorker.register('sw.js').catch(err => { console.warn('Service worker registration failed:', err); }); }); diff --git a/scripts/replace-assets.js b/scripts/replace-assets.js index dec8495..0185bd9 100644 --- a/scripts/replace-assets.js +++ b/scripts/replace-assets.js @@ -29,6 +29,26 @@ function replaceAssets() { let cssReplaced = 0; let jsReplaced = 0; + // Compute simple query hash for cache-busting if files exist + const cssMinPath = path.resolve(__dirname, '..', 'dist', 'css', 'styles-min.css'); + const jsMinPath = path.resolve(__dirname, '..', 'dist', 'js', 'app-min.js'); + const safeHash = (p) => { + try { + const buf = fs.readFileSync(p); + let hash = 0; + for (let i = 0; i < buf.length; i += Math.ceil(buf.length / 128) || 1) { + hash = (hash * 31 + buf[i]) >>> 0; + } + return hash.toString(36); + } catch { + return null; + } + }; + const cssHash = safeHash(cssMinPath); + const jsHash = safeHash(jsMinPath); + const cssTarget = cssHash ? `href="./css/styles-min.css?v=${cssHash}"` : 'href="./css/styles-min.css"'; + const jsTarget = jsHash ? `src="js/app-min.js?v=${jsHash}"` : 'src="js/app-min.js"'; + // Replace CSS links in head - more flexible patterns const cssPatterns = [ /href=["']\.\/css\/styles\.css["']/g, @@ -39,11 +59,19 @@ function replaceAssets() { cssPatterns.forEach(pattern => { const matches = updatedHeadContent.match(pattern); if (matches) { - updatedHeadContent = updatedHeadContent.replace(pattern, 'href="./css/styles-min.css"'); + updatedHeadContent = updatedHeadContent.replace(pattern, cssTarget); cssReplaced += matches.length; } }); + // Ensure existing preload to styles-min.css carries the same version hash + if (cssHash) { + const preloadMinCssPattern = /href=["']\/?css\/styles-min\.css["']/g; + if (preloadMinCssPattern.test(updatedHeadContent)) { + updatedHeadContent = updatedHeadContent.replace(preloadMinCssPattern, `href="/css/styles-min.css?v=${cssHash}"`); + } + } + // Replace JS script src in head - more flexible patterns const jsPatterns = [ /src=["']\.\/js\/app\.js["']/g, @@ -54,7 +82,7 @@ function replaceAssets() { jsPatterns.forEach(pattern => { const matches = updatedHeadContent.match(pattern); if (matches) { - updatedHeadContent = updatedHeadContent.replace(pattern, 'src="js/app-min.js"'); + updatedHeadContent = updatedHeadContent.replace(pattern, jsTarget); jsReplaced += matches.length; } }); @@ -68,7 +96,7 @@ function replaceAssets() { jsPatterns.forEach(pattern => { const matches = updatedBodyContent.match(pattern); if (matches) { - updatedBodyContent = updatedBodyContent.replace(pattern, 'src="js/app-min.js"'); + updatedBodyContent = updatedBodyContent.replace(pattern, jsTarget); jsReplaced += matches.length; } }); diff --git a/workbox-config.js b/workbox-config.js index be45328..323b41f 100644 --- a/workbox-config.js +++ b/workbox-config.js @@ -1,9 +1,8 @@ module.exports = { // Base directory - still needed for SW generation globDirectory: "dist/", - - // REMOVE globPatterns entirely - no precaching! - // globPatterns: [], // Explicitly empty or just omit + // Explicitly disable precaching for lean SW install + globPatterns: [], // Enhanced runtime caching strategies runtimeCaching: [ @@ -72,8 +71,8 @@ module.exports = { plugins: [ { handlerDidError: async () => { - // Return fallback image on error - return caches.match('/assets/fallback-image.svg'); + // Return fallback image on error (use an existing icon) + return caches.match('/favicon.svg'); } } ] @@ -81,9 +80,9 @@ module.exports = { }, - // 5. JSON responses - NetworkFirst with timeout + // 5. JSON responses (local and API) - NetworkFirst with timeout { - urlPattern: /^https:\/\/api\.|\/api\/|\.json$/i, + urlPattern: ({url}) => /\.json$/i.test(url.pathname) || /\/api\//i.test(url.pathname) || /^https:\/\/api\./i.test(url.href), handler: 'NetworkFirst', options: { cacheName: 'api-cache', @@ -92,12 +91,7 @@ module.exports = { maxEntries: 100, maxAgeSeconds: 5 * 60 // 5 minutes for fresh API data }, - cacheableResponse: { - statuses: [0, 200], - headers: { - 'x-cache': 'hit' // Optional: only cache specific responses - } - }, + cacheableResponse: { statuses: [0, 200] }, matchOptions: { ignoreSearch: false // Consider query params }