@sap/xssec
Version:
XS Advanced Container Security API for node.js
70 lines (59 loc) • 3.42 kB
JavaScript
const ResponseReplica = require("./ResponseReplica");
const ConfigurationError = require("../error/configuration/ConfigurationError");
const { getLogger } = require('../util/logging');
const LOG = getLogger("ResponseCache.js");
/**
* Caches responses from an endpoint with different request parameters that are eagerly refreshed in the background
* when accessed shortly before expiration.
*/
class ResponseCache {
static get DEFAULT_EXPIRATION_TIME() { return 30 * 60 * 1000; } // 30 minutes
static get DEFAULT_REFRESH_PERIOD() { return 15 * 60 * 1000; } // 15 minutes
/** @type {Map<string,ResponseReplica>} */
cache; // map that stores response replicas by their key
endpointName; // name of cached endpoint for logging purposes, e.g. "JWKS", ".well-known" etc.
expirationTime; // expiration time that will be used for new cache entries
refreshPeriod; // time before expiration in which a response is considered stale
constructor({
expirationTime = ResponseCache.DEFAULT_EXPIRATION_TIME,
refreshPeriod = ResponseCache.DEFAULT_REFRESH_PERIOD,
endpointName = "response"
} = {}) {
if (expirationTime < 0) {
throw new ConfigurationError("ResponseCache expirationTime must be >=0.")
}
if (refreshPeriod < 0 || refreshPeriod > expirationTime) {
throw new ConfigurationError("ResponseCache refreshPeriod must be between 0 and <expirationTime>.")
}
this.cache = new Map();
this.endpointName = endpointName;
this.expirationTime = expirationTime;
this.refreshPeriod = refreshPeriod;
}
/**
* Returns an up-to-date response associated with the given key. If there is no replica yet for the key, caches a new replica under this key that
* uses the given request callback to fetch its responses.
* @param key cache key of response
* @param buildRequest callback that constructs a request function for fetching new responses if no replica exists yet for the key. The request function
* has to throw an Error with a statusCode and statusText if it fails to fetch the data.
*/
async getOrRequest(key, buildRequest, { correlationId }) {
const replica = this.cache.get(key) || this.#createReplica(key, buildRequest());
if (!replica.hasData() || replica.isExpired()) {
// await synchronous response refresh if replica response data is missing or expired
LOG.debug(`Awaiting ${this.endpointName} refresh because replica for key=${key} has ${replica.hasData() ? "expired" : "no"} data.)`, { correlationId });
await replica.refresh(correlationId);
} else if (replica.isStale(this.refreshPeriod)) {
// trigger asynchronous response refresh in background if replica is stale
LOG.debug(`Asynchronous ${this.endpointName} refresh scheduled because replica for key=${key} is stale (remaining time = ${replica.remainingTime}ms < ${replica.refreshPeriod}ms = refresh period).`);
replica.refresh(correlationId).catch(() => { }); // silently catch rejected promise to prevent uncaught rejection errors
}
return replica.data;
}
#createReplica(key, request) {
const replica = new ResponseReplica(this, key, request);
this.cache.set(key, replica);
return replica;
}
}
module.exports = ResponseCache;