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