Skip to content

Instantly share code, notes, and snippets.

@lowedown
Created November 30, 2022 15:25
Show Gist options
  • Save lowedown/bdb37f3dec5589489c164810f86d2b34 to your computer and use it in GitHub Desktop.
Save lowedown/bdb37f3dec5589489c164810f86d2b34 to your computer and use it in GitHub Desktop.
JSS Headless Proxy Cache Middleware that polls ContentDelivery for published changes
//cacheMiddleware for Node-level output caching
const mcache = require('memory-cache');
const config = require('./config');
const https = require('https');
const CACHE_TIME_MS = 8 * 60 * 60 * 1000; // 8h
const POLLING_INTERVAL_MS = 5000;
const STATS_LOGGING_INTERVAL_MS = 60000;
// List of urls that will be skipped during caching
// Bypassed urls are not cached
const EXCLUDED_PATHS = [,
].concat(config.pathRewriteExcludeRoutes);
const EXCLUDED_EXTENSIONS = [
'.aspx',
'.html',
'.txt',
'.ico'
]
const INCLUDED_CONTENT_TYPES = [
'text/html',
]
let cacheExcludedUrls = [];
let lastPublishTimestamp = new Date();
let OutputCacheEnabled = false; // Initially disabled until activated through CD status call
let cacheHits = 0;
let cacheMisses = 0;
/**
* @param {string} method request method
* @param {string} url request url
* @returns {boolean} is path excluded
*/
const isExcludedPath = (method, url) => {
if (method !== 'GET') return true;
if (url === '/') return true; // Don't cache root because of language issues
if (!!EXCLUDED_PATHS.find(toExclude => toExclude && url.startsWith(toExclude.toLowerCase()))) return true;
if (!!EXCLUDED_EXTENSIONS.find(toExclude => url.endsWith(toExclude))) return true;
return false;
}
/**
* Cache requests during {@link duration} that aren't excluded in {@link EXCLUDED_PATHS}
* @param {number} duration The number of milliseconds to cache request
*/
const pollingCacheMiddleware = (duration = CACHE_TIME_MS) => (req, res, next) => {
if (!OutputCacheEnabled) return next();
const urlPathOnly = req.originalUrl.split('?')[0].toLowerCase();
const urlLowerCase = urlPathOnly.endsWith('/') ? urlPathOnly : `${urlPathOnly}/`;
if (isExcludedPath(req.method, urlLowerCase)) return next();
const key = '__proxy_cache__' + urlLowerCase || req.url
const cachedBody = mcache.get(key)
if (cachedBody) {
cacheHits++;
res.send(cachedBody);
return;
}
cacheMisses++;
const { end: _end, write: _write } = res;
let buffer = new Buffer.alloc(0);
// Rewrite response method and get the content.
res.write = data => {
buffer = Buffer.concat([buffer, data]);
};
res.end = () => {
const body = buffer.toString();
const contentType = res.get('content-type');
const contentTypeMatches = contentType && !!INCLUDED_CONTENT_TYPES.find(included => contentType.toLowerCase().startsWith(included));
const statusCodeOk = res.statusCode === 200;
if (contentTypeMatches && statusCodeOk) {
mcache.put(key, body + `\n<!-- OutputCache ${new Date().toISOString()} -->`, duration);
}
_write.call(res, body);
_end.call(res);
};
next()
}
const checkCDStatus = () => {
const currentMemMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
console.info(`OutputCache: Checking CD status. Memory: ${currentMemMB} MB. Last publish: ${lastPublishTimestamp}. CacheEnabled: ${OutputCacheEnabled}`);
const statusEndpoint = `${config.apiHost}/api/jssOutputCache/status`;
https.get(statusEndpoint, (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
if (resp.statusCode != 200) {
console.error(`OutputCache: Unable to reach CD servers at '${statusEndpoint}'`, resp.statusCode);
return;
}
let result = JSON.parse(data);
if (OutputCacheEnabled && !result.OutputCacheEnabled) {
console.info("OutputCache: Cache has been disabled. Clearing cache.");
mcache.clear();
}
OutputCacheEnabled = result.OutputCacheEnabled;
if (lastPublishTimestamp < result.LastPublish) {
console.info("OutputCache: Publish detected. Clearing cache.");
mcache.clear();
}
lastPublishTimestamp = result.LastPublish;
});
}).on("error", (err) => {
console.error(`Error getting status from '${statusEndpoint}': ${err.message}`);
});
}
const logStats = () => {
if (!OutputCacheEnabled) {
return;
}
console.log(`OutputCache: Stats: Hits: ${cacheHits}; Misses: ${cacheMisses}; In the last ${STATS_LOGGING_INTERVAL_MS / 1000}s`)
cacheHits = 0;
cacheMisses = 0;
}
// Poll
setInterval(function() {
checkCDStatus();
}, POLLING_INTERVAL_MS);
setInterval(function() {
logStats();
}, STATS_LOGGING_INTERVAL_MS);
module.exports = pollingCacheMiddleware;
@lowedown
Copy link
Author

in index.js add the following line:
server.use(pollingCacheMiddleware());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment