Created
November 30, 2022 15:25
-
-
Save lowedown/bdb37f3dec5589489c164810f86d2b34 to your computer and use it in GitHub Desktop.
JSS Headless Proxy Cache Middleware that polls ContentDelivery for published changes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
in index.js add the following line:
server.use(pollingCacheMiddleware());