├── .babelrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package-scripts.js ├── package.json ├── rollup.config.js └── src ├── __tests__ ├── deepEquals.spec.js └── memoize.spec.js ├── deepEquals.js ├── index.d.ts ├── index.js ├── index.js.flow ├── lruCache.js ├── memoize.js └── singletonCache.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "loose": true, 7 | "targets": { 8 | "node": "8" 9 | } 10 | } 11 | ], 12 | "@babel/preset-flow" 13 | ], 14 | "plugins": [ 15 | "@babel/plugin-transform-flow-strip-types", 16 | "@babel/plugin-syntax-dynamic-import", 17 | "@babel/plugin-syntax-import-meta", 18 | "@babel/plugin-proposal-class-properties", 19 | "@babel/plugin-proposal-json-strings", 20 | [ 21 | "@babel/plugin-proposal-decorators", 22 | { 23 | "legacy": true 24 | } 25 | ], 26 | "@babel/plugin-proposal-function-sent", 27 | "@babel/plugin-proposal-export-namespace-from", 28 | "@babel/plugin-proposal-numeric-separator", 29 | "@babel/plugin-proposal-throw-expressions" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | dist/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | esproposal.decorators=ignore 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage 4 | dist 5 | lib 6 | npm-debug.log 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | scripts 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | before_install: 4 | - npm install -g npm@6.7.0 5 | cache: 6 | directories: 7 | - node_modules 8 | notifications: 9 | email: false 10 | node_js: 11 | - '8' 12 | - '9' 13 | - '10' 14 | - '11' 15 | - 'stable' 16 | script: 17 | - npm start validate 18 | after_success: 19 | - npx codecov 20 | branches: 21 | only: 22 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Erik Rasmussen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lru-memoize 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/lru-memoize.svg?style=flat-square)](https://www.npmjs.com/package/lru-memoize) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/lru-memoize.svg?style=flat-square)](https://www.npmjs.com/package/lru-memoize) 5 | [![Build Status](https://img.shields.io/travis/erikras/lru-memoize/master.svg?style=flat-square)](https://travis-ci.org/erikras/lru-memoize) 6 | 7 | `lru-memoize` is a utility to provide simple memoization for any pure javascript function, using an [LRU cache](https://en.wikipedia.org/wiki/Cache_algorithms) that prioritizes the most recently accessed values, and discards the "least recently used" (LRU) items when the size limit is reached. _If your function has side effects or relies on some external state to generate its result, it should not be memoized._ 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install --save lru-memoize 13 | ``` 14 | 15 | ## Usage 16 | 17 | Let's look at an example where we want to memoize a function that multiplies three numbers together, and we want to keep the last ten `arguments -> value` mappings in memory. 18 | 19 | ### ES5 20 | 21 | ```javascript 22 | var memoize = require('lru-memoize'); 23 | 24 | var multiply = function(a, b, c) { 25 | return a * b * c; 26 | } 27 | 28 | multiply = memoize(10)(multiply); 29 | 30 | module.exports = multiply; 31 | ``` 32 | 33 | ### ES6 34 | 35 | ```javascript 36 | import memoize from 'lru-memoize'; 37 | 38 | let multiply = (a, b, c) => a * b * c; 39 | 40 | multiply = memoize(10)(multiply); 41 | 42 | export default multiply; 43 | ``` 44 | 45 | ## API 46 | 47 | #### `memoize(limit:Integer?, equals:Function?, deepObjects:Boolean?)` 48 | 49 | Returns `(Function) => Function`. 50 | 51 | ###### -`limit` : Integer [optional] 52 | 53 | > The number of `arguments -> value` mappings to keep in memory. Defaults to `1`. 54 | 55 | ###### -`equals` : Function [optional] 56 | 57 | > A function to compare two values for equality. Defaults to `===`. 58 | 59 | ###### -`deepObjects` : Boolean [optional] 60 | 61 | > Whether or not to perform a deep equals on Object values. Defaults to `false`. 62 | 63 | -------------------------------------------------------------------------------- /package-scripts.js: -------------------------------------------------------------------------------- 1 | const npsUtils = require("nps-utils"); 2 | 3 | const series = npsUtils.series; 4 | const concurrent = npsUtils.concurrent; 5 | const rimraf = npsUtils.rimraf; 6 | const crossEnv = npsUtils.crossEnv; 7 | 8 | module.exports = { 9 | scripts: { 10 | test: { 11 | default: crossEnv("NODE_ENV=test jest --coverage"), 12 | update: crossEnv("NODE_ENV=test jest --coverage --updateSnapshot"), 13 | watch: crossEnv("NODE_ENV=test jest --watch"), 14 | codeCov: crossEnv( 15 | "cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js" 16 | ), 17 | size: { 18 | description: "check the size of the bundle", 19 | script: "bundlesize" 20 | } 21 | }, 22 | build: { 23 | description: "delete the dist directory and run all builds", 24 | default: series( 25 | rimraf("dist"), 26 | concurrent.nps( 27 | "build.es", 28 | "build.cjs", 29 | "build.umd.main", 30 | "build.umd.min", 31 | "copyTypes" 32 | ) 33 | ), 34 | es: { 35 | description: "run the build with rollup (uses rollup.config.js)", 36 | script: "rollup --config --environment FORMAT:es" 37 | }, 38 | cjs: { 39 | description: "run rollup build with CommonJS format", 40 | script: "rollup --config --environment FORMAT:cjs" 41 | }, 42 | umd: { 43 | min: { 44 | description: "run the rollup build with sourcemaps", 45 | script: "rollup --config --sourcemap --environment MINIFY,FORMAT:umd" 46 | }, 47 | main: { 48 | description: "builds the cjs and umd files", 49 | script: "rollup --config --sourcemap --environment FORMAT:umd" 50 | } 51 | }, 52 | andTest: series.nps("build", "test.size") 53 | }, 54 | docs: { 55 | description: "Generates table of contents in README", 56 | script: "doctoc README.md" 57 | }, 58 | copyTypes: series( 59 | npsUtils.copy("src/*.js.flow src/*.d.ts dist"), 60 | npsUtils.copy( 61 | 'dist/index.js.flow dist --rename="lru-memoize.cjs.js.flow"' 62 | ), 63 | npsUtils.copy('dist/index.js.flow dist --rename="lru-memoize.es.js.flow"') 64 | ), 65 | flow: { 66 | description: "flow check the entire project", 67 | script: "flow check" 68 | }, 69 | validate: { 70 | description: 71 | "This runs several scripts to make sure things look good before committing or on clean install", 72 | default: concurrent.nps("flow", "build.andTest", "test") 73 | } 74 | }, 75 | options: { 76 | silent: false 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lru-memoize", 3 | "version": "1.1.0", 4 | "description": "A utility to provide lru memoization for any js function", 5 | "main": "dist/lru-memoize.cjs.js", 6 | "jsnext:main": "dist/lru-memoize.es.js", 7 | "module": "dist/lru-memoize.es.js", 8 | "typings": "dist/index.d.ts", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/erikras/lru-memoize" 12 | }, 13 | "scripts": { 14 | "start": "nps", 15 | "test": "nps test", 16 | "prepare": "lint-staged && npm start validate" 17 | }, 18 | "keywords": [ 19 | "memoize", 20 | "cache", 21 | "caching", 22 | "es7", 23 | "decorator" 24 | ], 25 | "author": "Erik Rasmussen (http://github.com/erikras)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/erikras/lru-memoize/issues" 29 | }, 30 | "homepage": "https://github.com/erikras/lru-memoize", 31 | "devDependencies": { 32 | "@babel/cli": "^7.0.0", 33 | "@babel/core": "^7.0.0", 34 | "@babel/plugin-proposal-class-properties": "^7.3.3", 35 | "@babel/plugin-proposal-decorators": "^7.3.0", 36 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0", 37 | "@babel/plugin-proposal-function-sent": "^7.2.0", 38 | "@babel/plugin-proposal-json-strings": "^7.2.0", 39 | "@babel/plugin-proposal-numeric-separator": "^7.2.0", 40 | "@babel/plugin-proposal-throw-expressions": "^7.2.0", 41 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 42 | "@babel/plugin-syntax-import-meta": "^7.2.0", 43 | "@babel/plugin-transform-flow-strip-types": "^7.2.3", 44 | "@babel/plugin-transform-runtime": "^7.2.0", 45 | "@babel/preset-env": "^7.0.0", 46 | "@babel/preset-flow": "^7.0.0", 47 | "@babel/register": "^7.0.0", 48 | "babel-jest": "^24.1.0", 49 | "babel-loader": "^8.0.5", 50 | "bundlesize": "^0.17.1", 51 | "flow-bin": "^0.93.0", 52 | "glow": "^1.2.2", 53 | "husky": "^1.3.1", 54 | "jest": "^24.1.0", 55 | "lint-staged": "^8.1.4", 56 | "nps": "^5.9.3", 57 | "nps-utils": "^1.7.0", 58 | "nyc": "^13.2.0", 59 | "prettier": "^1.16.4", 60 | "rifraf": "^2.0.3", 61 | "rimraf": "^2.6.1", 62 | "rollup": "^1.2.2", 63 | "rollup-plugin-babel": "^4.3.2", 64 | "rollup-plugin-commonjs": "^9.2.0", 65 | "rollup-plugin-flow": "^1.1.1", 66 | "rollup-plugin-node-resolve": "^4.0.0", 67 | "rollup-plugin-replace": "^2.1.0", 68 | "rollup-plugin-uglify": "^6.0.2" 69 | }, 70 | "lint-staged": { 71 | "*.{js*,ts*,json,md,css}": [ 72 | "prettier --write", 73 | "git add" 74 | ] 75 | }, 76 | "jest": { 77 | "testEnvironment": "node", 78 | "testPathIgnorePatterns": [ 79 | ".*\\.ts" 80 | ] 81 | }, 82 | "bundlesize": [ 83 | { 84 | "path": "dist/lru-memoize.umd.min.js", 85 | "maxSize": "1kB" 86 | }, 87 | { 88 | "path": "dist/lru-memoize.es.js", 89 | "maxSize": "2kB" 90 | }, 91 | { 92 | "path": "dist/lru-memoize.cjs.js", 93 | "maxSize": "2kB" 94 | } 95 | ], 96 | "husky": { 97 | "hooks": { 98 | "pre-commit": "lint-staged && npm start validate" 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import flow from "rollup-plugin-flow"; 4 | import commonjs from "rollup-plugin-commonjs"; 5 | import { uglify } from "rollup-plugin-uglify"; 6 | import replace from "rollup-plugin-replace"; 7 | import pkg from "./package.json"; 8 | 9 | const makeExternalPredicate = externalArr => { 10 | if (externalArr.length === 0) { 11 | return () => false; 12 | } 13 | const pattern = new RegExp(`^(${externalArr.join("|")})($|/)`); 14 | return id => pattern.test(id); 15 | }; 16 | 17 | const minify = process.env.MINIFY; 18 | const format = process.env.FORMAT; 19 | const es = format === "es"; 20 | const umd = format === "umd"; 21 | const cjs = format === "cjs"; 22 | 23 | let output; 24 | 25 | if (es) { 26 | output = { file: `dist/lru-memoize.es.js`, format: "es" }; 27 | } else if (umd) { 28 | if (minify) { 29 | output = { 30 | file: `dist/lru-memoize.umd.min.js`, 31 | format: "umd" 32 | }; 33 | } else { 34 | output = { file: `dist/lru-memoize.umd.js`, format: "umd" }; 35 | } 36 | } else if (cjs) { 37 | output = { file: `dist/lru-memoize.cjs.js`, format: "cjs" }; 38 | } else if (format) { 39 | throw new Error(`invalid format specified: "${format}".`); 40 | } else { 41 | throw new Error("no format specified. --environment FORMAT:xxx"); 42 | } 43 | 44 | export default { 45 | input: "src/index.js", 46 | output: Object.assign( 47 | { 48 | name: "lru-memoize", 49 | exports: "named" 50 | }, 51 | output 52 | ), 53 | external: makeExternalPredicate( 54 | umd 55 | ? Object.keys(pkg.peerDependencies || {}) 56 | : [ 57 | ...Object.keys(pkg.dependencies || {}), 58 | ...Object.keys(pkg.peerDependencies || {}) 59 | ] 60 | ), 61 | plugins: [ 62 | resolve({ jsnext: true, main: true }), 63 | flow(), 64 | commonjs({ include: "node_modules/**" }), 65 | babel({ 66 | exclude: "node_modules/**", 67 | babelrc: false, 68 | runtimeHelpers: true, 69 | presets: [ 70 | [ 71 | "@babel/preset-env", 72 | { 73 | modules: false, 74 | loose: true 75 | } 76 | ], 77 | "@babel/preset-flow" 78 | ], 79 | plugins: [ 80 | ["@babel/plugin-transform-runtime", { useESModules: !cjs }], 81 | "@babel/plugin-transform-flow-strip-types", 82 | "@babel/plugin-syntax-dynamic-import", 83 | "@babel/plugin-syntax-import-meta", 84 | "@babel/plugin-proposal-class-properties", 85 | "@babel/plugin-proposal-json-strings", 86 | [ 87 | "@babel/plugin-proposal-decorators", 88 | { 89 | legacy: true 90 | } 91 | ], 92 | "@babel/plugin-proposal-function-sent", 93 | "@babel/plugin-proposal-export-namespace-from", 94 | "@babel/plugin-proposal-numeric-separator", 95 | "@babel/plugin-proposal-throw-expressions" 96 | ] 97 | }), 98 | umd || es 99 | ? replace({ 100 | "process.env.NODE_ENV": JSON.stringify( 101 | minify ? "production" : "development" 102 | ) 103 | }) 104 | : null, 105 | minify ? uglify() : null 106 | ].filter(Boolean) 107 | }; 108 | -------------------------------------------------------------------------------- /src/__tests__/deepEquals.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import deepEquals from '../deepEquals' 3 | 4 | const tripleEquals = deepEquals((valueA, valueB) => valueA === valueB, true) 5 | 6 | describe('deepEquals', () => { 7 | it('should return true if argument fields are equal', () => { 8 | expect(tripleEquals(3, 3)).toBe(true) 9 | 10 | expect(tripleEquals('dog', 'dog')).toBe(true) 11 | 12 | expect( 13 | tripleEquals({ a: 1, b: 2, c: undefined }, { a: 1, b: 2, c: undefined }) 14 | ).toBe(true) 15 | 16 | expect(tripleEquals({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(true) 17 | 18 | const obj = {} 19 | expect(tripleEquals({ a: 1, b: 2, c: obj }, { a: 1, b: 2, c: obj })).toBe( 20 | true 21 | ) 22 | 23 | expect(tripleEquals(null, null)).toBe(true) 24 | }) 25 | 26 | it('should return false if arguments are number and string', () => { 27 | expect(tripleEquals(2, '2')).toBe(false) 28 | }) 29 | 30 | it('should return false if arguments are string and number', () => { 31 | expect(tripleEquals('2', 2)).toBe(false) 32 | }) 33 | 34 | it('should return false if arguments are number and object', () => { 35 | expect(tripleEquals(4, {})).toBe(false) 36 | }) 37 | 38 | it('should return false if arguments are object and number', () => { 39 | expect(tripleEquals({}, 4)).toBe(false) 40 | }) 41 | 42 | it('should return false if arguments are number and array', () => { 43 | expect(tripleEquals(4, [])).toBe(false) 44 | }) 45 | 46 | it('should return false if arguments are array and number', () => { 47 | expect(tripleEquals([], 4)).toBe(false) 48 | }) 49 | 50 | it('should return false if arguments are string and object', () => { 51 | expect(tripleEquals('cat', {})).toBe(false) 52 | }) 53 | 54 | it('should return false if arguments are object and string', () => { 55 | expect(tripleEquals({}, 'cat')).toBe(false) 56 | }) 57 | 58 | it('should return false if arguments are string and array', () => { 59 | expect(tripleEquals('cat', ['c', 'a', 't'])).toBe(false) 60 | }) 61 | 62 | it('should return false if arguments are array and string', () => { 63 | expect(tripleEquals(['c', 'a', 't'], 'cat')).toBe(false) 64 | }) 65 | 66 | it('should return false if arguments are array and object', () => { 67 | expect(tripleEquals([], {})).toBe(false) 68 | }) 69 | 70 | it('should return false if arguments are object and array', () => { 71 | expect(tripleEquals({}, [])).toBe(false) 72 | }) 73 | 74 | it('should return false if arguments are object and null', () => { 75 | expect(tripleEquals({ a: 1 }, null)).toBe(false) 76 | }) 77 | 78 | it('should return false if arguments are null and object', () => { 79 | expect(tripleEquals(null, { a: 1 })).toBe(false) 80 | }) 81 | 82 | it('should return false if first argument has too many keys', () => { 83 | expect(tripleEquals({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false) 84 | }) 85 | 86 | it('should return false if second argument has too many keys', () => { 87 | expect(tripleEquals({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(false) 88 | }) 89 | 90 | it('should return false if arguments have different keys', () => { 91 | expect( 92 | tripleEquals({ a: 1, b: 2, c: undefined }, { a: 1, bb: 2, c: undefined }) 93 | ).toBe(false) 94 | }) 95 | 96 | it('should return false if first array argument has too many items', () => { 97 | expect(tripleEquals([1, 2, 3, 4], [1, 2, 3])).toBe(false) 98 | }) 99 | 100 | it('should return false if second array argument has too many items', () => { 101 | expect(tripleEquals([1, 2, 3], [1, 2, 3, 4])).toBe(false) 102 | }) 103 | 104 | it('should work with objects inside arrays', () => { 105 | expect(tripleEquals( 106 | [ { val: 4 }, { val: 2 }, { val: 3 } ], 107 | [ { val: 1 }, { val: 2 }, { val: 3 } ] 108 | )).toBe(false) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/__tests__/memoize.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import memoize from '../memoize' 3 | 4 | describe('memoize', () => { 5 | describe('basic', () => { 6 | it('single', () => { 7 | let callCount = 0 8 | let multiply = (x, y, z) => { 9 | callCount += 1 10 | return x * y * z 11 | } 12 | multiply = memoize()(multiply) 13 | 14 | expect(multiply(1, 2, 3)).toBe(6) 15 | 16 | expect(multiply(1, 2, 3)).toBe(6) 17 | 18 | expect(callCount).toBe(1) 19 | 20 | expect(multiply(4, 5, 6)).toBe(120) 21 | 22 | expect(callCount).toBe(2) 23 | 24 | expect(multiply(1, 2, 3)).toBe(6) 25 | 26 | expect(callCount).toBe(3) 27 | }) 28 | 29 | it('multiple', () => { 30 | let callCount = 0 31 | let multiply = (x, y, z) => { 32 | callCount += 1 33 | return x * y * z 34 | } 35 | multiply = memoize(2)(multiply) 36 | 37 | expect(multiply(1, 2, 3)).toBe(6) 38 | 39 | expect(multiply(1, 2, 3)).toBe(6) 40 | 41 | expect(callCount).toBe(1) 42 | 43 | expect(multiply(4, 5, 6)).toBe(120) 44 | 45 | expect(callCount).toBe(2) 46 | 47 | expect(multiply(1, 2, 3)).toBe(6) 48 | 49 | expect(callCount).toBe(2) 50 | 51 | expect(multiply(4, 5, 6)).toBe(120) 52 | 53 | expect(callCount).toBe(2) 54 | }) 55 | }) 56 | 57 | describe('shallow', () => { 58 | it('array', () => { 59 | let callCount = 0 60 | let multiply = (x, y, z) => { 61 | callCount += 1 62 | return x.concat(y, z).reduce((t, n) => t * n) 63 | } 64 | multiply = memoize()(multiply) 65 | 66 | const x = [1, 2, 3] 67 | const y = [4, 5, 6] 68 | const z = [7, 8, 9] 69 | 70 | const x2 = [1, 2, 3] 71 | 72 | expect(multiply(x, y, z)).toBe(362880) 73 | 74 | expect(multiply(x2, y, z)).toBe(362880) 75 | 76 | expect(callCount).toBe(2) 77 | }) 78 | 79 | it('object', () => { 80 | let callCount = 0 81 | let multiply = (x, y, z) => { 82 | callCount += 1 83 | return x.val * y.val * z.val 84 | } 85 | multiply = memoize(2)(multiply) 86 | 87 | const x = { val: 1 } 88 | const y = { val: 2 } 89 | const z = { val: 3 } 90 | 91 | const x2 = { val: 1 } 92 | 93 | expect(multiply(x, y, z)).toBe(6) 94 | 95 | expect(multiply(x2, y, z)).toBe(6) 96 | 97 | expect(callCount).toBe(2) 98 | }) 99 | }) 100 | 101 | describe('deep', () => { 102 | it('array', () => { 103 | let callCount = 0 104 | let multiply = (x, y, z) => { 105 | callCount += 1 106 | return x.concat(y, z).reduce((t, n) => t * n) 107 | } 108 | multiply = memoize(true)(multiply) 109 | 110 | const x = [1, 2, 3] 111 | const y = [4, 5, 6] 112 | const z = [7, 8, 9] 113 | 114 | const x2 = [1, 2, 3] 115 | const x3 = [3, 2, 1] 116 | 117 | expect(multiply(x, y, z)).toBe(362880) 118 | 119 | expect(multiply(x2, y, z)).toBe(362880) 120 | 121 | expect(callCount).toBe(1) 122 | 123 | expect(multiply(x3, y, z)).toBe(362880) 124 | 125 | expect(callCount).toBe(2) 126 | }) 127 | 128 | it('object', () => { 129 | let callCount = 0 130 | let multiply = (x, y, z) => { 131 | callCount += 1 132 | return x.val * y.val * z.val 133 | } 134 | multiply = memoize(true)(multiply) 135 | 136 | const x = { val: 1 } 137 | const y = { val: 2 } 138 | const z = { val: 3 } 139 | 140 | const x2 = { val: 1 } 141 | const x3 = { val: 4 } 142 | 143 | expect(multiply(x, y, z)).toBe(6) 144 | 145 | expect(multiply(x2, y, z)).toBe(6) 146 | 147 | expect(callCount).toBe(1) 148 | 149 | expect(multiply(x3, y, z)).toBe(24) 150 | 151 | expect(callCount).toBe(2) 152 | }) 153 | 154 | it('object nested', () => { 155 | let callCount = 0 156 | let multiply = (x, y, z) => { 157 | callCount += 1 158 | return x.inner.val * y.inner.val * z.inner.val 159 | } 160 | multiply = memoize(true)(multiply) 161 | 162 | const x = { inner: { val: 1 } } 163 | const y = { inner: { val: 2 } } 164 | const z = { inner: { val: 3 } } 165 | 166 | const x2 = { inner: { val: 1 } } 167 | const x3 = { inner: { val: 4 } } 168 | 169 | expect(multiply(x, y, z)).toBe(6) 170 | 171 | expect(multiply(x2, y, z)).toBe(6) 172 | 173 | expect(callCount).toBe(1) 174 | 175 | expect(multiply(x3, y, z)).toBe(24) 176 | 177 | expect(callCount).toBe(2) 178 | }) 179 | }) 180 | }) 181 | -------------------------------------------------------------------------------- /src/deepEquals.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Equals } from "."; 3 | const hasOwn = (object, key) => 4 | Object.prototype.hasOwnProperty.call(object, key); 5 | export default function deepEquals(equals: Equals, deepObjects: boolean) { 6 | function deep(valueA: any, valueB: any) { 7 | if (equals(valueA, valueB)) { 8 | return true; 9 | } 10 | 11 | if (Array.isArray(valueA)) { 12 | if (!Array.isArray(valueB) || valueA.length !== valueB.length) { 13 | return false; 14 | } 15 | 16 | // Check deep equality of each value in A against the same indexed value in B 17 | if (!valueA.every((value, index) => deep(value, valueB[index]))) { 18 | return false; 19 | } 20 | 21 | // could not find unequal items 22 | return true; 23 | } 24 | 25 | if (Array.isArray(valueB)) { 26 | return false; 27 | } 28 | 29 | if (typeof valueA === "object") { 30 | if (typeof valueB !== "object") { 31 | return false; 32 | } 33 | 34 | const isANull = valueA === null; 35 | const isBNull = valueB === null; 36 | if (isANull || isBNull) { 37 | return isANull === isBNull; 38 | } 39 | 40 | const aKeys = Object.keys(valueA); 41 | const bKeys = Object.keys(valueB); 42 | 43 | if (aKeys.length !== bKeys.length) { 44 | return false; 45 | } 46 | 47 | // Should we compare with shallow equivalence or deep equivalence? 48 | const equalityChecker = deepObjects ? deep : equals; 49 | 50 | // Check if objects share same keys, and each of those keys are equal 51 | if ( 52 | !aKeys.every( 53 | aKey => 54 | hasOwn(valueA, aKey) && 55 | hasOwn(valueB, aKey) && 56 | equalityChecker(valueA[aKey], valueB[aKey]) 57 | ) 58 | ) { 59 | return false; 60 | } 61 | 62 | // could not find unequal keys or values 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | return deep; 69 | } 70 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare const memoize = ( 2 | limit?: number, 3 | equals?: (a: any, b: any) => boolean, 4 | deepObjects?: boolean 5 | ) => (func: T) => T; 6 | export default memoize; 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import memoize from './memoize' 2 | 3 | export default memoize 4 | -------------------------------------------------------------------------------- /src/index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | export type Entry = { 3 | key: any, 4 | value: any 5 | }; 6 | type ConfigItem = number | Function | boolean | void; 7 | export type Config = ConfigItem[]; 8 | export type Equals = (any, any) => boolean; 9 | 10 | declare export default function memoize(Config): Function => Function; 11 | -------------------------------------------------------------------------------- /src/lruCache.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Entry, Equals } from "."; 3 | 4 | const findIndex = (arr: any[], fn: any => any) => { 5 | for (let i = 0; i < arr.length; i++) { 6 | if (fn(arr[i])) { 7 | return i; 8 | } 9 | } 10 | 11 | return -1; 12 | }; 13 | 14 | export default function lruCache(limit: number, equals: Equals) { 15 | const entries: Entry[] = []; 16 | 17 | function get(key: any) { 18 | const cacheIndex = findIndex(entries, entry => equals(key, entry.key)); 19 | 20 | // We found a cached entry 21 | if (cacheIndex > -1) { 22 | const entry = entries[cacheIndex]; 23 | 24 | // Cached entry not at top of cache, move it to the top 25 | if (cacheIndex > 0) { 26 | entries.splice(cacheIndex, 1); 27 | entries.unshift(entry); 28 | } 29 | 30 | return entry.value; 31 | } 32 | 33 | // No entry found in cache, return null 34 | return undefined; 35 | } 36 | 37 | function put(key: any, value: any) { 38 | if (!get(key)) { 39 | entries.unshift({ key, value }); 40 | if (entries.length > limit) { 41 | entries.pop(); 42 | } 43 | } 44 | } 45 | 46 | return { get, put }; 47 | } 48 | -------------------------------------------------------------------------------- /src/memoize.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Config, Equals } from "."; 3 | import deepEquals from "./deepEquals"; 4 | import lruCache from "./lruCache"; 5 | import singletonCache from "./singletonCache"; 6 | 7 | function createCache(limit: number, equals: Equals) { 8 | return limit === 1 ? singletonCache(equals) : lruCache(limit, equals); 9 | } 10 | 11 | function createEqualsFn(basicEquals: Equals, deepObjects: boolean) { 12 | // Choose strategy for basic or deep object equals 13 | const equals = deepObjects 14 | ? deepEquals(basicEquals, deepObjects) 15 | : basicEquals; 16 | 17 | return (valueA, valueB) => { 18 | // The arguments are always the argument array-like objects 19 | 20 | // Different lengths means they are not the same 21 | if (valueA.length !== valueB.length) { 22 | return false; 23 | } 24 | 25 | // Compare the values 26 | for (let index = 0; index < valueA.length; index += 1) { 27 | if (!equals(valueA[index], valueB[index])) { 28 | return false; 29 | } 30 | } 31 | // Found no conflicts 32 | return true; 33 | }; 34 | } 35 | 36 | export default function memoize(...config: Config): Function => Function { 37 | let limit: number = 1; 38 | let equals: Equals = (valueA, valueB) => valueA === valueB; 39 | let deepObjects: boolean = false; 40 | 41 | if (typeof config[0] === "number") { 42 | limit = ((config.shift(): any): number); 43 | } 44 | if (typeof config[0] === "function") { 45 | equals = ((config.shift(): any): Function); 46 | } else if (typeof config[0] === "undefined") { 47 | // Support passing undefined equal argument; 48 | config.shift(); 49 | } 50 | if (typeof config[0] === "boolean") { 51 | deepObjects = config[0]; 52 | } 53 | 54 | const cache = createCache(limit, createEqualsFn(equals, deepObjects)); 55 | 56 | return fn => (...args: any[]) => { 57 | let value = cache.get(args); 58 | if (value === undefined) { 59 | value = fn.apply(fn, args); 60 | cache.put(args, value); 61 | } 62 | return value; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/singletonCache.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Entry, Equals } from "."; 3 | 4 | export default function singletonCache(equals: Equals) { 5 | let entry: Entry; 6 | return { 7 | get(key: any) { 8 | if (entry && equals(key, entry.key)) { 9 | return entry.value; 10 | } 11 | }, 12 | 13 | put(key: any, value: any) { 14 | entry = { key, value }; 15 | } 16 | }; 17 | } 18 | --------------------------------------------------------------------------------