├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── basic.js /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | test/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memo-async-lru [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [travis-image]: https://img.shields.io/travis/feross/memo-async-lru/master.svg 4 | [travis-url]: https://travis-ci.org/feross/memo-async-lru 5 | [npm-image]: https://img.shields.io/npm/v/memo-async-lru.svg 6 | [npm-url]: https://npmjs.org/package/memo-async-lru 7 | [downloads-image]: https://img.shields.io/npm/dm/memo-async-lru.svg 8 | [downloads-url]: https://npmjs.org/package/memo-async-lru 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | ### Memoize Node.js style callback-last functions, using an in-memory LRU store 13 | 14 | Also works in the browser with [browserify](http://browserify.org/)! 15 | 16 | ## install 17 | 18 | ``` 19 | npm install memo-async-lru 20 | ``` 21 | 22 | ## usage 23 | 24 | ```js 25 | const memo = require('memo-async-lru') 26 | 27 | function fn (arg, cb) { 28 | t.equal(arg, 'foo') 29 | cb(null, 'bar') 30 | } 31 | 32 | const memoFn = memo(fn) 33 | 34 | memoFn('foo', (err, result) => { 35 | console.log(result) // prints 'bar' 36 | 37 | memoFn('foo', (err, result) => { 38 | console.log(result) // prints 'bar', cached, does not call fn() 39 | }) 40 | }) 41 | ``` 42 | 43 | ## API 44 | 45 | ### `memo(fn, [opts])` 46 | 47 | Memoize the given function `fn`, using 48 | [`async-lru`](https://www.npmjs.com/package/async-lru), a simple async LRU cache supporting 49 | O(1) set, get and eviction of old keys. 50 | 51 | The function must be a Node.js style function, where the last argument is a callback. 52 | 53 | function(key: Object, [...], fetch: function(err: Error, value: Object)) 54 | 55 | So, if you were to do: 56 | 57 | ```js 58 | const readFile = memo(fs.readFile) 59 | readFile('file.txt', fn) 60 | readFile('file.txt', fn) // <-- this uses the cache 61 | ``` 62 | 63 | The file would only be read from disk once, it's value cached, and returned 64 | anytime the first argument is 'file.txt'. 65 | 66 | Repeated calls to the function with the same first argument will return a 67 | cached value, rather than re-fetch the data. 68 | 69 | Optionally, an `opts` parameter can be specified with the following properties: 70 | Optional options: 71 | 72 | ```js 73 | { 74 | max: maxElementsToStore, 75 | maxAge: maxAgeInMilliseconds 76 | } 77 | ``` 78 | 79 | If you pass `max`, items will be evicted if the cache is storing more than `max` items. 80 | If you pass `maxAge`, items will be evicted if they are older than `maxAge` when you access them. 81 | 82 | ## license 83 | 84 | MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! memo-async-lru. MIT License. Feross Aboukhadijeh */ 2 | 'use strict' 3 | 4 | module.exports = memoAsyncLRU 5 | 6 | const AsyncLRU = require('async-lru') 7 | 8 | function memoAsyncLRU (load, opts) { 9 | opts = Object.assign({ 10 | max: 100, 11 | maxAge: 60 * 60 * 1000 // 1 hour 12 | }, opts) 13 | 14 | const cache = new AsyncLRU({ 15 | max: opts.max, 16 | maxAge: opts.maxAge, 17 | load: load 18 | }) 19 | 20 | return loadWithCache 21 | 22 | function loadWithCache (...args) { 23 | if (args.length < 2) { 24 | throw new Error('Must be called with at least 2 arguments') 25 | } else if (args.length === 2) { 26 | const key = args[0] 27 | const cb = args[1] 28 | if (typeof key === 'string') { 29 | cache.get(key, cb) 30 | } else { 31 | cache.get(JSON.stringify(key), key, cb) 32 | } 33 | } else { 34 | const loadArgs = args.slice(0, args.length - 1) 35 | const cb = args[args.length - 1] 36 | const key = loadArgs.map(arg => { 37 | return (typeof arg === 'string') ? arg : JSON.stringify(arg) 38 | }).join('~') 39 | cache.get(key, loadArgs, cb) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memo-async-lru", 3 | "description": "Memoize Node.js style callback-last functions, using an in-memory LRU store", 4 | "version": "1.0.3", 5 | "author": { 6 | "name": "Feross Aboukhadijeh", 7 | "email": "feross@feross.org", 8 | "url": "https://feross.org" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/feross/memo-async-lru/issues" 12 | }, 13 | "dependencies": { 14 | "async-lru": "^1.1.1" 15 | }, 16 | "devDependencies": { 17 | "standard": "*", 18 | "tape": "^5.0.0" 19 | }, 20 | "homepage": "https://github.com/feross/memo-async-lru", 21 | "keywords": [ 22 | "memoize", 23 | "memo", 24 | "node.js", 25 | "callback", 26 | "lru", 27 | "lru cache", 28 | "async lru" 29 | ], 30 | "license": "MIT", 31 | "main": "index.js", 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/feross/memo-async-lru.git" 35 | }, 36 | "scripts": { 37 | "test": "standard && tape test/*.js" 38 | }, 39 | "funding": [ 40 | { 41 | "type": "github", 42 | "url": "https://github.com/sponsors/feross" 43 | }, 44 | { 45 | "type": "patreon", 46 | "url": "https://www.patreon.com/feross" 47 | }, 48 | { 49 | "type": "consulting", 50 | "url": "https://feross.org/support" 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const memo = require('../') 3 | 4 | test('one argument function', (t) => { 5 | t.plan(3) 6 | 7 | function fn (arg, cb) { 8 | t.equal(arg, 'foo') 9 | cb(null, 'bar') 10 | } 11 | 12 | const memoFn = memo(fn) 13 | 14 | memoFn('foo', (err, result) => { 15 | t.error(err) 16 | t.equal(result, 'bar') 17 | }) 18 | }) 19 | 20 | test('one argument function, called twice', (t) => { 21 | t.plan(5) 22 | 23 | function fn (arg, cb) { 24 | t.equal(arg, 'foo') 25 | cb(null, 'bar') 26 | } 27 | 28 | const memoFn = memo(fn) 29 | 30 | memoFn('foo', (err, result) => { 31 | t.error(err) 32 | t.equal(result, 'bar') 33 | 34 | memoFn('foo', (err, result) => { // should be cached, so `fn` is not called again 35 | t.error(err) 36 | t.equal(result, 'bar') 37 | }) 38 | }) 39 | }) 40 | 41 | test('two argument function, called twice', (t) => { 42 | t.plan(14) 43 | 44 | let called = 0 45 | function fn (arg1, arg2, cb) { 46 | called += 1 47 | if (called === 1) { 48 | t.equal(arg1, 'foo1') 49 | t.equal(arg2, 'foo2') 50 | cb(null, 'bar') 51 | } else if (called === 2) { 52 | t.equal(arg1, 'foo1') 53 | t.equal(arg2, 'baz') 54 | cb(null, 'new') 55 | } else if (called === 3) { 56 | t.equal(arg1, 'baz') 57 | t.equal(arg2, 'foo2') 58 | cb(null, 'new2') 59 | } else if (called === 4) { 60 | t.fail('fn should not be called 4 times') 61 | } 62 | } 63 | 64 | const memoFn = memo(fn) 65 | 66 | memoFn('foo1', 'foo2', (err, result) => { 67 | t.error(err) 68 | t.equal(result, 'bar') 69 | 70 | memoFn('foo1', 'foo2', (err, result) => { // should be cached, so `fn` is not called again 71 | t.error(err) 72 | t.equal(result, 'bar') 73 | 74 | memoFn('foo1', 'baz', (err, result) => { // should not be cached, since one arg changed 75 | t.error(err) 76 | t.equal(result, 'new') 77 | 78 | memoFn('baz', 'foo2', (err, result) => { // should not be cached, since one arg changed 79 | t.error(err) 80 | t.equal(result, 'new2') 81 | }) 82 | }) 83 | }) 84 | }) 85 | }) 86 | 87 | test('errors are not cached', (t) => { 88 | t.plan(5) 89 | 90 | let called = 0 91 | function fn (arg, cb) { 92 | called += 1 93 | t.equal(arg, 'foo') 94 | if (called === 1) { 95 | cb(new Error('first call returns an error')) 96 | } else if (called === 2) { 97 | cb(null, 'bar') // second call does *not* return an error 98 | } else { 99 | t.fail('fn should not be called 3 times') 100 | } 101 | } 102 | 103 | const memoFn = memo(fn) 104 | 105 | memoFn('foo', (err, result) => { 106 | t.ok(err instanceof Error) 107 | memoFn('foo', (err, result) => { 108 | t.error(err) // second call should not return a cached error 109 | t.equal(result, 'bar') 110 | }) 111 | }) 112 | }) 113 | --------------------------------------------------------------------------------