Skip to content

Instantly share code, notes, and snippets.

@dscheerens
Created June 16, 2023 08:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dscheerens/92f18c53f9f7cac3b80451d153858fa1 to your computer and use it in GitHub Desktop.
Save dscheerens/92f18c53f9f7cac3b80451d153858fa1 to your computer and use it in GitHub Desktop.
TypeScript decorator to apply caching on functions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface CachedDecoratorOptions<FunctionArguments extends any[]> {
keyGenerator?: CacheKeyGenerator<FunctionArguments>;
cache?: CacheFactory;
}
export type CacheKeyGenerator<T extends any[]> = (...args: T) => unknown; // eslint-disable-line @typescript-eslint/no-explicit-any
export interface Cache {
get(cacheKey: unknown): unknown;
set(cacheKey: unknown, value: unknown): void;
}
export interface CacheFactory {
createCache(): Cache;
}
export const CACHE_ENTRY_MISSING = Symbol('CACHE_ENTRY_MISSING');
const EMPTY_ARGUMENTS = Symbol('EMPTY_ARGUMENTS');
const CACHE_MAP = new WeakMap<object, Map<string, Cache>>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function Cached<FunctionArguments extends any[]>(
options?: CachedDecoratorOptions<FunctionArguments>,
): <FunctionType extends (...args: FunctionArguments) => unknown>(
target: object,
propertyKey: string,
descriptor: TypedPropertyDescriptor<FunctionType>,
) => TypedPropertyDescriptor<FunctionType> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
const keyGenerator: (...args: unknown[]) => unknown = (options?.keyGenerator as any) ?? defaultCacheKeyGenerator;
return (target, propertyKey, descriptor) => {
if (descriptor.value === undefined) {
throw new Error(`Cannot apply @Cached decorator to '${target.constructor.name}.${propertyKey}' since it has no value`);
}
const originalFunction = descriptor.value;
function cachedFunction(this: typeof target, ...args: FunctionArguments): unknown {
let thisScopedCacheMap = CACHE_MAP.get(this);
if (!thisScopedCacheMap) {
thisScopedCacheMap = new Map<string, Cache>();
CACHE_MAP.set(this, thisScopedCacheMap);
}
let cache = thisScopedCacheMap.get(propertyKey);
if (!cache) {
cache = (options?.cache ?? SINGLE_ENTRY_CACHE).createCache();
thisScopedCacheMap.set(propertyKey, cache);
}
const cacheKey = keyGenerator(...args);
const cachedValue = cache.get(cacheKey);
if (cachedValue !== CACHE_ENTRY_MISSING) {
return cachedValue;
}
const value = originalFunction.apply(this, args);
cache.set(cacheKey, value);
return value;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
return { ...descriptor, value: cachedFunction as any };
};
}
function defaultCacheKeyGenerator(...functionArguments: unknown[]): unknown {
if (functionArguments.length === 0) {
return EMPTY_ARGUMENTS;
}
if (functionArguments.length === 1) {
return functionArguments[0];
}
return functionArguments.join('---');
}
const SINGLE_ENTRY_CACHE = new (class SingleEntryCacheFactory implements CacheFactory {
public createCache(): Cache {
return new SingleEntryCache();
}
})();
class SingleEntryCache implements Cache {
private entry?: { key: unknown; value: unknown };
public get(key: unknown): unknown {
return this.entry === undefined || this.entry.key !== key ? CACHE_ENTRY_MISSING : this.entry.value;
}
public set(key: unknown, value: unknown): void {
this.entry = { key, value };
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment