import mimicFunction from 'mimic-function'; const cacheStore = new WeakMap(); const cacheTimerStore = new WeakMap(); /** [Memoize](https://en.wikipedia.org/wiki/Memoization) functions - An optimization used to speed up consecutive function calls by caching the result of calls with identical input. @param fn - The function to be memoized. @example ``` import memoize from 'memoize'; let index = 0; const counter = () => ++index; const memoized = memoize(counter); memoized('foo'); //=> 1 // Cached as it's the same argument memoized('foo'); //=> 1 // Not cached anymore as the arguments changed memoized('bar'); //=> 2 memoized('bar'); //=> 2 ``` */ export default function memoize(fn, { cacheKey, cache = new Map(), maxAge, } = {}) { if (maxAge === 0) { return fn; } if (typeof maxAge === 'number') { const maxSetIntervalValue = 2147483647; if (maxAge > maxSetIntervalValue) { throw new TypeError(`The \`maxAge\` option cannot exceed ${maxSetIntervalValue}.`); } if (maxAge < 0) { throw new TypeError('The `maxAge` option should not be a negative number.'); } } const memoized = function (...arguments_) { const key = cacheKey ? cacheKey(arguments_) : arguments_[0]; const cacheItem = cache.get(key); if (cacheItem) { return cacheItem.data; } const result = fn.apply(this, arguments_); cache.set(key, { data: result, maxAge: maxAge ? Date.now() + maxAge : Number.POSITIVE_INFINITY, }); if (typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY) { const timer = setTimeout(() => { cache.delete(key); }, maxAge); timer.unref?.(); const timers = cacheTimerStore.get(fn) ?? new Set(); timers.add(timer); cacheTimerStore.set(fn, timers); } return result; }; mimicFunction(memoized, fn, { ignoreNonConfigurable: true, }); cacheStore.set(memoized, cache); return memoized; } /** @returns A [decorator](https://github.com/tc39/proposal-decorators) to memoize class methods or static class methods. @example ``` import {memoizeDecorator} from 'memoize'; class Example { index = 0 @memoizeDecorator() counter() { return ++this.index; } } class ExampleWithOptions { index = 0 @memoizeDecorator({maxAge: 1000}) counter() { return ++this.index; } } ``` */ export function memoizeDecorator(options = {}) { const instanceMap = new WeakMap(); return (target, propertyKey, descriptor) => { const input = target[propertyKey]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment if (typeof input !== 'function') { throw new TypeError('The decorated value must be a function'); } delete descriptor.value; delete descriptor.writable; descriptor.get = function () { if (!instanceMap.has(this)) { const value = memoize(input, options); instanceMap.set(this, value); return value; } return instanceMap.get(this); }; }; } /** Clear all cached data of a memoized function. @param fn - The memoized function. */ export function memoizeClear(fn) { const cache = cacheStore.get(fn); if (!cache) { throw new TypeError('Can\'t clear a function that was not memoized!'); } if (typeof cache.clear !== 'function') { throw new TypeError('The cache Map can\'t be cleared!'); } cache.clear(); for (const timer of cacheTimerStore.get(fn) ?? []) { clearTimeout(timer); } }