/* eslint-disable prefer-rest-params */ /* eslint-disable prefer-spread */ // This memoize function is heavily inspired by reselect's defaultMemoize // https://github.com/reduxjs/reselect type EqualityFn = (a: any, b: any) => boolean; // Cache implementation based on Erik Rasmussen's `lru-memoize`: // https://github.com/erikras/lru-memoize const NOT_FOUND = 'NOT_FOUND'; type NOT_FOUND_TYPE = typeof NOT_FOUND; interface Entry { key: unknown; value: unknown; } interface Cache { get(key: unknown): unknown | NOT_FOUND_TYPE; put(key: unknown, value: unknown): void; getEntries(): Entry[]; clear(): void; } function createSingletonCache(equals: EqualityFn): Cache { let entry: Entry | undefined; return { get(key: unknown) { if (entry && equals(entry.key, key)) { return entry.value; } return NOT_FOUND; }, put(key: unknown, value: unknown) { entry = {key, value}; }, getEntries() { return entry ? [entry] : []; }, clear() { entry = undefined; }, }; } function createLruCache(maxSize: number, equals: EqualityFn): Cache { let entries: Entry[] = []; function get(key: unknown) { const cacheIndex = entries.findIndex((entry) => equals(key, entry.key)); // We found a cached entry if (cacheIndex > -1) { const entry = entries[cacheIndex]; // Cached entry not at top of cache, move it to the top if (cacheIndex > 0) { entries.splice(cacheIndex, 1); entries.unshift(entry); } return entry.value; } // No entry found in cache, return sentinel return NOT_FOUND; } function put(key: unknown, value: unknown) { if (get(key) === NOT_FOUND) { // eslint-disable-next-line no-warning-comments // TODO Is unshift slow? entries.unshift({key, value}); if (entries.length > maxSize) { entries.pop(); } } } function getEntries() { return entries; } function clear() { entries = []; } return {get, put, getEntries, clear}; } export const defaultEqualityCheck: EqualityFn = (first, second): boolean => { return first === second; }; export function createCacheKeyComparator(equalityCheck: EqualityFn) { return function areArgumentsShallowlyEqual( prev: unknown[] | IArguments | null, next: unknown[] | IArguments | null, ): boolean { if (prev === null || next === null || prev.length !== next.length) { return false; } // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible. const length = prev.length; for (let i = 0; i < length; i++) { if (!equalityCheck(prev[i], next[i])) { return false; } } return true; }; } export interface DefaultMemoizeOptions { equalityCheck?: EqualityFn; resultEqualityCheck?: EqualityFn; maxSize?: number; } // defaultMemoize now supports a configurable cache size with LRU behavior, // and optional comparison of the result value with existing values export function memoize any>( func: F, equalityCheckOrOptions?: EqualityFn | DefaultMemoizeOptions, ) { const providedOptions = typeof equalityCheckOrOptions === 'object' ? equalityCheckOrOptions : {equalityCheck: equalityCheckOrOptions}; const { equalityCheck = defaultEqualityCheck, maxSize = 1, resultEqualityCheck, } = providedOptions; const comparator = createCacheKeyComparator(equalityCheck); const cache = maxSize === 1 ? createSingletonCache(comparator) : createLruCache(maxSize, comparator); // we reference arguments instead of spreading them for performance reasons function memoized() { let value = cache.get(arguments); if (value === NOT_FOUND) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore value = func.apply(null, arguments); if (resultEqualityCheck) { const entries = cache.getEntries(); const matchingEntry = entries.find((entry) => resultEqualityCheck(entry.value, value), ); if (matchingEntry) { value = matchingEntry.value; } } cache.put(arguments, value); } return value; } memoized.clearCache = () => cache.clear(); return memoized as F & {clearCache: () => void}; }