1 | # mem
|
2 |
|
3 | > [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
|
4 |
|
5 | Memory is automatically released when an item expires or the cache is cleared.
|
6 |
|
7 |
|
8 |
|
9 | By default, **only the memoized function's first argument is considered** via strict equality comparison. If you need to cache multiple arguments or cache `object`s *by value*, have a look at alternative [caching strategies](#caching-strategy) below.
|
10 |
|
11 | If you want to memoize Promise-returning functions (like `async` functions), you might be better served by [p-memoize](https://github.com/sindresorhus/p-memoize).
|
12 |
|
13 | ## Install
|
14 |
|
15 | ```
|
16 | $ npm install mem
|
17 | ```
|
18 |
|
19 | ## Usage
|
20 |
|
21 | ```js
|
22 | import mem from 'mem';
|
23 |
|
24 | let index = 0;
|
25 | const counter = () => ++index;
|
26 | const memoized = mem(counter);
|
27 |
|
28 | memoized('foo');
|
29 | //=> 1
|
30 |
|
31 | // Cached as it's the same argument
|
32 | memoized('foo');
|
33 | //=> 1
|
34 |
|
35 | // Not cached anymore as the argument changed
|
36 | memoized('bar');
|
37 | //=> 2
|
38 |
|
39 | memoized('bar');
|
40 | //=> 2
|
41 |
|
42 | // Only the first argument is considered by default
|
43 | memoized('bar', 'foo');
|
44 | //=> 2
|
45 | ```
|
46 |
|
47 | ##### Works well with Promise-returning functions
|
48 |
|
49 | But you might want to use [p-memoize](https://github.com/sindresorhus/p-memoize) for more Promise-specific behaviors.
|
50 |
|
51 | ```js
|
52 | import mem from 'mem';
|
53 |
|
54 | let index = 0;
|
55 | const counter = async () => ++index;
|
56 | const memoized = mem(counter);
|
57 |
|
58 | console.log(await memoized());
|
59 | //=> 1
|
60 |
|
61 | // The return value didn't increase as it's cached
|
62 | console.log(await memoized());
|
63 | //=> 1
|
64 | ```
|
65 |
|
66 | ```js
|
67 | import mem from 'mem';
|
68 | import got from 'got';
|
69 | import delay from 'delay';
|
70 |
|
71 | const memGot = mem(got, {maxAge: 1000});
|
72 |
|
73 | await memGot('https://sindresorhus.com');
|
74 |
|
75 | // This call is cached
|
76 | await memGot('https://sindresorhus.com');
|
77 |
|
78 | await delay(2000);
|
79 |
|
80 | // This call is not cached as the cache has expired
|
81 | await memGot('https://sindresorhus.com');
|
82 | ```
|
83 |
|
84 | ### Caching strategy
|
85 |
|
86 | By default, only the first argument is compared via exact equality (`===`) to determine whether a call is identical.
|
87 |
|
88 | ```js
|
89 | const power = mem((a, b) => Math.power(a, b));
|
90 |
|
91 | power(2, 2); // => 4, stored in cache with the key 2 (number)
|
92 | power(2, 3); // => 4, retrieved from cache at key 2 (number), it's wrong
|
93 | ```
|
94 |
|
95 | You will have to use the `cache` and `cacheKey` options appropriate to your function. In this specific case, the following could work:
|
96 |
|
97 | ```js
|
98 | const power = mem((a, b) => Math.power(a, b), {
|
99 | cacheKey: arguments_ => arguments_.join(',')
|
100 | });
|
101 |
|
102 | power(2, 2); // => 4, stored in cache with the key '2,2' (both arguments as one string)
|
103 | power(2, 3); // => 8, stored in cache with the key '2,3'
|
104 | ```
|
105 |
|
106 | More advanced examples follow.
|
107 |
|
108 | #### Example: Options-like argument
|
109 |
|
110 | If your function accepts an object, it won't be memoized out of the box:
|
111 |
|
112 | ```js
|
113 | const heavyMemoizedOperation = mem(heavyOperation);
|
114 |
|
115 | heavyMemoizedOperation({full: true}); // Stored in cache with the object as key
|
116 | heavyMemoizedOperation({full: true}); // Stored in cache with the object as key, again
|
117 | // The objects look the same but for JS they're two different objects
|
118 | ```
|
119 |
|
120 | You might want to serialize or hash them, for example using `JSON.stringify` or something like [serialize-javascript](https://github.com/yahoo/serialize-javascript), which can also serialize `RegExp`, `Date` and so on.
|
121 |
|
122 | ```js
|
123 | const heavyMemoizedOperation = mem(heavyOperation, {cacheKey: JSON.stringify});
|
124 |
|
125 | heavyMemoizedOperation({full: true}); // Stored in cache with the key '[{"full":true}]' (string)
|
126 | heavyMemoizedOperation({full: true}); // Retrieved from cache
|
127 | ```
|
128 |
|
129 | The same solution also works if it accepts multiple serializable objects:
|
130 |
|
131 | ```js
|
132 | const heavyMemoizedOperation = mem(heavyOperation, {cacheKey: JSON.stringify});
|
133 |
|
134 | heavyMemoizedOperation('hello', {full: true}); // Stored in cache with the key '["hello",{"full":true}]' (string)
|
135 | heavyMemoizedOperation('hello', {full: true}); // Retrieved from cache
|
136 | ```
|
137 |
|
138 | #### Example: Multiple non-serializable arguments
|
139 |
|
140 | If your function accepts multiple arguments that aren't supported by `JSON.stringify` (e.g. DOM elements and functions), you can instead extend the initial exact equality (`===`) to work on multiple arguments using [`many-keys-map`](https://github.com/fregante/many-keys-map):
|
141 |
|
142 | ```js
|
143 | import ManyKeysMap from 'many-keys-map';
|
144 |
|
145 | const addListener = (emitter, eventName, listener) => emitter.on(eventName, listener);
|
146 |
|
147 | const addOneListener = mem(addListener, {
|
148 | cacheKey: arguments_ => arguments_, // Use *all* the arguments as key
|
149 | cache: new ManyKeysMap() // Correctly handles all the arguments for exact equality
|
150 | });
|
151 |
|
152 | addOneListener(header, 'click', console.log); // `addListener` is run, and it's cached with the `arguments` array as key
|
153 | addOneListener(header, 'click', console.log); // `addListener` is not run again
|
154 | addOneListener(mainContent, 'load', console.log); // `addListener` is run, and it's cached with the `arguments` array as key
|
155 | ```
|
156 |
|
157 | Better yet, if your function’s arguments are compatible with `WeakMap`, you should use [`deep-weak-map`](https://github.com/futpib/deep-weak-map) instead of `many-keys-map`. This will help avoid memory leaks.
|
158 |
|
159 | ## API
|
160 |
|
161 | ### mem(fn, options?)
|
162 |
|
163 | #### fn
|
164 |
|
165 | Type: `Function`
|
166 |
|
167 | Function to be memoized.
|
168 |
|
169 | #### options
|
170 |
|
171 | Type: `object`
|
172 |
|
173 | ##### maxAge
|
174 |
|
175 | Type: `number`\
|
176 | Default: `Infinity`
|
177 |
|
178 | Milliseconds until the cache expires.
|
179 |
|
180 | ##### cacheKey
|
181 |
|
182 | Type: `Function`\
|
183 | Default: `arguments_ => arguments_[0]`\
|
184 | Example: `arguments_ => JSON.stringify(arguments_)`
|
185 |
|
186 | Determines the cache key for storing the result based on the function arguments. By default, **only the first argument is considered**.
|
187 |
|
188 | A `cacheKey` function can return any type supported by `Map` (or whatever structure you use in the `cache` option).
|
189 |
|
190 | Refer to the [caching strategies](#caching-strategy) section for more information.
|
191 |
|
192 | ##### cache
|
193 |
|
194 | Type: `object`\
|
195 | Default: `new Map()`
|
196 |
|
197 | Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, and optionally `.clear()`. You could for example use a `WeakMap` instead or [`quick-lru`](https://github.com/sindresorhus/quick-lru) for a LRU cache.
|
198 |
|
199 | Refer to the [caching strategies](#caching-strategy) section for more information.
|
200 |
|
201 | ### memDecorator(options)
|
202 |
|
203 | Returns a [decorator](https://github.com/tc39/proposal-decorators) to memoize class methods or static class methods.
|
204 |
|
205 | Notes:
|
206 |
|
207 | - Only class methods and getters/setters can be memoized, not regular functions (they aren't part of the proposal);
|
208 | - 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;
|
209 | - Being an experimental feature, they need to be enabled with `--experimentalDecorators`; follow TypeScript’s docs.
|
210 |
|
211 | #### options
|
212 |
|
213 | Type: `object`
|
214 |
|
215 | Same as options for `mem()`.
|
216 |
|
217 | ```ts
|
218 | import {memDecorator} from 'mem';
|
219 |
|
220 | class Example {
|
221 | index = 0
|
222 |
|
223 | @memDecorator()
|
224 | counter() {
|
225 | return ++this.index;
|
226 | }
|
227 | }
|
228 |
|
229 | class ExampleWithOptions {
|
230 | index = 0
|
231 |
|
232 | @memDecorator({maxAge: 1000})
|
233 | counter() {
|
234 | return ++this.index;
|
235 | }
|
236 | }
|
237 | ```
|
238 |
|
239 | ### memClear(fn)
|
240 |
|
241 | Clear all cached data of a memoized function.
|
242 |
|
243 | #### fn
|
244 |
|
245 | Type: `Function`
|
246 |
|
247 | Memoized function.
|
248 |
|
249 | ## Tips
|
250 |
|
251 | ### Cache statistics
|
252 |
|
253 | If you want to know how many times your cache had a hit or a miss, you can make use of [stats-map](https://github.com/SamVerschueren/stats-map) as a replacement for the default cache.
|
254 |
|
255 | #### Example
|
256 |
|
257 | ```js
|
258 | import mem from 'mem';
|
259 | import StatsMap from 'stats-map';
|
260 | import got from 'got';
|
261 |
|
262 | const cache = new StatsMap();
|
263 | const memGot = mem(got, {cache});
|
264 |
|
265 | await memGot('https://sindresorhus.com');
|
266 | await memGot('https://sindresorhus.com');
|
267 | await memGot('https://sindresorhus.com');
|
268 |
|
269 | console.log(cache.stats);
|
270 | //=> {hits: 2, misses: 1}
|
271 | ```
|
272 |
|
273 | ## Related
|
274 |
|
275 | - [p-memoize](https://github.com/sindresorhus/p-memoize) - Memoize promise-returning & async functions
|
276 |
|
277 | ---
|
278 |
|
279 | <div align="center">
|
280 | <b>
|
281 | <a href="https://tidelift.com/subscription/pkg/npm-mem?utm_source=npm-mem&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
|
282 | </b>
|
283 | <br>
|
284 | <sub>
|
285 | Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
|
286 | </sub>
|
287 | </div>
|