// @flow export type SimpleCacheConfigurator = SimpleCacheConfiguratorFn & SimpleCacheConfiguratorObj; type SimpleCacheConfiguratorFn = { (boolean): void, (handler: () => T): T, }; type SimpleCacheConfiguratorObj = { forever: () => void, never: () => void, using: (handler: () => T) => T, invalidate: (handler: () => T) => T, }; type CacheEntry = Array<{ value: ResultT, valid: SideChannel => boolean, }>; export type { CacheConfigurator }; /** * Given a function with a single argument, cache its results based on its argument and how it * configures its caching behavior. Cached values are stored strongly. */ export function makeStrongCache( handler: (ArgT, CacheConfigurator) => ResultT, ): (ArgT, SideChannel) => ResultT { return makeCachedFunction(new Map(), handler); } /** * Given a function with a single argument, cache its results based on its argument and how it * configures its caching behavior. Cached values are stored weakly and the function argument must be * an object type. */ export function makeWeakCache< ArgT: {} | Array<*> | $ReadOnlyArray<*>, ResultT, SideChannel, >( handler: (ArgT, CacheConfigurator) => ResultT, ): (ArgT, SideChannel) => ResultT { return makeCachedFunction(new WeakMap(), handler); } type CacheMap = | Map> | WeakMap>; function makeCachedFunction< ArgT, ResultT, SideChannel, Cache: CacheMap, >( callCache: Cache, handler: (ArgT, CacheConfigurator) => ResultT, ): (ArgT, SideChannel) => ResultT { return function cachedFunction(arg, data) { let cachedValue: CacheEntry | void = callCache.get( arg, ); if (cachedValue) { for (const { value, valid } of cachedValue) { if (valid(data)) return value; } } const cache = new CacheConfigurator(data); const value = handler(arg, cache); if (!cache.configured()) cache.forever(); cache.deactivate(); switch (cache.mode()) { case "forever": cachedValue = [{ value, valid: () => true }]; callCache.set(arg, cachedValue); break; case "invalidate": cachedValue = [{ value, valid: cache.validator() }]; callCache.set(arg, cachedValue); break; case "valid": if (cachedValue) { cachedValue.push({ value, valid: cache.validator() }); } else { cachedValue = [{ value, valid: cache.validator() }]; callCache.set(arg, cachedValue); } } return value; }; } class CacheConfigurator { _active: boolean = true; _never: boolean = false; _forever: boolean = false; _invalidate: boolean = false; _configured: boolean = false; _pairs: Array<[mixed, (SideChannel) => mixed]> = []; _data: SideChannel; constructor(data: SideChannel) { this._data = data; } simple() { return makeSimpleConfigurator(this); } mode() { if (this._never) return "never"; if (this._forever) return "forever"; if (this._invalidate) return "invalidate"; return "valid"; } forever() { if (!this._active) { throw new Error("Cannot change caching after evaluation has completed."); } if (this._never) { throw new Error("Caching has already been configured with .never()"); } this._forever = true; this._configured = true; } never() { if (!this._active) { throw new Error("Cannot change caching after evaluation has completed."); } if (this._forever) { throw new Error("Caching has already been configured with .forever()"); } this._never = true; this._configured = true; } using(handler: SideChannel => T): T { if (!this._active) { throw new Error("Cannot change caching after evaluation has completed."); } if (this._never || this._forever) { throw new Error( "Caching has already been configured with .never or .forever()", ); } this._configured = true; const key = handler(this._data); this._pairs.push([key, handler]); return key; } invalidate(handler: SideChannel => T): T { if (!this._active) { throw new Error("Cannot change caching after evaluation has completed."); } if (this._never || this._forever) { throw new Error( "Caching has already been configured with .never or .forever()", ); } this._invalidate = true; this._configured = true; const key = handler(this._data); this._pairs.push([key, handler]); return key; } validator(): SideChannel => boolean { const pairs = this._pairs; return (data: SideChannel) => pairs.every(([key, fn]) => key === fn(data)); } deactivate() { this._active = false; } configured() { return this._configured; } } function makeSimpleConfigurator( cache: CacheConfigurator, ): SimpleCacheConfigurator { function cacheFn(val) { if (typeof val === "boolean") { if (val) cache.forever(); else cache.never(); return; } return cache.using(() => assertSimpleType(val())); } cacheFn.forever = () => cache.forever(); cacheFn.never = () => cache.never(); cacheFn.using = cb => cache.using(() => assertSimpleType(cb())); cacheFn.invalidate = cb => cache.invalidate(() => assertSimpleType(cb())); return (cacheFn: any); } // Types are limited here so that in the future these values can be used // as part of Babel's caching logic. type SimpleType = string | boolean | number | null | void; export function assertSimpleType(value: mixed): SimpleType { if ( value != null && typeof value !== "string" && typeof value !== "boolean" && typeof value !== "number" ) { throw new Error( "Cache keys must be either string, boolean, number, null, or undefined.", ); } return value; }