import { assertMethodDecorator, isDecoratorCall, } from "../common/decorators.js"; import type { Method } from "../common/types.js"; import { isWeakMapKey, resolveCallable, } from "../common/utils.js"; export type KeyResolver = (...args: Args) => string; export interface CacheStore { set: (key: string, value: Value) => unknown; get: (key: string) => Value | null | undefined; delete: (key: string) => unknown; has: (key: string) => boolean; } export interface CacheConfig { store?: CacheStore; keyResolver?: KeyResolver | keyof This; ttlMs?: number; } type CacheDecorator = ( value: Method, context: ClassMethodDecoratorContext>, ) => Method; export function resolveCacheKey( instance: This, keyResolver: KeyResolver | keyof This | undefined, args: Args, ): string { if (keyResolver === undefined) { return JSON.stringify(args); } return resolveCallable(instance, keyResolver)(...args); } export function normalizeCacheInput( input?: CacheConfig | number, ): CacheConfig { if (typeof input === "number") { return { ttlMs: input, }; } return input ?? {}; } export function scheduleCacheExpiration(store: CacheStore, key: string, ttlMs: number): void { setTimeout(() => { store.delete(key); }, ttlMs); } export function createCachedMethod( originalMethod: Method, input?: CacheConfig | number, ): Method { const resolvedConfig = normalizeCacheInput(input); const storesByInstance = new WeakMap>(); const fallbackStore: CacheStore = resolvedConfig.store ?? new Map(); const getStore = (instance: This): CacheStore => { if (resolvedConfig.store !== undefined) { return resolvedConfig.store; } if (!isWeakMapKey(instance)) { return fallbackStore; } const instanceKey = instance as object; const existingStore = storesByInstance.get(instanceKey); if (existingStore !== undefined) { return existingStore; } const store = new Map(); storesByInstance.set(instanceKey, store); return store; }; return function(this: This, ...args: Args): Return { const store = getStore(this); const key = resolveCacheKey(this, resolvedConfig.keyResolver, args); if (!store.has(key)) { const response = originalMethod.apply(this, args); store.set(key, response); if (resolvedConfig.ttlMs !== undefined) { scheduleCacheExpiration(store, key, resolvedConfig.ttlMs); } } return store.get(key) as Return; }; } export function cache( value: Method, context: ClassMethodDecoratorContext>, ): Method; export function cache( input?: CacheConfig | number, ): CacheDecorator; export function cache(inputOrValue?: unknown, context?: unknown): unknown { const decorate = ( value: Method, decoratorContext: ClassMethodDecoratorContext>, input?: CacheConfig | number, ): Method => { assertMethodDecorator("cache", value, decoratorContext); return createCachedMethod(value, input as CacheConfig | number); }; if (isDecoratorCall(context)) { return decorate( inputOrValue as Method, context as ClassMethodDecoratorContext>, ); } return ( value: Method, decoratorContext: ClassMethodDecoratorContext>, ): Method => { return decorate(value, decoratorContext, inputOrValue as CacheConfig | number | undefined); }; }