freealberta/map/app/utils/cacheBusting.js

180 lines
5.6 KiB
JavaScript

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