235 lines
5.9 KiB
JavaScript
235 lines
5.9 KiB
JavaScript
// @flow
|
|
|
|
export type SimpleCacheConfigurator = SimpleCacheConfiguratorFn &
|
|
SimpleCacheConfiguratorObj;
|
|
|
|
type SimpleCacheConfiguratorFn = {
|
|
(boolean): void,
|
|
<T>(handler: () => T): T,
|
|
};
|
|
type SimpleCacheConfiguratorObj = {
|
|
forever: () => void,
|
|
never: () => void,
|
|
using: <T>(handler: () => T) => T,
|
|
invalidate: <T>(handler: () => T) => T,
|
|
};
|
|
|
|
type CacheEntry<ResultT, SideChannel> = 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<ArgT, ResultT, SideChannel>(
|
|
handler: (ArgT, CacheConfigurator<SideChannel>) => 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<SideChannel>) => ResultT,
|
|
): (ArgT, SideChannel) => ResultT {
|
|
return makeCachedFunction(new WeakMap(), handler);
|
|
}
|
|
|
|
type CacheMap<ArgT, ResultT, SideChannel> =
|
|
| Map<ArgT, CacheEntry<ResultT, SideChannel>>
|
|
| WeakMap<ArgT, CacheEntry<ResultT, SideChannel>>;
|
|
|
|
function makeCachedFunction<
|
|
ArgT,
|
|
ResultT,
|
|
SideChannel,
|
|
Cache: CacheMap<ArgT, ResultT, SideChannel>,
|
|
>(
|
|
callCache: Cache,
|
|
handler: (ArgT, CacheConfigurator<SideChannel>) => ResultT,
|
|
): (ArgT, SideChannel) => ResultT {
|
|
return function cachedFunction(arg, data) {
|
|
let cachedValue: CacheEntry<ResultT, SideChannel> | 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<SideChannel = void> {
|
|
_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<T>(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<T>(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<any>,
|
|
): 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;
|
|
}
|