import { TAnyFunction } from '../typeHelpers' export interface MemoizeOptions { strategy?: MemoizeStrategy serializer?: MemoizeSerializer ttl?: number } type MemoizeStrategy = 'monadic' | 'variadic' type MemoizeSerializer = (data: unknown) => string /** * ### memoize(func, options?) * * Create a function that memoizes the return value of `func`. * * ```js * const func = (a, b) => a + b * const memoizedFunc = flocky.memoize(func) * const memoizedFuncWithTtl = flocky.memoize(func, { ttl: 30 * 1000 }) * memoizedFunc(1, 2) * // -> 3 * ``` * *
* Implementation Details * * This method's implementation is based on [fast-memoize](https://github.com/caiogondim/fast-memoize.js), * with some improvements for variadic performance and additional support for a TTL based cache. *
*/ export function memoize>( this: TThis, func: TFunc, options: MemoizeOptions = {} ): TFunc { const strategy = options.strategy === 'monadic' || (options.strategy !== 'variadic' && func.length <= 1) ? monadic : variadic const cache = options.ttl ? ttlCache(options.ttl) : defaultCache() const serializer = options.serializer ? options.serializer : defaultSerializer return strategy.bind(this, func, cache, serializer) as TFunc } function isPrimitive(value: unknown): value is string { // We can not treat strings as primitive, because they overwrite numbers return value == null || typeof value === 'number' || typeof value === 'boolean' } function monadic>( this: TThis, func: TFunc, cache: MemoizeCache, serializer: MemoizeSerializer, arg: unknown ): TReturn { const cacheKey = isPrimitive(arg) ? arg : serializer(arg) let value = cache.get(cacheKey) if (typeof value === 'undefined') { value = func.call(this, arg) if (value instanceof Promise) { value.catch(() => cache.remove(cacheKey)) } cache.set(cacheKey, value) } return value } function variadic>( this: TThis, func: TFunc, cache: MemoizeCache, serializer: MemoizeSerializer, ...args: Array ): TReturn { const cacheKey = serializer(args) let value = cache.get(cacheKey) if (typeof value === 'undefined') { value = func.apply(this, args) if (value instanceof Promise) { value.catch(() => cache.remove(cacheKey)) } cache.set(cacheKey, value) } return value } function defaultSerializer(data: unknown): string { return JSON.stringify(data) } interface MemoizeCache { get: (key: string) => TReturn | undefined set: (key: string, value: TReturn) => void remove: (key: string) => void } function defaultCache(): MemoizeCache { const cache = Object.create(null) as Record return { get: (key) => cache[key], set: (key, value): void => { cache[key] = value }, remove: (key) => delete cache[key], } } function ttlCache(ttl: number): MemoizeCache { const cache = Object.create(null) as Record return { get: (key) => cache[key], set: (key, value): void => { cache[key] = value // Note: We do not need to clear the timeout because we never set a key // if it still exists in the cache. setTimeout(() => { delete cache[key] }, ttl) }, remove: (key) => delete cache[key], } }