import {Clock, Instant} from '@croct/time'; import {DefaultClockProvider} from '@croct/time/defaultClockProvider'; import {CacheLoader, CacheProvider} from './cacheProvider'; import {TimestampedCacheEntry} from './timestampedCacheEntry'; type Configuration = { /** * The underlying cache provider to use for storing the cached data. */ cacheProvider: CacheProvider>, /** * The maximum retention period of the cached data in seconds. * * It defines the time duration for which the cache entry is * retained. Once the maximum age expires, the cache entry * will be removed and a new value will be retrieved using * the provided loader. * * For example, if maxAge is set to 3600 seconds (1 hour), * the cache entry will be retained for 1 hour. After 1 hour, * it will be removed and a new value will be retrieved. */ maxAge: number | ((key: K, value: V) => number), /** * The clock to use for time-related operations. * * It is used for retrieving the current time and for time calculations. * If not provided, the default clock is used. * * @default DefaultClockProvider.getClock() */ clock?: Clock, }; export class HoldWhileRevalidateCache implements CacheProvider { private readonly cacheProvider: Configuration['cacheProvider']; private readonly maxAge: number | ((key: K, value: V) => number); private readonly clock: Clock; public constructor({cacheProvider, maxAge, clock}: Configuration) { this.cacheProvider = cacheProvider; this.maxAge = maxAge; this.clock = clock ?? DefaultClockProvider.getClock(); } public async get(key: K, loader: CacheLoader): Promise { const now = Instant.now(this.clock); const retrieveAndSave = async (): Promise> => { const entry: TimestampedCacheEntry = { value: await loader(key), timestamp: now, }; await this.cacheProvider.set(key, entry); return entry; }; const possiblyStaleEntry = await this.cacheProvider.get(key, retrieveAndSave); const maxAge = typeof this.maxAge === 'function' ? this.maxAge(key, possiblyStaleEntry.value) : this.maxAge; if (now.isAfter(possiblyStaleEntry.timestamp.plusSeconds(maxAge))) { const entry = await retrieveAndSave(); return entry.value; } return possiblyStaleEntry.value; } public set(key: K, value: V): Promise { return this.cacheProvider.set(key, { value: value, timestamp: Instant.now(this.clock), }); } public delete(key: K): Promise { return this.cacheProvider.delete(key); } }