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