UNPKG

@sap/xssec

Version:

XS Advanced Container Security API for node.js

70 lines (59 loc) 3.42 kB
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;