[6a3a178] | 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>
|
---|