feat: implement fetch with timeout and fallback for improved data loading reliability
This commit is contained in:
parent
3f16bc36e7
commit
946d8d695c
3 changed files with 74 additions and 25 deletions
45
dist/js/app.js
vendored
45
dist/js/app.js
vendored
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue