UNPKG

transitory

Version:

In-memory cache with high hit rates via LFU eviction. Supports time-based expiration, automatic loading and metrics.

328 lines (288 loc) 8.64 kB
import { BoundedCache } from '../cache/bounded/BoundedCache'; import { BoundlessCache } from '../cache/boundless/BoundlessCache'; import { Cache } from '../cache/Cache'; import { Expirable } from '../cache/expiration/Expirable'; import { ExpirationCache } from '../cache/expiration/ExpirationCache'; import { MaxAgeDecider } from '../cache/expiration/MaxAgeDecider'; import { KeyType } from '../cache/KeyType'; import { DefaultLoadingCache } from '../cache/loading/DefaultLoadingCache'; import { Loader } from '../cache/loading/Loader'; import { LoadingCache } from '../cache/loading/LoadingCache'; import { MetricsCache } from '../cache/metrics/MetricsCache'; import { RemovalListener } from '../cache/RemovalListener'; import { Weigher } from '../cache/Weigher'; export interface CacheBuilder<K extends KeyType, V> { /** * Set a listener that will be called every time something is removed * from the cache. */ withRemovalListener(listener: RemovalListener<K, V>): this; /** * Set the maximum number of items to keep in the cache before evicting * something. */ maxSize(size: number): this; /** * Set a function to use to determine the size of a cached object. */ withWeigher(weigher: Weigher<K, V>): this; /** * Change to a cache where get can also resolve values if provided with * a function as the second argument. */ loading(): LoadingCacheBuilder<K, V>; /** * Change to a loading cache, where the get-method will return instances * of Promise and automatically load unknown values. */ withLoader(loader: Loader<K, V>): LoadingCacheBuilder<K, V>; /** * Set that the cache should expire items some time after they have been * written to the cache. */ expireAfterWrite(time: number | MaxAgeDecider<K, V>): this; /** * Set that the cache should expire items some time after they have been * read from the cache. */ expireAfterRead(time: number | MaxAgeDecider<K, V>): this; /** * Activate tracking of metrics for this cache. */ metrics(): this; /** * Build the cache. */ build(): Cache<K, V>; } export interface LoadingCacheBuilder<K extends KeyType, V> extends CacheBuilder<K, V> { /** * Build the cache. */ build(): LoadingCache<K, V>; } /** * Builder for cache instances. */ export class CacheBuilderImpl<K extends KeyType, V> implements CacheBuilder<K, V> { private optRemovalListener?: RemovalListener<K, V>; private optMaxSize?: number; private optWeigher?: Weigher<K, V>; private optMaxWriteAge?: MaxAgeDecider<K, V>; private optMaxNoReadAge?: MaxAgeDecider<K, V>; private optMetrics: boolean = false; /** * Set a listener that will be called every time something is removed * from the cache. * * @param listener - * removal listener to use * @returns self */ public withRemovalListener(listener: RemovalListener<K, V>) { this.optRemovalListener = listener; return this; } /** * Set the maximum number of items to keep in the cache before evicting * something. * * @param size - * number of items to keep * @returns self */ public maxSize(size: number) { this.optMaxSize = size; return this; } /** * Set a function to use to determine the size of a cached object. * * @param weigher - * function used to weight objects * @returns self */ public withWeigher(weigher: Weigher<K, V>) { if(typeof weigher !== 'function') { throw new Error('Weigher should be a function that takes a key and value and returns a number'); } this.optWeigher = weigher; return this; } /** * Change to a cache where get can also resolve values if provided with * a function as the second argument. * * @returns self */ public loading(): LoadingCacheBuilder<K, V> { return new LoadingCacheBuilderImpl(this, null); } /** * Change to a loading cache, where the get-method will return instances * of Promise and automatically load unknown values. * * @param loader - * function used to load objects * @returns self */ public withLoader(loader: Loader<K, V>): LoadingCacheBuilder<K, V> { if(typeof loader !== 'function') { throw new Error('Loader should be a function that takes a key and returns a value or a promise that resolves to a value'); } return new LoadingCacheBuilderImpl(this, loader); } /** * Set that the cache should expire items after some time. * * @param time - * max time in milliseconds, or function that will be asked per key/value * for expiration time * @returns self */ public expireAfterWrite(time: number | MaxAgeDecider<K, V>) { let evaluator; if(typeof time === 'function') { evaluator = time; } else if(typeof time === 'number') { evaluator = () => time; } else { throw new Error('expireAfterWrite needs either a maximum age as a number or a function that returns a number'); } this.optMaxWriteAge = evaluator; return this; } /** * Set that the cache should expire items some time after they have been read. * * @param time - * max time in milliseconds, or function will be asked per key/value * for expiration time * @returns self */ public expireAfterRead(time: number | MaxAgeDecider<K, V>): this { let evaluator; if(typeof time === 'function') { evaluator = time; } else if(typeof time === 'number') { evaluator = () => time; } else { throw new Error('expireAfterRead needs either a maximum age as a number or a function that returns a number'); } this.optMaxNoReadAge = evaluator; return this; } /** * Activate tracking of metrics for this cache. * * @returns self */ public metrics(): this { this.optMetrics = true; return this; } /** * Build and return the cache. * * @returns cache */ public build() { let cache: Cache<K, V>; if(typeof this.optMaxWriteAge !== 'undefined' || typeof this.optMaxNoReadAge !== 'undefined') { /* * Requested expiration - wrap the base cache a bit as it needs * custom types, a custom weigher if used and removal listeners * are added on the expiration cache instead. */ let parentCache: Cache<K, Expirable<V>>; if(this.optMaxSize) { parentCache = new BoundedCache({ maxSize: this.optMaxSize, weigher: createExpirableWeigher(this.optWeigher) }); } else { parentCache = new BoundlessCache({}); } cache = new ExpirationCache({ maxNoReadAge: this.optMaxNoReadAge, maxWriteAge: this.optMaxWriteAge, removalListener: this.optRemovalListener, parent: parentCache }); } else { if(this.optMaxSize) { cache = new BoundedCache({ maxSize: this.optMaxSize, weigher: this.optWeigher, removalListener: this.optRemovalListener }); } else { cache = new BoundlessCache({ removalListener: this.optRemovalListener }); } } if(this.optMetrics) { // Collect metrics if requested cache = new MetricsCache({ parent: cache }); } return cache; } } class LoadingCacheBuilderImpl<K extends KeyType, V> implements LoadingCacheBuilder<K, V> { private parent: CacheBuilder<K, V>; private loader: Loader<K, V> | null; public constructor(parent: CacheBuilder<K, V>, loader: Loader<K, V> | null) { this.parent = parent; this.loader = loader; } public withRemovalListener(listener: RemovalListener<K, V>): this { this.parent.withRemovalListener(listener); return this; } public maxSize(size: number): this { this.parent.maxSize(size); return this; } public withWeigher(weigher: Weigher<K, V>): this { this.parent.withWeigher(weigher); return this; } public loading(): LoadingCacheBuilder<K, V> { throw new Error('Already building a loading cache'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars public withLoader(loader: Loader<K, V>): LoadingCacheBuilder<K, V> { throw new Error('Already building a loading cache'); } public expireAfterWrite(time: number | MaxAgeDecider<K, V>): this { this.parent.expireAfterWrite(time); return this; } public expireAfterRead(time: number | MaxAgeDecider<K, V>): this { this.parent.expireAfterRead(time); return this; } public metrics(): this { this.parent.metrics(); return this; } public build(): LoadingCache<K, V> { return new DefaultLoadingCache({ loader: this.loader, parent: this.parent.build() }); } } /** * Helper function to create a weigher that uses an Expirable object. * * @param w - * @returns weigher */ function createExpirableWeigher<K extends KeyType, V>(w: Weigher<K, V> | undefined): Weigher<K, Expirable<V>> | null { if(! w) return null; return (key, node) => w(key, node.value as V); }