├── .eslintrc.json ├── .travis.yml ├── README.md ├── index.js ├── lib └── memoize.js ├── package.json └── test ├── .eslintrc.json ├── mocha.opts └── spec ├── index.spec.js └── memoize.spec.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base/legacy", 4 | "globals": { 5 | "Map": false, 6 | "WeakMap": false 7 | }, 8 | "rules": { 9 | "vars-on-top": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "stable" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memoize-weak 2 | [![npm version](https://img.shields.io/npm/v/memoize-weak.svg)](https://www.npmjs.com/package/memoize-weak) 3 | ![Stability](https://img.shields.io/badge/stability-stable-brightgreen.svg) 4 | [![Build Status](https://travis-ci.org/timkendrick/memoize-weak.svg?branch=master)](https://travis-ci.org/timkendrick/memoize-weak) 5 | 6 | > Garbage-collected memoizer for variadic functions 7 | 8 | ## Installation 9 | 10 | ```bash 11 | npm install memoize-weak 12 | ``` 13 | 14 | ## Example 15 | 16 | ```js 17 | import memoize from 'memoize-weak'; 18 | 19 | let foo = { foo: true }; 20 | let bar = { bar: true }; 21 | let baz = { baz: true }; 22 | 23 | const fn = memoize((...args) => args); // Create a memoized function 24 | 25 | fn(foo, bar, baz); // Returns [{ foo: true }, { bar: true }, { baz: true }] 26 | fn(foo, bar, baz); // Returns cached result 27 | 28 | foo = bar = baz = undefined; // Original foo, bar and baz are now eligible for garbage collection 29 | ``` 30 | 31 | ## Features 32 | 33 | - Memoizes multiple arguments of any type 34 | - Previous arguments are automatically garbage-collected when no longer referenced elsewhere 35 | - No external dependencies 36 | - Compatible with ES5 and up 37 | 38 | ## How does `memoize-weak` differ from other memoize implementations? 39 | 40 | Memoize functions cache the return value of a function, so that it can be used again without having to recalculate the value. 41 | 42 | They do this by maintaining a cache of arguments that the function has previously been called with, in order to return results that correspond to an earlier set of arguments. 43 | 44 | Usually this argument cache is retained indefinitely, or for a predefined duration after the original function call. This means that any objects passed as arguments are not eligible for garbage collection, even if all other references to these objects have been removed. 45 | 46 | `memoize-weak` uses "weak references" to the argument values, so that once all the references to the arguments have been removed elsewehere in the application, the arguments will become eligible for cleanup (along with any cached return values that correspond to those arguments). 47 | 48 | This allows you to use memoized functions with impunity, without having to worry about potential memory leaks. 49 | 50 | ## Using `memoize-weak` in ES5 applications 51 | 52 | `memoize-weak` requires that `Map` and `WeakMap` are globally available. This means that these will have to be polyfilled for use in an ES5 environment. 53 | 54 | Some examples of `Map` and `WeakMap` polyfills for ES5: 55 | 56 | - [Babel Polyfill](https://babeljs.io/docs/usage/polyfill/) 57 | - [CoreJS](https://github.com/zloirock/core-js) 58 | - [`es6-map`](https://www.npmjs.com/package/es6-map) and [`es6-weak-map`](https://www.npmjs.com/package/es6-weak-map) 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/memoize'); 2 | -------------------------------------------------------------------------------- /lib/memoize.js: -------------------------------------------------------------------------------- 1 | function isPrimitive(value) { 2 | return ((typeof value !== 'object') && (typeof value !== 'function')) || (value === null); 3 | } 4 | 5 | function MapTree() { 6 | this.childBranches = new WeakMap(); 7 | this.primitiveKeys = new Map(); 8 | this.hasValue = false; 9 | this.value = undefined; 10 | } 11 | 12 | MapTree.prototype.has = function has(key) { 13 | var keyObject = (isPrimitive(key) ? this.primitiveKeys.get(key) : key); 14 | return (keyObject ? this.childBranches.has(keyObject) : false); 15 | }; 16 | 17 | MapTree.prototype.get = function get(key) { 18 | var keyObject = (isPrimitive(key) ? this.primitiveKeys.get(key) : key); 19 | return (keyObject ? this.childBranches.get(keyObject) : undefined); 20 | }; 21 | 22 | MapTree.prototype.resolveBranch = function resolveBranch(key) { 23 | if (this.has(key)) { return this.get(key); } 24 | var newBranch = new MapTree(); 25 | var keyObject = this.createKey(key); 26 | this.childBranches.set(keyObject, newBranch); 27 | return newBranch; 28 | }; 29 | 30 | MapTree.prototype.setValue = function setValue(value) { 31 | this.hasValue = true; 32 | return (this.value = value); 33 | }; 34 | 35 | MapTree.prototype.createKey = function createKey(key) { 36 | if (isPrimitive(key)) { 37 | var keyObject = {}; 38 | this.primitiveKeys.set(key, keyObject); 39 | return keyObject; 40 | } 41 | return key; 42 | }; 43 | 44 | MapTree.prototype.clear = function clear() { 45 | if (arguments.length === 0) { 46 | this.childBranches = new WeakMap(); 47 | this.primitiveKeys.clear(); 48 | this.hasValue = false; 49 | this.value = undefined; 50 | } else if (arguments.length === 1) { 51 | var key = arguments[0]; 52 | if (isPrimitive(key)) { 53 | var keyObject = this.primitiveKeys.get(key); 54 | if (keyObject) { 55 | this.childBranches.delete(keyObject); 56 | this.primitiveKeys.delete(key); 57 | } 58 | } else { 59 | this.childBranches.delete(key); 60 | } 61 | } else { 62 | var childKey = arguments[0]; 63 | if (this.has(childKey)) { 64 | var childBranch = this.get(childKey); 65 | childBranch.clear.apply(childBranch, Array.prototype.slice.call(arguments, 1)); 66 | } 67 | } 68 | }; 69 | 70 | module.exports = function memoize(fn) { 71 | var argsTree = new MapTree(); 72 | 73 | function memoized() { 74 | var args = Array.prototype.slice.call(arguments); 75 | var argNode = args.reduce(function getBranch(parentBranch, arg) { 76 | return parentBranch.resolveBranch(arg); 77 | }, argsTree); 78 | if (argNode.hasValue) { return argNode.value; } 79 | var value = fn.apply(null, args); 80 | return argNode.setValue(value); 81 | } 82 | 83 | memoized.clear = argsTree.clear.bind(argsTree); 84 | 85 | return memoized; 86 | }; 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memoize-weak", 3 | "version": "1.0.2", 4 | "description": "Garbage-collected memoizer for variadic functions", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib", 8 | "test": "test" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "lib" 13 | ], 14 | "scripts": { 15 | "test": "eslint index.js test lib && mocha --reporter spec" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/timkendrick/memoize-weak.git" 20 | }, 21 | "keywords": [ 22 | "memoize", 23 | "weak", 24 | "weakmap", 25 | "garbage" 26 | ], 27 | "author": "Tim Kendrick ", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/timkendrick/memoize-weak/issues" 31 | }, 32 | "homepage": "https://github.com/timkendrick/memoize-weak#readme", 33 | "dependencies": {}, 34 | "devDependencies": { 35 | "chai": "^3.5.0", 36 | "eslint": "^3.10.0", 37 | "eslint-config-airbnb-base": "^10.0.0", 38 | "eslint-plugin-import": "^2.2.0", 39 | "mocha": "^3.0.0", 40 | "sinon": "^1.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base", 4 | "env": { 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "import/no-extraneous-dependencies": ["error", { 9 | "devDependencies": true, 10 | "optionalDependencies": false 11 | }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | ./test/**/*.spec.js 2 | -------------------------------------------------------------------------------- /test/spec/index.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | const memoize = require('../..'); 4 | 5 | describe('memoize-weak', () => { 6 | it('Should export a function', () => { 7 | expect(memoize).to.be.a('function'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spec/memoize.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { spy } = require('sinon'); 3 | 4 | const memoize = require('../../lib/memoize'); 5 | 6 | describe('memoize', () => { 7 | it('SHOULD return a function', () => { 8 | const actual = memoize(() => {}); 9 | const expected = Function; 10 | expect(actual).to.be.an.instanceOf(expected); 11 | }); 12 | 13 | describe('GIVEN a memoized function that expects no arguments', () => { 14 | let fn; 15 | let memoized; 16 | beforeEach(() => { 17 | fn = spy(() => 'foo'); 18 | memoized = spy(memoize(fn)); 19 | }); 20 | 21 | describe('AND the memoized function is called', () => { 22 | beforeEach(() => { 23 | memoized(); 24 | }); 25 | 26 | it('SHOULD call the underlying function with the correct arguments', () => { 27 | const actual = fn.firstCall.args; 28 | const expected = []; 29 | expect(actual).to.deep.equal(expected); 30 | }); 31 | 32 | it('SHOULD return the correct result', () => { 33 | const actual = memoized.firstCall.returnValue; 34 | const expected = 'foo'; 35 | expect(actual).to.deep.equal(expected); 36 | }); 37 | 38 | describe('AND the memoized function is called again', () => { 39 | beforeEach(() => { 40 | memoized(); 41 | }); 42 | 43 | it('SHOULD NOT call the underlying function again', () => { 44 | const actual = fn.callCount; 45 | const expected = 1; 46 | expect(actual).to.equal(expected); 47 | }); 48 | 49 | it('SHOULD return the correct result', () => { 50 | const actual = memoized.secondCall.returnValue; 51 | const expected = 'foo'; 52 | expect(actual).to.deep.equal(expected); 53 | }); 54 | }); 55 | 56 | describe('AND the memoized function\'s cache is cleared', () => { 57 | beforeEach(() => { 58 | memoized.clear(); 59 | }); 60 | 61 | describe('AND the memoized function is called again', () => { 62 | beforeEach(() => { 63 | memoized(); 64 | }); 65 | 66 | it('SHOULD call the underlying function again', () => { 67 | const actual = fn.callCount; 68 | const expected = 2; 69 | expect(actual).to.equal(expected); 70 | }); 71 | 72 | it('SHOULD return the correct result', () => { 73 | const actual = memoized.secondCall.returnValue; 74 | const expected = 'foo'; 75 | expect(actual).to.deep.equal(expected); 76 | }); 77 | 78 | describe('AND the memoized function is called again', () => { 79 | beforeEach(() => { 80 | memoized(); 81 | }); 82 | 83 | it('SHOULD NOT call the underlying function again', () => { 84 | const actual = fn.callCount; 85 | const expected = 2; 86 | expect(actual).to.equal(expected); 87 | }); 88 | 89 | it('SHOULD return the correct result', () => { 90 | const actual = memoized.secondCall.returnValue; 91 | const expected = 'foo'; 92 | expect(actual).to.deep.equal(expected); 93 | }); 94 | }); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('GIVEN a memoized function that expects a single primitive argument', () => { 101 | let fn; 102 | let memoized; 103 | beforeEach(() => { 104 | fn = spy((...args) => args); 105 | memoized = spy(memoize(fn)); 106 | }); 107 | 108 | describe('AND the memoized function is called with a single primitive argument', () => { 109 | beforeEach(() => { 110 | memoized('foo'); 111 | }); 112 | 113 | it('SHOULD call the underlying function with the correct arguments', () => { 114 | const actual = fn.firstCall.args; 115 | const expected = ['foo']; 116 | expect(actual).to.deep.equal(expected); 117 | }); 118 | 119 | it('SHOULD return the correct result', () => { 120 | const actual = memoized.firstCall.returnValue; 121 | const expected = ['foo']; 122 | expect(actual).to.deep.equal(expected); 123 | }); 124 | 125 | describe('AND the memoized function is called again with the same argument', () => { 126 | beforeEach(() => { 127 | memoized('foo'); 128 | }); 129 | 130 | it('SHOULD NOT call the underlying function again', () => { 131 | const actual = fn.callCount; 132 | const expected = 1; 133 | expect(actual).to.equal(expected); 134 | }); 135 | 136 | it('SHOULD return the correct result', () => { 137 | const actual = memoized.secondCall.returnValue; 138 | const expected = ['foo']; 139 | expect(actual).to.deep.equal(expected); 140 | }); 141 | 142 | describe('AND the memoized function is called again with a different argument', () => { 143 | beforeEach(() => { 144 | memoized('bar'); 145 | }); 146 | 147 | it('SHOULD call the underlying function with the correct arguments', () => { 148 | const actual = fn.secondCall.args; 149 | const expected = ['bar']; 150 | expect(actual).to.deep.equal(expected); 151 | }); 152 | 153 | it('SHOULD return the correct result', () => { 154 | const actual = memoized.thirdCall.returnValue; 155 | const expected = ['bar']; 156 | expect(actual).to.deep.equal(expected); 157 | }); 158 | }); 159 | }); 160 | 161 | describe('AND the memoized function\'s cache is cleared', () => { 162 | beforeEach(() => { 163 | memoized.clear(); 164 | }); 165 | 166 | describe('AND the memoized function is called again with the same argument', () => { 167 | beforeEach(() => { 168 | memoized('foo'); 169 | }); 170 | 171 | it('SHOULD call the underlying function again', () => { 172 | const actual = fn.callCount; 173 | const expected = 2; 174 | expect(actual).to.equal(expected); 175 | }); 176 | 177 | it('SHOULD return the correct result', () => { 178 | const actual = memoized.secondCall.returnValue; 179 | const expected = ['foo']; 180 | expect(actual).to.deep.equal(expected); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('AND the memoized function\'s cache for that argument is cleared', () => { 186 | beforeEach(() => { 187 | memoized.clear('foo'); 188 | }); 189 | 190 | describe('AND the memoized function is called again with the same argument', () => { 191 | beforeEach(() => { 192 | memoized('foo'); 193 | }); 194 | 195 | it('SHOULD call the underlying function again', () => { 196 | const actual = fn.callCount; 197 | const expected = 2; 198 | expect(actual).to.equal(expected); 199 | }); 200 | 201 | it('SHOULD return the correct result', () => { 202 | const actual = memoized.secondCall.returnValue; 203 | const expected = ['foo']; 204 | expect(actual).to.deep.equal(expected); 205 | }); 206 | }); 207 | }); 208 | 209 | describe('AND the memoized function\'s cache for a different argument is cleared', () => { 210 | beforeEach(() => { 211 | memoized.clear('bar'); 212 | }); 213 | 214 | describe('AND the memoized function is called again with the same argument', () => { 215 | beforeEach(() => { 216 | memoized('foo'); 217 | }); 218 | 219 | it('SHOULD NOT call the underlying function again', () => { 220 | const actual = fn.callCount; 221 | const expected = 1; 222 | expect(actual).to.equal(expected); 223 | }); 224 | 225 | it('SHOULD return the correct result', () => { 226 | const actual = memoized.secondCall.returnValue; 227 | const expected = ['foo']; 228 | expect(actual).to.deep.equal(expected); 229 | }); 230 | }); 231 | }); 232 | }); 233 | }); 234 | 235 | describe('GIVEN a memoized function that expects a single function argument', () => { 236 | let fn; 237 | let memoized; 238 | beforeEach(() => { 239 | fn = spy((...args) => args); 240 | memoized = spy(memoize(fn)); 241 | }); 242 | 243 | describe('AND the memoized function is called with a single function argument', () => { 244 | const foo = () => {}; 245 | beforeEach(() => { 246 | memoized(foo); 247 | }); 248 | 249 | it('SHOULD call the underlying function with the correct arguments', () => { 250 | const actual = fn.firstCall.args; 251 | const expected = [foo]; 252 | expect(actual).to.deep.equal(expected); 253 | }); 254 | 255 | it('SHOULD return the correct result', () => { 256 | const actual = memoized.firstCall.returnValue; 257 | const expected = [foo]; 258 | expect(actual).to.deep.equal(expected); 259 | }); 260 | 261 | describe('AND the memoized function is called again with the same argument', () => { 262 | beforeEach(() => { 263 | memoized(foo); 264 | }); 265 | 266 | it('SHOULD NOT call the underlying function again', () => { 267 | const actual = fn.callCount; 268 | const expected = 1; 269 | expect(actual).to.equal(expected); 270 | }); 271 | 272 | it('SHOULD return the correct result', () => { 273 | const actual = memoized.secondCall.returnValue; 274 | const expected = [foo]; 275 | expect(actual).to.deep.equal(expected); 276 | }); 277 | 278 | describe('AND the memoized function is called again with a different argument', () => { 279 | const bar = () => {}; 280 | beforeEach(() => { 281 | memoized(bar); 282 | }); 283 | 284 | it('SHOULD call the underlying function with the correct arguments', () => { 285 | const actual = fn.secondCall.args; 286 | const expected = [bar]; 287 | expect(actual).to.deep.equal(expected); 288 | }); 289 | 290 | it('SHOULD return the correct result', () => { 291 | const actual = memoized.thirdCall.returnValue; 292 | const expected = [bar]; 293 | expect(actual).to.deep.equal(expected); 294 | }); 295 | }); 296 | }); 297 | 298 | describe('AND the memoized function\'s cache is cleared', () => { 299 | beforeEach(() => { 300 | memoized.clear(); 301 | }); 302 | 303 | describe('AND the memoized function is called again with the same argument', () => { 304 | beforeEach(() => { 305 | memoized(foo); 306 | }); 307 | 308 | it('SHOULD call the underlying function again', () => { 309 | const actual = fn.callCount; 310 | const expected = 2; 311 | expect(actual).to.equal(expected); 312 | }); 313 | 314 | it('SHOULD return the correct result', () => { 315 | const actual = memoized.secondCall.returnValue; 316 | const expected = [foo]; 317 | expect(actual).to.deep.equal(expected); 318 | }); 319 | }); 320 | }); 321 | 322 | describe('AND the memoized function\'s cache for that argument is cleared', () => { 323 | beforeEach(() => { 324 | memoized.clear(foo); 325 | }); 326 | 327 | describe('AND the memoized function is called again with the same argument', () => { 328 | beforeEach(() => { 329 | memoized(foo); 330 | }); 331 | 332 | it('SHOULD call the underlying function again', () => { 333 | const actual = fn.callCount; 334 | const expected = 2; 335 | expect(actual).to.equal(expected); 336 | }); 337 | 338 | it('SHOULD return the correct result', () => { 339 | const actual = memoized.secondCall.returnValue; 340 | const expected = [foo]; 341 | expect(actual).to.deep.equal(expected); 342 | }); 343 | }); 344 | }); 345 | 346 | describe('AND the memoized function\'s cache for a different argument is cleared', () => { 347 | const bar = () => {}; 348 | beforeEach(() => { 349 | memoized.clear(bar); 350 | }); 351 | 352 | describe('AND the memoized function is called again with the same argument', () => { 353 | beforeEach(() => { 354 | memoized(foo); 355 | }); 356 | 357 | it('SHOULD NOT call the underlying function again', () => { 358 | const actual = fn.callCount; 359 | const expected = 1; 360 | expect(actual).to.equal(expected); 361 | }); 362 | 363 | it('SHOULD return the correct result', () => { 364 | const actual = memoized.secondCall.returnValue; 365 | const expected = [foo]; 366 | expect(actual).to.deep.equal(expected); 367 | }); 368 | }); 369 | }); 370 | }); 371 | }); 372 | 373 | describe('GIVEN a memoized function that expects a single object argument', () => { 374 | let fn; 375 | let memoized; 376 | beforeEach(() => { 377 | fn = spy((...args) => args); 378 | memoized = spy(memoize(fn)); 379 | }); 380 | 381 | describe('AND the function is called with a single object argument', () => { 382 | const foo = { foo: true }; 383 | beforeEach(() => { 384 | memoized(foo); 385 | }); 386 | 387 | it('SHOULD call the underlying function with the correct arguments', () => { 388 | const actual = fn.firstCall.args; 389 | const expected = [foo]; 390 | expect(actual).to.deep.equal(expected); 391 | }); 392 | 393 | it('SHOULD return the correct result', () => { 394 | const actual = memoized.firstCall.args; 395 | const expected = [foo]; 396 | expect(actual).to.deep.equal(expected); 397 | }); 398 | 399 | describe('AND the memoized function is called again with the same argument', () => { 400 | beforeEach(() => { 401 | memoized(foo); 402 | }); 403 | 404 | it('SHOULD NOT call the underlying function again', () => { 405 | const actual = fn.callCount; 406 | const expected = 1; 407 | expect(actual).to.equal(expected); 408 | }); 409 | 410 | it('SHOULD return the correct result', () => { 411 | const actual = memoized.secondCall.args; 412 | const expected = [foo]; 413 | expect(actual).to.deep.equal(expected); 414 | }); 415 | 416 | describe('AND the memoized function is called again with a different argument', () => { 417 | const bar = { bar: true }; 418 | beforeEach(() => { 419 | memoized(bar); 420 | }); 421 | 422 | it('SHOULD call the underlying function with the correct arguments', () => { 423 | const actual = fn.secondCall.args; 424 | const expected = [bar]; 425 | expect(actual).to.deep.equal(expected); 426 | }); 427 | 428 | it('SHOULD return the correct result', () => { 429 | const actual = memoized.thirdCall.returnValue; 430 | const expected = [bar]; 431 | expect(actual).to.deep.equal(expected); 432 | }); 433 | }); 434 | }); 435 | 436 | describe('AND the memoized function\'s cache is cleared', () => { 437 | beforeEach(() => { 438 | memoized.clear(); 439 | }); 440 | 441 | describe('AND the memoized function is called again with the same argument', () => { 442 | beforeEach(() => { 443 | memoized(foo); 444 | }); 445 | 446 | it('SHOULD call the underlying function again', () => { 447 | const actual = fn.callCount; 448 | const expected = 2; 449 | expect(actual).to.equal(expected); 450 | }); 451 | 452 | it('SHOULD return the correct result', () => { 453 | const actual = memoized.secondCall.returnValue; 454 | const expected = [foo]; 455 | expect(actual).to.deep.equal(expected); 456 | }); 457 | }); 458 | }); 459 | 460 | describe('AND the memoized function\'s cache for that argument is cleared', () => { 461 | beforeEach(() => { 462 | memoized.clear(foo); 463 | }); 464 | 465 | describe('AND the memoized function is called again with the same argument', () => { 466 | beforeEach(() => { 467 | memoized(foo); 468 | }); 469 | 470 | it('SHOULD call the underlying function again', () => { 471 | const actual = fn.callCount; 472 | const expected = 2; 473 | expect(actual).to.equal(expected); 474 | }); 475 | 476 | it('SHOULD return the correct result', () => { 477 | const actual = memoized.secondCall.returnValue; 478 | const expected = [foo]; 479 | expect(actual).to.deep.equal(expected); 480 | }); 481 | }); 482 | }); 483 | 484 | describe('AND the memoized function\'s cache for a different argument is cleared', () => { 485 | const bar = { bar: true }; 486 | beforeEach(() => { 487 | memoized.clear(bar); 488 | }); 489 | 490 | describe('AND the memoized function is called again with the same argument', () => { 491 | beforeEach(() => { 492 | memoized(foo); 493 | }); 494 | 495 | it('SHOULD NOT call the underlying function again', () => { 496 | const actual = fn.callCount; 497 | const expected = 1; 498 | expect(actual).to.equal(expected); 499 | }); 500 | 501 | it('SHOULD return the correct result', () => { 502 | const actual = memoized.secondCall.returnValue; 503 | const expected = [foo]; 504 | expect(actual).to.deep.equal(expected); 505 | }); 506 | }); 507 | }); 508 | }); 509 | }); 510 | 511 | describe('GIVEN a memoized function that expects multiple primitive arguments', () => { 512 | let fn; 513 | let memoized; 514 | beforeEach(() => { 515 | fn = spy((...args) => args); 516 | memoized = spy(memoize(fn)); 517 | }); 518 | 519 | describe('AND the memoized function is called with multiple primitive arguments', () => { 520 | beforeEach(() => { 521 | memoized('foo', 'bar', 'baz'); 522 | }); 523 | 524 | it('SHOULD call the underlying function with the correct arguments', () => { 525 | const actual = fn.firstCall.args; 526 | const expected = ['foo', 'bar', 'baz']; 527 | expect(actual).to.deep.equal(expected); 528 | }); 529 | 530 | it('SHOULD return the correct result', () => { 531 | const actual = memoized.firstCall.returnValue; 532 | const expected = ['foo', 'bar', 'baz']; 533 | expect(actual).to.deep.equal(expected); 534 | }); 535 | 536 | describe('AND the memoized function is called again with the same arguments', () => { 537 | beforeEach(() => { 538 | memoized('foo', 'bar', 'baz'); 539 | }); 540 | 541 | it('SHOULD NOT call the underlying function again', () => { 542 | const actual = fn.callCount; 543 | const expected = 1; 544 | expect(actual).to.equal(expected); 545 | }); 546 | 547 | it('SHOULD return the correct result', () => { 548 | const actual = memoized.secondCall.returnValue; 549 | const expected = ['foo', 'bar', 'baz']; 550 | expect(actual).to.deep.equal(expected); 551 | }); 552 | 553 | describe('AND the memoized function is called again with different arguments', () => { 554 | beforeEach(() => { 555 | memoized('foo', 'bar', 'qux'); 556 | }); 557 | 558 | it('SHOULD call the underlying function with the correct arguments', () => { 559 | const actual = fn.secondCall.args; 560 | const expected = ['foo', 'bar', 'qux']; 561 | expect(actual).to.deep.equal(expected); 562 | }); 563 | 564 | it('SHOULD return the correct result', () => { 565 | const actual = memoized.thirdCall.returnValue; 566 | const expected = ['foo', 'bar', 'qux']; 567 | expect(actual).to.deep.equal(expected); 568 | }); 569 | }); 570 | }); 571 | 572 | describe('AND the memoized function\'s cache is cleared', () => { 573 | beforeEach(() => { 574 | memoized.clear(); 575 | }); 576 | 577 | describe('AND the memoized function is called again with the same arguments', () => { 578 | beforeEach(() => { 579 | memoized('foo', 'bar', 'baz'); 580 | }); 581 | 582 | it('SHOULD call the underlying function again', () => { 583 | const actual = fn.callCount; 584 | const expected = 2; 585 | expect(actual).to.equal(expected); 586 | }); 587 | 588 | it('SHOULD return the correct result', () => { 589 | const actual = memoized.secondCall.returnValue; 590 | const expected = ['foo', 'bar', 'baz']; 591 | expect(actual).to.deep.equal(expected); 592 | }); 593 | }); 594 | }); 595 | 596 | describe('AND the memoized function\'s cache for that set of arguments is cleared', () => { 597 | beforeEach(() => { 598 | memoized.clear('foo', 'bar', 'baz'); 599 | }); 600 | 601 | describe('AND the memoized function is called again with the same arguments', () => { 602 | beforeEach(() => { 603 | memoized('foo', 'bar', 'baz'); 604 | }); 605 | 606 | it('SHOULD call the underlying function again', () => { 607 | const actual = fn.callCount; 608 | const expected = 2; 609 | expect(actual).to.equal(expected); 610 | }); 611 | 612 | it('SHOULD return the correct result', () => { 613 | const actual = memoized.secondCall.returnValue; 614 | const expected = ['foo', 'bar', 'baz']; 615 | expect(actual).to.deep.equal(expected); 616 | }); 617 | }); 618 | }); 619 | 620 | describe('AND the memoized function\'s cache for a partial set of arguments is cleared', () => { 621 | beforeEach(() => { 622 | memoized.clear('foo', 'bar'); 623 | }); 624 | 625 | describe('AND the memoized function is called again with the same argument', () => { 626 | beforeEach(() => { 627 | memoized('foo', 'bar', 'baz'); 628 | }); 629 | 630 | it('SHOULD call the underlying function again', () => { 631 | const actual = fn.callCount; 632 | const expected = 2; 633 | expect(actual).to.equal(expected); 634 | }); 635 | 636 | it('SHOULD return the correct result', () => { 637 | const actual = memoized.secondCall.returnValue; 638 | const expected = ['foo', 'bar', 'baz']; 639 | expect(actual).to.deep.equal(expected); 640 | }); 641 | }); 642 | }); 643 | 644 | describe('AND the memoized function\'s cache for a different set of arguments is cleared', () => { 645 | beforeEach(() => { 646 | memoized.clear('foo', 'bar', 'qux'); 647 | }); 648 | 649 | describe('AND the memoized function is called again with the same argument', () => { 650 | beforeEach(() => { 651 | memoized('foo', 'bar', 'baz'); 652 | }); 653 | 654 | it('SHOULD NOT call the underlying function again', () => { 655 | const actual = fn.callCount; 656 | const expected = 1; 657 | expect(actual).to.equal(expected); 658 | }); 659 | 660 | it('SHOULD return the correct result', () => { 661 | const actual = memoized.secondCall.returnValue; 662 | const expected = ['foo', 'bar', 'baz']; 663 | expect(actual).to.deep.equal(expected); 664 | }); 665 | }); 666 | }); 667 | }); 668 | }); 669 | 670 | describe('GIVEN a memoized function that expects multiple object arguments', () => { 671 | let fn; 672 | let memoized; 673 | beforeEach(() => { 674 | fn = spy((...args) => args); 675 | memoized = spy(memoize(fn)); 676 | }); 677 | 678 | describe('AND the memoized function is called with multiple object arguments', () => { 679 | const foo = { foo: true }; 680 | const bar = { bar: true }; 681 | const baz = { baz: true }; 682 | beforeEach(() => { 683 | memoized(foo, bar, baz); 684 | }); 685 | 686 | it('SHOULD call the underlying function with the correct arguments', () => { 687 | const actual = fn.firstCall.args; 688 | const expected = [foo, bar, baz]; 689 | expect(actual).to.deep.equal(expected); 690 | }); 691 | 692 | it('SHOULD return the correct result', () => { 693 | const actual = memoized.firstCall.returnValue; 694 | const expected = [foo, bar, baz]; 695 | expect(actual).to.deep.equal(expected); 696 | }); 697 | 698 | describe('AND the memoized function is called again with the same arguments', () => { 699 | beforeEach(() => { 700 | memoized(foo, bar, baz); 701 | }); 702 | 703 | it('SHOULD NOT call the underlying function again', () => { 704 | const actual = fn.callCount; 705 | const expected = 1; 706 | expect(actual).to.equal(expected); 707 | }); 708 | 709 | it('SHOULD return the correct result', () => { 710 | const actual = memoized.secondCall.returnValue; 711 | const expected = [foo, bar, baz]; 712 | expect(actual).to.deep.equal(expected); 713 | }); 714 | 715 | describe('AND the memoized function is called again with different arguments', () => { 716 | const qux = { qux: true }; 717 | beforeEach(() => { 718 | memoized(foo, bar, qux); 719 | }); 720 | 721 | it('SHOULD call the underlying function with the correct arguments', () => { 722 | const actual = fn.secondCall.args; 723 | const expected = [foo, bar, qux]; 724 | expect(actual).to.deep.equal(expected); 725 | }); 726 | 727 | it('SHOULD return the correct result', () => { 728 | const actual = memoized.thirdCall.returnValue; 729 | const expected = [foo, bar, qux]; 730 | expect(actual).to.deep.equal(expected); 731 | }); 732 | }); 733 | }); 734 | 735 | describe('AND the memoized function\'s cache is cleared', () => { 736 | beforeEach(() => { 737 | memoized.clear(); 738 | }); 739 | 740 | describe('AND the memoized function is called again with the same arguments', () => { 741 | beforeEach(() => { 742 | memoized(foo, bar, baz); 743 | }); 744 | 745 | it('SHOULD call the underlying function again', () => { 746 | const actual = fn.callCount; 747 | const expected = 2; 748 | expect(actual).to.equal(expected); 749 | }); 750 | 751 | it('SHOULD return the correct result', () => { 752 | const actual = memoized.secondCall.returnValue; 753 | const expected = [foo, bar, baz]; 754 | expect(actual).to.deep.equal(expected); 755 | }); 756 | }); 757 | }); 758 | 759 | describe('AND the memoized function\'s cache for that set of arguments is cleared', () => { 760 | beforeEach(() => { 761 | memoized.clear(foo, bar, baz); 762 | }); 763 | 764 | describe('AND the memoized function is called again with the same arguments', () => { 765 | beforeEach(() => { 766 | memoized(foo, bar, baz); 767 | }); 768 | 769 | it('SHOULD call the underlying function again', () => { 770 | const actual = fn.callCount; 771 | const expected = 2; 772 | expect(actual).to.equal(expected); 773 | }); 774 | 775 | it('SHOULD return the correct result', () => { 776 | const actual = memoized.secondCall.returnValue; 777 | const expected = [foo, bar, baz]; 778 | expect(actual).to.deep.equal(expected); 779 | }); 780 | }); 781 | }); 782 | 783 | describe('AND the memoized function\'s cache for a partial set of arguments is cleared', () => { 784 | beforeEach(() => { 785 | memoized.clear(foo, bar); 786 | }); 787 | 788 | describe('AND the memoized function is called again with the same argument', () => { 789 | beforeEach(() => { 790 | memoized(foo, bar, baz); 791 | }); 792 | 793 | it('SHOULD call the underlying function again', () => { 794 | const actual = fn.callCount; 795 | const expected = 2; 796 | expect(actual).to.equal(expected); 797 | }); 798 | 799 | it('SHOULD return the correct result', () => { 800 | const actual = memoized.secondCall.returnValue; 801 | const expected = [foo, bar, baz]; 802 | expect(actual).to.deep.equal(expected); 803 | }); 804 | }); 805 | }); 806 | 807 | describe('AND the memoized function\'s cache for a different set of arguments is cleared', () => { 808 | const qux = { qux: true }; 809 | beforeEach(() => { 810 | memoized.clear(foo, bar, qux); 811 | }); 812 | 813 | describe('AND the memoized function is called again with the same argument', () => { 814 | beforeEach(() => { 815 | memoized(foo, bar, baz); 816 | }); 817 | 818 | it('SHOULD NOT call the underlying function again', () => { 819 | const actual = fn.callCount; 820 | const expected = 1; 821 | expect(actual).to.equal(expected); 822 | }); 823 | 824 | it('SHOULD return the correct result', () => { 825 | const actual = memoized.secondCall.returnValue; 826 | const expected = [foo, bar, baz]; 827 | expect(actual).to.deep.equal(expected); 828 | }); 829 | }); 830 | }); 831 | }); 832 | }); 833 | 834 | describe('GIVEN a memoized function that allows null, undefined and false arguments', () => { 835 | let fn; 836 | let memoized; 837 | beforeEach(() => { 838 | fn = spy((...args) => args); 839 | memoized = spy(memoize(fn)); 840 | }); 841 | 842 | 843 | describe('AND the memoized function is called with null, undefined and false arguments', () => { 844 | beforeEach(() => { 845 | memoized(null, undefined, false); 846 | }); 847 | 848 | it('SHOULD call the underlying function with the correct arguments', () => { 849 | const actual = fn.firstCall.args; 850 | const expected = [null, undefined, false]; 851 | expect(actual).to.deep.equal(expected); 852 | }); 853 | 854 | it('SHOULD return the correct result', () => { 855 | const actual = memoized.firstCall.returnValue; 856 | const expected = [null, undefined, false]; 857 | expect(actual).to.deep.equal(expected); 858 | }); 859 | 860 | describe('AND the memoized function is called again with the same arguments', () => { 861 | beforeEach(() => { 862 | memoized(null, undefined, false); 863 | }); 864 | 865 | it('SHOULD NOT call the underlying function again', () => { 866 | const actual = fn.callCount; 867 | const expected = 1; 868 | expect(actual).to.equal(expected); 869 | }); 870 | 871 | it('SHOULD return the correct result', () => { 872 | const actual = memoized.secondCall.returnValue; 873 | const expected = [null, undefined, false]; 874 | expect(actual).to.deep.equal(expected); 875 | }); 876 | }); 877 | 878 | describe('AND the memoized function\'s cache is cleared', () => { 879 | beforeEach(() => { 880 | memoized.clear(); 881 | }); 882 | 883 | describe('AND the memoized function is called again with the same arguments', () => { 884 | beforeEach(() => { 885 | memoized(null, undefined, false); 886 | }); 887 | 888 | it('SHOULD call the underlying function again', () => { 889 | const actual = fn.callCount; 890 | const expected = 2; 891 | expect(actual).to.equal(expected); 892 | }); 893 | 894 | it('SHOULD return the correct result', () => { 895 | const actual = memoized.secondCall.returnValue; 896 | const expected = [null, undefined, false]; 897 | expect(actual).to.deep.equal(expected); 898 | }); 899 | }); 900 | }); 901 | 902 | describe('AND the memoized function\'s cache for that set of arguments is cleared', () => { 903 | beforeEach(() => { 904 | memoized.clear(null, undefined, false); 905 | }); 906 | 907 | describe('AND the memoized function is called again with the same arguments', () => { 908 | beforeEach(() => { 909 | memoized(null, undefined, false); 910 | }); 911 | 912 | it('SHOULD call the underlying function again', () => { 913 | const actual = fn.callCount; 914 | const expected = 2; 915 | expect(actual).to.equal(expected); 916 | }); 917 | 918 | it('SHOULD return the correct result', () => { 919 | const actual = memoized.secondCall.returnValue; 920 | const expected = [null, undefined, false]; 921 | expect(actual).to.deep.equal(expected); 922 | }); 923 | }); 924 | }); 925 | 926 | describe('AND the memoized function\'s cache for a partial set of arguments is cleared', () => { 927 | beforeEach(() => { 928 | memoized.clear(null, undefined); 929 | }); 930 | 931 | describe('AND the memoized function is called again with the same argument', () => { 932 | beforeEach(() => { 933 | memoized(null, undefined, false); 934 | }); 935 | 936 | it('SHOULD call the underlying function again', () => { 937 | const actual = fn.callCount; 938 | const expected = 2; 939 | expect(actual).to.equal(expected); 940 | }); 941 | 942 | it('SHOULD return the correct result', () => { 943 | const actual = memoized.secondCall.returnValue; 944 | const expected = [null, undefined, false]; 945 | expect(actual).to.deep.equal(expected); 946 | }); 947 | }); 948 | }); 949 | 950 | describe('AND the memoized function\'s cache for a different set of arguments is cleared', () => { 951 | beforeEach(() => { 952 | memoized.clear(null, undefined, 0); 953 | }); 954 | 955 | describe('AND the memoized function is called again with the same argument', () => { 956 | beforeEach(() => { 957 | memoized(null, undefined, false); 958 | }); 959 | 960 | it('SHOULD NOT call the underlying function again', () => { 961 | const actual = fn.callCount; 962 | const expected = 1; 963 | expect(actual).to.equal(expected); 964 | }); 965 | 966 | it('SHOULD return the correct result', () => { 967 | const actual = memoized.secondCall.returnValue; 968 | const expected = [null, undefined, false]; 969 | expect(actual).to.deep.equal(expected); 970 | }); 971 | }); 972 | }); 973 | }); 974 | }); 975 | }); 976 | --------------------------------------------------------------------------------