A tonne of updates to how the system builds the view points in hopes of having a better mobile expereince
This commit is contained in:
179
map/app/utils/cacheBusting.js
Normal file
179
map/app/utils/cacheBusting.js
Normal file
@@ -0,0 +1,179 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Cache busting utility for preventing browser caching of static assets
|
||||
*/
|
||||
class CacheBusting {
|
||||
constructor() {
|
||||
// Generate a unique version identifier on server start
|
||||
this.version = this.generateVersion();
|
||||
this.fileHashes = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a version identifier based on server start time and random data
|
||||
* @returns {string} Version string
|
||||
*/
|
||||
generateVersion() {
|
||||
const timestamp = Date.now();
|
||||
const random = crypto.randomBytes(4).toString('hex');
|
||||
return `${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache busting version for the application
|
||||
* @returns {string} Version string
|
||||
*/
|
||||
getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a file hash for specific file-based cache busting
|
||||
* @param {string} filePath - Path to the file
|
||||
* @returns {string} File hash or version fallback
|
||||
*/
|
||||
getFileHash(filePath) {
|
||||
try {
|
||||
if (this.fileHashes.has(filePath)) {
|
||||
return this.fileHashes.get(filePath);
|
||||
}
|
||||
|
||||
const fullPath = path.join(__dirname, '..', 'public', filePath);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
const content = fs.readFileSync(fullPath);
|
||||
const hash = crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
|
||||
this.fileHashes.set(filePath, hash);
|
||||
return hash;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Cache busting: Could not hash file ${filePath}:`, error.message);
|
||||
}
|
||||
|
||||
// Fallback to version
|
||||
return this.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add cache busting parameter to a URL
|
||||
* @param {string} url - Original URL
|
||||
* @param {boolean} useFileHash - Whether to use file-specific hash
|
||||
* @returns {string} URL with cache busting parameter
|
||||
*/
|
||||
bustCache(url, useFileHash = false) {
|
||||
if (!url) return url;
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const version = useFileHash ? this.getFileHash(url) : this.version;
|
||||
|
||||
return `${url}${separator}v=${version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache headers for static assets
|
||||
* @param {boolean} longTerm - Whether to use long-term caching
|
||||
* @returns {object} Cache headers
|
||||
*/
|
||||
getCacheHeaders(longTerm = false) {
|
||||
if (longTerm) {
|
||||
// Long-term caching for versioned assets
|
||||
return {
|
||||
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
|
||||
'ETag': this.version
|
||||
};
|
||||
} else {
|
||||
// Short-term caching for frequently changing content
|
||||
return {
|
||||
'Cache-Control': 'public, max-age=300', // 5 minutes
|
||||
'ETag': this.version
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get no-cache headers for dynamic content
|
||||
* @returns {object} No-cache headers
|
||||
*/
|
||||
getNoCacheHeaders() {
|
||||
return {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to add cache busting to HTML responses
|
||||
* @returns {function} Express middleware
|
||||
*/
|
||||
htmlMiddleware() {
|
||||
return (req, res, next) => {
|
||||
// Store original send method
|
||||
const originalSend = res.send;
|
||||
|
||||
// Override send method to modify HTML content
|
||||
res.send = (body) => {
|
||||
if (typeof body === 'string' && res.get('Content-Type')?.includes('text/html')) {
|
||||
// Add cache busting to CSS and JS files
|
||||
body = body.replace(
|
||||
/(href|src)=["']([^"']+\.(css|js))["']/g,
|
||||
(match, attr, url, ext) => {
|
||||
// Skip external URLs
|
||||
if (url.startsWith('http') || url.startsWith('//')) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const bustedUrl = this.bustCache(url, true);
|
||||
return `${attr}="${bustedUrl}"`;
|
||||
}
|
||||
);
|
||||
|
||||
// Add version info to HTML for debugging
|
||||
body = body.replace(
|
||||
'</head>',
|
||||
` <meta name="app-version" content="${this.version}">\n</head>`
|
||||
);
|
||||
}
|
||||
|
||||
// Call original send method
|
||||
originalSend.call(res, body);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to set cache headers for static files
|
||||
* @returns {function} Express middleware
|
||||
*/
|
||||
staticCacheMiddleware() {
|
||||
return (req, res, next) => {
|
||||
const ext = path.extname(req.path).toLowerCase();
|
||||
const isVersioned = req.query.v !== undefined;
|
||||
|
||||
// Set cache headers based on file type and versioning
|
||||
if (['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg'].includes(ext)) {
|
||||
const headers = isVersioned ?
|
||||
this.getCacheHeaders(true) :
|
||||
this.getCacheHeaders(false);
|
||||
|
||||
Object.keys(headers).forEach(key => {
|
||||
res.set(key, headers[key]);
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const cacheBusting = new CacheBusting();
|
||||
|
||||
module.exports = {
|
||||
CacheBusting,
|
||||
cacheBusting
|
||||
};
|
||||
Reference in New Issue
Block a user