feat: implement fetch with timeout and fallback for improved data loading reliability

This commit is contained in:
fynks 2025-09-18 10:52:36 +05:00
parent 3f16bc36e7
commit 946d8d695c
3 changed files with 74 additions and 25 deletions

45
dist/js/app.js vendored
View file

@ -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 = `<div class="error-state"><p>Failed to load data: <span class="error-dataurl">${dataUrls[index]}</span></p></div>`;
if (el) el.innerHTML = `<div class="error-state"><p>Failed to load data. Please check your connection and refresh.</p></div>`;
}
});
@ -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);
});
});

View file

@ -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;
}
});

View file

@ -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
}