├── .github └── workflows │ ├── ci.yml │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── isaacs-makework.yml │ └── package-json-repo.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── ac.js ├── example.js ├── package-lock.json ├── package.json └── test └── basic.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [12.x, 14.x, 16.x, 17.x] 10 | platform: 11 | - os: ubuntu-latest 12 | shell: bash 13 | - os: macos-latest 14 | shell: bash 15 | - os: windows-latest 16 | shell: bash 17 | - os: windows-latest 18 | shell: powershell 19 | fail-fast: false 20 | 21 | runs-on: ${{ matrix.platform.os }} 22 | defaults: 23 | run: 24 | shell: ${{ matrix.platform.shell }} 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v1.1.0 29 | 30 | - name: Use Nodejs ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Run Tests 39 | run: npm test -- -c -t0 40 | -------------------------------------------------------------------------------- /.github/workflows/commit-if-modified.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git config --global user.email "$1" 3 | shift 4 | git config --global user.name "$1" 5 | shift 6 | message="$1" 7 | shift 8 | if [ $(git status --porcelain "$@" | egrep '^ M' | wc -l) -gt 0 ]; then 9 | git add "$@" 10 | git commit -m "$message" 11 | git push || git pull --rebase 12 | git push 13 | fi 14 | -------------------------------------------------------------------------------- /.github/workflows/copyright-year.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=${1:-$PWD} 3 | dates=($(git log --date=format:%Y --pretty=format:'%ad' --reverse | sort | uniq)) 4 | if [ "${#dates[@]}" -eq 1 ]; then 5 | datestr="${dates}" 6 | else 7 | datestr="${dates}-${dates[${#dates[@]}-1]}" 8 | fi 9 | 10 | stripDate='s/^((.*)Copyright\b(.*?))((?:,\s*)?(([0-9]{4}\s*-\s*[0-9]{4})|(([0-9]{4},\s*)*[0-9]{4})))(?:,)?\s*(.*)\n$/$1$9\n/g' 11 | addDate='s/^.*Copyright(?:\s*\(c\))? /Copyright \(c\) '$datestr' /g' 12 | for l in $dir/LICENSE*; do 13 | perl -pi -e "$stripDate" $l 14 | perl -pi -e "$addDate" $l 15 | done 16 | -------------------------------------------------------------------------------- /.github/workflows/isaacs-makework.yml: -------------------------------------------------------------------------------- 1 | name: "various tidying up tasks to silence nagging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | makework: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Use Node.js 17 | uses: actions/setup-node@v2.1.4 18 | with: 19 | node-version: 16.x 20 | - name: put repo in package.json 21 | run: node .github/workflows/package-json-repo.js 22 | - name: check in package.json if modified 23 | run: | 24 | bash -x .github/workflows/commit-if-modified.sh \ 25 | "package-json-repo-bot@example.com" \ 26 | "package.json Repo Bot" \ 27 | "chore: add repo to package.json" \ 28 | package.json package-lock.json 29 | - name: put all dates in license copyright line 30 | run: bash .github/workflows/copyright-year.sh 31 | - name: check in licenses if modified 32 | run: | 33 | bash .github/workflows/commit-if-modified.sh \ 34 | "license-year-bot@example.com" \ 35 | "License Year Bot" \ 36 | "chore: add copyright year to license" \ 37 | LICENSE* 38 | -------------------------------------------------------------------------------- /.github/workflows/package-json-repo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pf = require.resolve(`${process.cwd()}/package.json`) 4 | const pj = require(pf) 5 | 6 | if (!pj.repository && process.env.GITHUB_REPOSITORY) { 7 | const fs = require('fs') 8 | const server = process.env.GITHUB_SERVER_URL || 'https://github.com' 9 | const repo = `${server}/${process.env.GITHUB_REPOSITORY}` 10 | pj.repository = repo 11 | const json = fs.readFileSync(pf, 'utf8') 12 | const match = json.match(/^\s*\{[\r\n]+([ \t]*)"/) 13 | const indent = match[1] 14 | const output = JSON.stringify(pj, null, indent || 2) + '\n' 15 | fs.writeFileSync(pf, output) 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | nyc_output 3 | node_modules 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4' 6 | before_install: 7 | - npm install -g npm@latest 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2012-2022 Isaac Z. Schlueter 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-cache 2 | 3 | # Deprecated. 4 | 5 | Use [lru-cache](http://npm.im/lru-cache) version 7.6 or higher, and provide 6 | an asynchronous `fetchMethod` option. 7 | 8 | This project will no longer be maintained, as it is no longer necessary. 9 | -------------------------------------------------------------------------------- /ac.js: -------------------------------------------------------------------------------- 1 | module.exports = AsyncCache 2 | 3 | var LRU = require('lru-cache') 4 | 5 | function AsyncCache (opt) { 6 | if (!opt || typeof opt !== 'object') { 7 | throw new Error('options must be an object') 8 | } 9 | 10 | if (!opt.load) { 11 | throw new Error('load function is required') 12 | } 13 | 14 | if (!(this instanceof AsyncCache)) { 15 | return new AsyncCache(opt) 16 | } 17 | 18 | this._opt = opt 19 | this._cache = new LRU(opt) 20 | this._load = opt.load 21 | this._loading = {} 22 | this._stales = {} 23 | this._allowStale = opt.stale 24 | } 25 | 26 | Object.defineProperty(AsyncCache.prototype, 'itemCount', { 27 | get: function () { 28 | return this._cache.itemCount 29 | }, 30 | enumerable: true, 31 | configurable: true 32 | }) 33 | 34 | AsyncCache.prototype.get = function (key, cb) { 35 | var stale = this._stales[key] 36 | if (this._allowStale && stale !== undefined) { 37 | return process.nextTick(function () { 38 | cb(null, stale) 39 | }) 40 | } 41 | 42 | if (this._loading[key]) { 43 | return this._loading[key].push(cb) 44 | } 45 | 46 | var has = this._cache.has(key) 47 | var cached = this._cache.get(key) 48 | if (has && undefined !== cached) { 49 | return process.nextTick(function () { 50 | cb(null, cached) 51 | }) 52 | } 53 | 54 | if (undefined !== cached && this._allowStale && !has) { 55 | this._stales[key] = cached 56 | process.nextTick(function () { 57 | cb(null, cached) 58 | }) 59 | } else { 60 | this._loading[key] = [ cb ] 61 | } 62 | 63 | this._load(key, function (er, res, maxAge) { 64 | if (!er) { 65 | this._cache.set(key, res, maxAge) 66 | } 67 | 68 | if (this._allowStale) { 69 | delete this._stales[key] 70 | } 71 | 72 | var cbs = this._loading[key] 73 | if (!cbs) { 74 | return 75 | } 76 | delete this._loading[key] 77 | 78 | cbs.forEach(function (cb) { 79 | cb(er, res) 80 | }) 81 | }.bind(this)) 82 | } 83 | 84 | AsyncCache.prototype.keys = function () { 85 | return this._cache.keys() 86 | } 87 | 88 | AsyncCache.prototype.set = function (key, val, maxAge) { 89 | return this._cache.set(key, val, maxAge) 90 | } 91 | 92 | AsyncCache.prototype.reset = function () { 93 | return this._cache.reset() 94 | } 95 | 96 | AsyncCache.prototype.has = function (key) { 97 | return this._cache.has(key) 98 | } 99 | 100 | AsyncCache.prototype.del = function (key) { 101 | return this._cache.del(key) 102 | } 103 | 104 | AsyncCache.prototype.peek = function (key) { 105 | return this._cache.peek(key) 106 | } 107 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var AsyncCache = require('./') 2 | var fs = require('fs') 3 | var stats = new AsyncCache({ 4 | max: 100, 5 | maxAge: 1000, 6 | load: function (filePath, cb) { 7 | fs.stat(filePath, cb) 8 | } 9 | }) 10 | 11 | stats.get(__filename, function (er, st) { 12 | if (er) { 13 | throw er 14 | } 15 | console.error('first', st) 16 | }) 17 | 18 | setTimeout(function () { 19 | stats.get(__filename, function (er, st) { 20 | if (er) { 21 | throw er 22 | } 23 | console.error('second, from cache', st) 24 | }) 25 | }, 100) 26 | 27 | setTimeout(function () { 28 | stats.get(__filename, function (er, st) { 29 | if (er) { 30 | throw er 31 | } 32 | console.error('third from cache', st) 33 | }) 34 | }, 200) 35 | 36 | setTimeout(function () { 37 | stats.get(__filename, function (er, st) { 38 | if (er) { 39 | throw er 40 | } 41 | console.error('new, not from cache', st) 42 | }) 43 | }, 1100) 44 | 45 | /* 46 | $ node s.js 47 | first { dev: 234881026, 48 | mode: 33188, 49 | nlink: 1, 50 | uid: 24561, 51 | gid: 20, 52 | rdev: 0, 53 | blksize: 4096, 54 | ino: 79975795, 55 | size: 742, 56 | blocks: 8, 57 | atime: Thu Jan 17 2013 11:26:22 GMT-0800 (PST), 58 | mtime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST), 59 | ctime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST) } 60 | second, from cache { dev: 234881026, 61 | mode: 33188, 62 | nlink: 1, 63 | uid: 24561, 64 | gid: 20, 65 | rdev: 0, 66 | blksize: 4096, 67 | ino: 79975795, 68 | size: 742, 69 | blocks: 8, 70 | atime: Thu Jan 17 2013 11:26:22 GMT-0800 (PST), 71 | mtime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST), 72 | ctime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST) } 73 | third from cache { dev: 234881026, 74 | mode: 33188, 75 | nlink: 1, 76 | uid: 24561, 77 | gid: 20, 78 | rdev: 0, 79 | blksize: 4096, 80 | ino: 79975795, 81 | size: 742, 82 | blocks: 8, 83 | atime: Thu Jan 17 2013 11:26:22 GMT-0800 (PST), 84 | mtime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST), 85 | ctime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST) } 86 | new, not from cache { dev: 234881026, 87 | mode: 33188, 88 | nlink: 1, 89 | uid: 24561, 90 | gid: 20, 91 | rdev: 0, 92 | blksize: 4096, 93 | ino: 79975795, 94 | size: 742, 95 | blocks: 8, 96 | atime: Thu Jan 17 2013 11:26:22 GMT-0800 (PST), 97 | mtime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST), 98 | ctime: Thu Jan 17 2013 11:26:21 GMT-0800 (PST) } 99 | */ 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-cache", 3 | "version": "1.1.0", 4 | "description": "Cache your async lookups and don't fetch the same thing more than necessary.", 5 | "main": "ac.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "files": [ 10 | "ac.js" 11 | ], 12 | "dependencies": { 13 | "lru-cache": "^4.0.0" 14 | }, 15 | "devDependencies": { 16 | "tap": "^15.1.6" 17 | }, 18 | "scripts": { 19 | "test": "tap" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/isaacs/async-cache" 24 | }, 25 | "keywords": [ 26 | "async", 27 | "cache", 28 | "lru" 29 | ], 30 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 31 | "license": "ISC" 32 | } 33 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | var AC = require('../ac.js') 3 | var fs = require('fs') 4 | 5 | test('options check', function (t) { 6 | var types = ['string', 123, true, new RegExp('boom', 'g'), undefined, null] 7 | 8 | types.forEach(function (val, key) { 9 | t.throws(function () { 10 | var ac = new AC() 11 | t.equal(ac, undefined) 12 | }, 'options must be an object') 13 | }) 14 | 15 | t.throws(function () { 16 | var ac = new AC({ noload: function () {} }) 17 | t.equal(ac, undefined) 18 | }, 'throws since parameter is an object without a .load property') 19 | 20 | t.doesNotThrow(function () { 21 | var ac = AC({ load: function () {} }) 22 | t.type(ac, 'object') 23 | }, 'does not throw since parameter has a .load property') 24 | 25 | t.end() 26 | }) 27 | 28 | test('basic', function (t) { 29 | var ac = new AC({ 30 | max: 1, 31 | load: function (key, cb) { 32 | fs.stat(key, cb) 33 | } 34 | }) 35 | 36 | var called = 0 37 | var stFirst = null 38 | var stSecond = null 39 | 40 | t.equal(ac.itemCount, 0) 41 | ac.get(__filename, afterFirst) 42 | function afterFirst (er, st) { 43 | if (er) throw er 44 | t.equal(ac.itemCount, 1) 45 | called++ 46 | stFirst = st 47 | t.pass('called the first one') 48 | if (called === 2) next() 49 | } 50 | 51 | var expectLoading = {} 52 | expectLoading[__filename] = [afterFirst] 53 | t.same(ac._loading, expectLoading) 54 | 55 | ac.get(__filename, afterSecond) 56 | function afterSecond (er, st) { 57 | if (er) throw er 58 | t.equal(ac.itemCount, 1) 59 | called++ 60 | stSecond = st 61 | t.pass('called the second one') 62 | if (called === 2) next() 63 | } 64 | 65 | expectLoading[__filename].push(afterSecond) 66 | t.same(ac._loading, expectLoading) 67 | t.type(ac.peek(__filename), 'undefined') 68 | 69 | function next () { 70 | t.equal(ac.itemCount, 1) 71 | t.equal(stFirst, stSecond, 'should be same stat object') 72 | t.equal(stFirst, ac.peek(__filename), 'should be same stat object') 73 | t.same(ac._loading, {}) 74 | t.equal(called, 2) 75 | ac.get(__filename, function (er, st) { 76 | if (er) throw er 77 | t.equal(st, stFirst, 'should be cached stat object') 78 | next2() 79 | }) 80 | } 81 | 82 | function next2 () { 83 | // now make it fall out of cache by fetching a new one. 84 | ac.get(__dirname, function (er, st) { 85 | if (er) throw er 86 | t.type(ac.peek(__filename), 'undefined') 87 | t.equal(ac.itemCount, 1) 88 | ac.get(__filename, function (er, st) { 89 | if (er) throw er 90 | t.equal(ac.itemCount, 1) 91 | t.equal(ac.has(__filename), true) 92 | t.not(st, stFirst, 'should have re-fetched') 93 | ac.del(__filename) 94 | t.equal(ac.itemCount, 0) 95 | ac.set('foo', 'bar') 96 | t.equal(ac.itemCount, 1) 97 | ac.reset() 98 | t.equal(ac.itemCount, 0) 99 | t.end() 100 | }) 101 | }) 102 | } 103 | }) 104 | 105 | test('allow stale', function (t) { 106 | var v = 0 107 | var ac = new AC({ 108 | max: 1, 109 | load: function (key, cb) { 110 | setTimeout(function () { 111 | cb(null, v++) 112 | }, 100) 113 | }, 114 | maxAge: 10, 115 | stale: true 116 | }) 117 | 118 | t.equal(ac.itemCount, 0) 119 | ac.get('foo', function (er, val) { 120 | t.equal(ac.itemCount, 1) 121 | t.equal(val, 0) 122 | var start = Date.now() 123 | setTimeout(function () { 124 | ac.get('foo', function (er, val) { 125 | var end = Date.now() 126 | t.equal(val, 0) 127 | t.ok(end - start < 50, 'should be stale') 128 | t.end() 129 | }) 130 | }, 15) 131 | }) 132 | }) 133 | 134 | test('return stale while updating', function (t) { 135 | var maxAge = 500 136 | var loadingTimes = 0 137 | var ac = new AC({ 138 | max: 1000, 139 | stale: true, 140 | maxAge: maxAge, 141 | load: function (key, cb) { 142 | loadingTimes++ 143 | setTimeout(function () { 144 | cb(null, { created: Date.now(), version: loadingTimes }) 145 | }, 450) 146 | } 147 | }) 148 | 149 | var staleTimes = 0 150 | var responses = 0 151 | 152 | function step () { 153 | ac.get('someKey', function (err, item) { 154 | var resTime = Date.now() 155 | if (err) { 156 | throw err 157 | } else { 158 | var itemAge = resTime - item.created 159 | if (itemAge > maxAge) { 160 | staleTimes++ 161 | } 162 | 163 | responses++ 164 | 165 | if (responses === 30) { 166 | t.equal(staleTimes, 10, '10 stale times') 167 | t.equal(loadingTimes, 3, '3 loading times') 168 | t.end() 169 | } 170 | } 171 | }) 172 | } 173 | 174 | for (var i = 0; i < 30; i++) { 175 | setTimeout(step, 100 * i) 176 | } 177 | }) 178 | 179 | test('keys', function (t) { 180 | var ac = new AC({ 181 | max: 10, 182 | load: function (key, cb) { 183 | cb({ msg: 'item not in cache' }) 184 | }, 185 | maxAge: 10, 186 | stale: true 187 | }) 188 | 189 | t.equal(ac.itemCount, 0) 190 | 191 | ac.set('foo1', 'bar1') 192 | ac.set('foo2', 'bar2') 193 | 194 | var keys = ac.keys() 195 | 196 | t.ok(keys.indexOf('foo1') !== -1) 197 | t.ok(keys.indexOf('foo2') !== -1) 198 | 199 | t.end() 200 | }) 201 | 202 | test('per item maxAge', function (t) { 203 | var counter = 0 204 | var ac = new AC({ 205 | load: function (n, cb) { 206 | ++counter 207 | setTimeout(function () { 208 | // max age set to 500 209 | cb(null, 'value', 250) 210 | }, 0) 211 | } 212 | }) 213 | 214 | function afterFirst (err, item) { 215 | if (err) throw err 216 | t.equal(item, 'value') 217 | t.equal(counter, 1, 'load called 1 time') 218 | 219 | ac.get('key', afterSecond) 220 | } 221 | 222 | function afterSecond (err, item) { 223 | if (err) throw err 224 | t.equal(item, 'value') 225 | t.equal(counter, 1, 'load still called 1 time') 226 | 227 | setTimeout(function () { 228 | ac.get('key', afterThird) 229 | }, 260) // wait longer then maxAge 230 | } 231 | 232 | function afterThird (err, item) { 233 | if (err) throw err 234 | t.equal(item, 'value') 235 | t.equal(counter, 2, 'load called twice since maxAge elapsed') 236 | 237 | t.end() 238 | } 239 | 240 | ac.get('key', afterFirst) 241 | }) 242 | 243 | test('load error', function (t) { 244 | var ac = new AC({ 245 | load: function (key, cb) { 246 | cb(new Error('oops')) 247 | }, 248 | }) 249 | ac.get('key', function (er, value) { 250 | t.match(er, new Error('oops')) 251 | t.end() 252 | }) 253 | }) 254 | --------------------------------------------------------------------------------