1 | import mimicFn from 'mimic-fn';
|
2 | const cacheStore = new WeakMap();
|
3 | /**
|
4 | [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.
|
5 |
|
6 | @param fn - Function to be memoized.
|
7 |
|
8 | @example
|
9 | ```
|
10 | import {setTimeout as delay} from 'node:timer/promises';
|
11 | import pMemoize from 'p-memoize';
|
12 | import got from 'got';
|
13 |
|
14 | const memoizedGot = pMemoize(got);
|
15 |
|
16 | await memoizedGot('https://sindresorhus.com');
|
17 |
|
18 | // This call is cached
|
19 | await memoizedGot('https://sindresorhus.com');
|
20 |
|
21 | await delay(2000);
|
22 |
|
23 | // This call is not cached as the cache has expired
|
24 | await memoizedGot('https://sindresorhus.com');
|
25 | ```
|
26 | */
|
27 | export default function pMemoize(fn, { cacheKey = ([firstArgument]) => firstArgument, cache = new Map(), } = {}) {
|
28 | // Promise objects can't be serialized so we keep track of them internally and only provide their resolved values to `cache`
|
29 | // `Promise<AsyncReturnType<FunctionToMemoize>>` is used instead of `ReturnType<FunctionToMemoize>` because promise properties are not kept
|
30 | const promiseCache = new Map();
|
31 | const memoized = function (...arguments_) {
|
32 | const key = cacheKey(arguments_);
|
33 | if (promiseCache.has(key)) {
|
34 | return promiseCache.get(key);
|
35 | }
|
36 | const promise = (async () => {
|
37 | try {
|
38 | if (cache && await cache.has(key)) {
|
39 | return (await cache.get(key));
|
40 | }
|
41 | const promise = fn.apply(this, arguments_);
|
42 | const result = await promise;
|
43 | try {
|
44 | return result;
|
45 | }
|
46 | finally {
|
47 | if (cache) {
|
48 | await cache.set(key, result);
|
49 | }
|
50 | }
|
51 | }
|
52 | finally {
|
53 | promiseCache.delete(key);
|
54 | }
|
55 | })();
|
56 | promiseCache.set(key, promise);
|
57 | return promise;
|
58 | };
|
59 | mimicFn(memoized, fn, {
|
60 | ignoreNonConfigurable: true,
|
61 | });
|
62 | cacheStore.set(memoized, cache);
|
63 | return memoized;
|
64 | }
|
65 | /**
|
66 | - Only class methods and getters/setters can be memoized, not regular functions (they aren't part of the proposal);
|
67 | - Only [TypeScript’s decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#parameter-decorators) are supported, not [Babel’s](https://babeljs.io/docs/en/babel-plugin-proposal-decorators), which use a different version of the proposal;
|
68 | - Being an experimental feature, they need to be enabled with `--experimentalDecorators`; follow TypeScript’s docs.
|
69 |
|
70 | @returns A [decorator](https://github.com/tc39/proposal-decorators) to memoize class methods or static class methods.
|
71 |
|
72 | @example
|
73 | ```
|
74 | import {pMemoizeDecorator} from 'p-memoize';
|
75 |
|
76 | class Example {
|
77 | index = 0
|
78 |
|
79 | @pMemoizeDecorator()
|
80 | async counter() {
|
81 | return ++this.index;
|
82 | }
|
83 | }
|
84 |
|
85 | class ExampleWithOptions {
|
86 | index = 0
|
87 |
|
88 | @pMemoizeDecorator()
|
89 | async counter() {
|
90 | return ++this.index;
|
91 | }
|
92 | }
|
93 | ```
|
94 | */
|
95 | export function pMemoizeDecorator(options = {}) {
|
96 | const instanceMap = new WeakMap();
|
97 | return (target, propertyKey, descriptor) => {
|
98 | const input = target[propertyKey]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
99 | if (typeof input !== 'function') {
|
100 | throw new TypeError('The decorated value must be a function');
|
101 | }
|
102 | delete descriptor.value;
|
103 | delete descriptor.writable;
|
104 | descriptor.get = function () {
|
105 | if (!instanceMap.has(this)) {
|
106 | const value = pMemoize(input, options);
|
107 | instanceMap.set(this, value);
|
108 | return value;
|
109 | }
|
110 | return instanceMap.get(this);
|
111 | };
|
112 | };
|
113 | }
|
114 | /**
|
115 | Clear all cached data of a memoized function.
|
116 |
|
117 | @param fn - Memoized function.
|
118 | */
|
119 | export function pMemoizeClear(fn) {
|
120 | if (!cacheStore.has(fn)) {
|
121 | throw new TypeError('Can\'t clear a function that was not memoized!');
|
122 | }
|
123 | const cache = cacheStore.get(fn);
|
124 | if (!cache) {
|
125 | throw new TypeError('Can\'t clear a function that doesn\'t use a cache!');
|
126 | }
|
127 | if (typeof cache.clear !== 'function') {
|
128 | throw new TypeError('The cache Map can\'t be cleared!');
|
129 | }
|
130 | cache.clear();
|
131 | }
|