UNPKG

@linkedmink/multilevel-aging-cache

Version:

Package provides an interface to cache and persist data to Redis, MongoDB, memory

170 lines (146 loc) 5.43 kB
import { IDisposable } from '../shared/IDisposable'; import { IAgingCache, AgingCacheWriteStatus, KeyValueArray, IAgingCacheWrite } from './IAgingCache'; import { IAgedQueue } from '../queue/IAgedQueue'; import { Logger } from '../shared/Logger'; import { IStorageHierarchy } from '../storage/IStorageHierarchy'; import { IAgingCacheDeleteStrategy, IAgingCacheSetStrategy } from './IAgingCacheWriteStrategy'; /** * A cache that will replace entries in the order specified by the input IAgedQueue */ export class AgingCache<TKey, TValue> implements IAgingCache<TKey, TValue>, IDisposable { private readonly logger = Logger.get(AgingCache.name); private readonly purgeInterval: number; private purgeTimer?: NodeJS.Timeout; private purgePromise?: Promise<void>; /** * @param hierarchy The storage hierarchy to operate on * @param evictQueue The keys in the order to evict * @param setStrategy The implementation for setting keys * @param deleteStrategy The implementation for deleting keys * @param purgeInterval The interval to check for old entries in seconds */ constructor( private readonly hierarchy: IStorageHierarchy<TKey, TValue>, private readonly evictQueue: IAgedQueue<TKey>, private readonly setStrategy: IAgingCacheSetStrategy<TKey, TValue>, private readonly deleteStrategy: IAgingCacheDeleteStrategy<TKey, TValue>, private readonly evictAtLevel?: number, purgeInterval = 30 ) { this.purgeInterval = purgeInterval * 1000; // eslint-disable-next-line @typescript-eslint/no-misused-promises this.purgeTimer = setInterval(this.purge, this.purgeInterval); } /** * Clean up the object when it's no longer used. After a dispose(), an object * is no longer guaranteed to be usable. */ public dispose(): Promise<void> | void { this.logger.info(`Cleaning up cache`); if (this.purgeTimer) { clearInterval(this.purgeTimer); this.purgeTimer = undefined; } return this.purgePromise; } /** * @param key The key to retrieve * @returns The value if it's in the cache or undefined */ public get(key: TKey, force = false): Promise<TValue | null> { this.logger.debug(`Getting Key: ${key}`); return this.hierarchy.getAtLevel(key, undefined, !force).then(agedValue => { if (agedValue) { return agedValue.value; } return null; }); } /** * @param key The key to set * @param value The value to set * @returns If setting the value was successful */ public set(key: TKey, value: TValue, force = false): Promise<IAgingCacheWrite<TValue>> { this.logger.debug(`Setting Key: ${key}`); if (this.evictQueue.isNextExpired()) { void this.evict(); } return this.setStrategy.set(key, value, force); } /** * @param key The key to the value to delete * @returns If deleting the value was successful */ public delete(key: TKey, force = false): Promise<IAgingCacheWrite<TValue>> { this.logger.debug(`Deleting Key: ${key}`); return this.deleteStrategy.delete(key, force); } /** * @returns The keys that are currently in the cache */ public keys(): Promise<TKey[]> { this.logger.debug('Getting Key List'); return this.hierarchy.getKeysAtTopLevel(); } /** * @param key The key to the value to clear from cache layers * @param force If true write to levels below the persistence layer * @returns If the write succeeded or the error condition */ public clear(key: TKey, force?: boolean): Promise<IAgingCacheWrite<TValue>> { return this.deleteStrategy.evict(key, this.evictAtLevel, force); } /** * @returns The next value that's set to expire or null when nothing will expire */ public peek(): Promise<TValue | null> { const nextKey = this.evictQueue.next(); if (nextKey) { return this.hierarchy.getValueAtBottomLevel(nextKey).then(v => (v ? v.value : null)); } return Promise.resolve(null); } public load(keyValues: KeyValueArray<TKey, TValue>): Promise<number> { const promises = keyValues.map(kv => { return this.setStrategy.load(kv.key, kv.val, this.evictAtLevel); }); return Promise.all(promises).then(all => { return all.reduce((acc, next) => { return next.status === AgingCacheWriteStatus.Success || next.status === AgingCacheWriteStatus.PartialWrite ? acc++ : acc; }, 0); }); } /** * Purge the cache of stale entries instead of waiting for a periodic check * @return A promise to track when the purge finishes */ public purge = (): Promise<void> => { if (!this.purgePromise) { this.logger.debug(`Starting Purge: ${Date.now()}`); this.purgePromise = this.purgeNext().then(() => (this.purgePromise = undefined)); } return this.purgePromise; }; private purgeNext(): Promise<void> { if (this.evictQueue.isNextExpired()) { return this.evict().then(write => { if (write?.status === AgingCacheWriteStatus.Success) { return this.purgeNext(); } }); } return Promise.resolve(); } private evict(): Promise<IAgingCacheWrite<TValue> | null> { const nextKey = this.evictQueue.next(); if (nextKey) { this.logger.debug(`Evicting Key: ${nextKey}`); return this.deleteStrategy.evict(nextKey, this.evictAtLevel); } return Promise.resolve(null); } }