Created
June 16, 2023 08:18
-
-
Save dscheerens/92f18c53f9f7cac3b80451d153858fa1 to your computer and use it in GitHub Desktop.
TypeScript decorator to apply caching on functions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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