transitory
Version:
In-memory cache with high hit rates via LFU eviction. Supports time-based expiration, automatic loading and metrics.
95 lines (76 loc) • 2.46 kB
text/typescript
import { Cache } from '../Cache';
import { CommonCacheOptions } from '../CommonCacheOptions';
import { KeyType } from '../KeyType';
import { WrappedCache } from '../WrappedCache';
import { Loader } from './Loader';
import { LoadingCache } from './LoadingCache';
const DATA = Symbol('loadingData');
/**
* Options available for a loading cache.
*/
export interface LoadingCacheOptions<K extends KeyType, V> extends CommonCacheOptions<K, V> {
loader?: Loader<K, V> | undefined | null;
parent: Cache<K, V>;
}
interface LoadingCacheData<K extends KeyType, V> {
promises: Map<K, Promise<V>>;
loader: Loader<K, V> | null;
}
/**
* Extension to another cache that will load items if they are not cached.
*/
export class DefaultLoadingCache<K extends KeyType, V> extends WrappedCache<K, V> implements LoadingCache<K, V> {
private [DATA]: LoadingCacheData<K, V>;
public constructor(options: LoadingCacheOptions<K, V>) {
super(options.parent, options.removalListener || null);
this[DATA] = {
promises: new Map(),
loader: options.loader || null
};
}
/**
* Get cached value or load it if not currently cached. Updates the usage
* of the key.
*
* @param key -
* key to get
* @param loader -
* optional loader to use for loading the object
* @returns
* promise that resolves to the loaded value
*/
public get(key: K, loader?: Loader<K, V>): Promise<V> {
const currentValue = this.getIfPresent(key);
if(currentValue !== null) {
return Promise.resolve(currentValue);
}
const data = this[DATA];
// First check if we are already loading this value
let promise = data.promises.get(key);
if(promise) return promise;
// Create the initial promise if we are not already loading
if(typeof loader !== 'undefined') {
if(typeof loader !== 'function') {
throw new Error('If loader is used it must be a function that returns a value or a Promise');
}
promise = Promise.resolve(loader(key));
} else if(data.loader) {
promise = Promise.resolve(data.loader(key));
}
if(! promise) {
throw new Error('No way to load data for key: ' + key);
}
// Enhance with handler that will remove promise and set value if success
const resolve = () => data.promises.delete(key);
promise = promise.then(result => {
this.set(key, result);
resolve();
return result;
}).catch(err => {
resolve();
throw err;
});
data.promises.set(key, promise);
return promise;
}
}