├── .github └── workflows │ ├── benchmark.yml │ ├── ci.yml │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── isaacs-makework.yml │ ├── package-json-repo.js │ └── typedoc.yml ├── .gitignore ├── .prettierignore ├── .tshy ├── build.json ├── commonjs.json └── esm.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── benchmark ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── fetch-impls.sh ├── impls.js ├── index.js ├── make-deps.sh ├── profile.sh └── worker.js ├── fixup.sh ├── map.js ├── package-lock.json ├── package.json ├── scripts ├── .gitignore ├── benchmark-results-typedoc.sh ├── package-lock.json ├── package.json └── transpile-to-esm.js ├── src └── index.ts ├── tap-snapshots └── test │ ├── basic.ts.test.cjs │ ├── deprecations.ts.test.cjs │ ├── fetch.ts.test.cjs │ ├── map-like.ts.test.cjs │ ├── move-to-tail.ts.test.cjs │ ├── size-calculation.ts.test.cjs │ └── ttl.ts.test.cjs ├── test ├── avoid-memory-leak.ts ├── basic.ts ├── delete-while-iterating.ts ├── dispose.ts ├── esm-load.mjs ├── fetch.ts ├── find.ts ├── fixtures │ └── expose.ts ├── import.mjs ├── info.ts ├── load-check.ts ├── load.ts ├── map-like.ts ├── move-to-tail.ts ├── pop.ts ├── purge-stale-exhaustive.ts ├── reverse-iterate-delete-all.ts ├── size-calculation.ts ├── ttl.ts ├── unbounded-warning.ts └── warn-missing-ac.ts ├── tsconfig.json └── typedoc.json /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [16.x, 18.x, 19.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 | fail-fast: false 18 | 19 | runs-on: ${{ matrix.platform.os }} 20 | defaults: 21 | run: 22 | shell: ${{ matrix.platform.shell }} 23 | 24 | steps: 25 | - name: Checkout Repository 26 | uses: actions/checkout@v3 27 | 28 | - name: Use Nodejs ${{ matrix.node-version }} 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | 33 | - name: Use latest npm 34 | run: npm i -g npm@latest 35 | 36 | - name: Install dependencies 37 | run: npm install 38 | 39 | - name: Run Benchmarks 40 | run: npm run benchmark 41 | -------------------------------------------------------------------------------- /.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: [8.x, 20.x, 21.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 | if: ${{ (matrix.node-version == '18.x') || (matrix.node-version == '19.x') || (matrix.platform.os != 'windows-latest') }} 29 | uses: actions/checkout@v3 30 | 31 | - name: Use Nodejs ${{ matrix.node-version }} 32 | if: ${{ (matrix.node-version == '18.x') || (matrix.node-version == '19.x') || (matrix.platform.os != 'windows-latest') }} 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - name: debugging 38 | run: echo NODEVERSION=[${{ matrix.node-version }}] OS=[${{ matrix.platform.os }}] 39 | 40 | - name: Install dependencies 41 | if: ${{ (matrix.node-version == '18.x') || (matrix.node-version == '19.x') || (matrix.platform.os != 'windows-latest') }} 42 | run: npm install 43 | 44 | - name: Run Tests 45 | if: ${{ (matrix.node-version == '18.x') || (matrix.node-version == '19.x') || (matrix.platform.os != 'windows-latest') }} 46 | run: npm test -- -c -t0 47 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/typedoc.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Use Nodejs ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18.x 37 | - name: Install dependencies 38 | run: npm install 39 | - name: Generate typedocs 40 | run: npm run typedoc 41 | - name: Generate Benchmarks 42 | run: npm run benchmark 43 | - name: Copy Benchmarks to Docs 44 | run: npm run benchmark-results-typedoc 45 | - name: Setup Pages 46 | uses: actions/configure-pages@v3 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v1 49 | with: 50 | path: './docs' 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v1 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.tap 2 | /node_modules 3 | /dist 4 | /docs 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /docs 3 | /.tap 4 | /*.json 5 | /example 6 | /.github 7 | /dist 8 | /tap-snapshots 9 | /benchmark 10 | /.tshy 11 | -------------------------------------------------------------------------------- /.tshy/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "../src", 5 | "target": "es2022", 6 | "module": "nodenext", 7 | "moduleResolution": "nodenext" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.tshy/commonjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build.json", 3 | "include": [ 4 | "../src/**/*.ts", 5 | "../src/**/*.cts", 6 | "../src/**/*.tsx" 7 | ], 8 | "exclude": [ 9 | "../src/**/*.mts" 10 | ], 11 | "compilerOptions": { 12 | "outDir": "../.tshy-build/commonjs" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.tshy/esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./build.json", 3 | "include": [ 4 | "../src/**/*.ts", 5 | "../src/**/*.mts", 6 | "../src/**/*.tsx" 7 | ], 8 | "exclude": [], 9 | "compilerOptions": { 10 | "outDir": "../.tshy-build/esm" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # cringe lorg 2 | 3 | ## 10.2.0 4 | 5 | - types: implement the `Map` interface 6 | 7 | ## 10.1.0 8 | 9 | - add `cache.info(key)` to get value as well as ttl and size 10 | information. 11 | 12 | ## 10.0.0 13 | 14 | - `cache.fetch()` return type is now `Promise` 15 | instead of `Promise`. This is an irrelevant change 16 | practically speaking, but can require changes for TypeScript 17 | users. 18 | 19 | ## 9.1.0 20 | 21 | - `cache.set(key, undefined)` is now an alias for 22 | `cache.delete(key)` 23 | 24 | ## 9.0.0 25 | 26 | - Use named export only, no default export. 27 | - Bring back minimal polyfill. If this polyfill ends up being 28 | used, then a warning is printed, as it is not safe for use 29 | outside of LRUCache. 30 | 31 | ## 8.0.0 32 | 33 | - The `fetchContext` option was renamed to `context`, and may no 34 | longer be set on the cache instance itself. 35 | - Rewritten in TypeScript, so pretty much all the types moved 36 | around a lot. 37 | - The AbortController/AbortSignal polyfill is removed. For this 38 | reason, **Node version 16.14.0 or higher is now required**. 39 | - Internal properties were moved to actual private class 40 | properties. 41 | - Keys and values must not be `null` or `undefined`. 42 | - Minified export available at `'lru-cache/min'`, for both CJS 43 | and MJS builds. 44 | 45 | ## 7.18.0 46 | 47 | - Add support for internal state investigation through the use of 48 | a `status` option to `has()`, `set()`, `get()`, and `fetch()`. 49 | 50 | ## 7.17.0 51 | 52 | - Add `signal` option for `fetch` to pass a user-supplied 53 | AbortSignal 54 | - Add `ignoreFetchAbort` and `allowStaleOnFetchAbort` options 55 | 56 | ## 7.16.2 57 | 58 | - Fail fetch() promises when they are aborted 59 | 60 | ## 7.16.0 61 | 62 | - Add `allowStaleOnFetchRejection` option 63 | 64 | ## 7.15.0 65 | 66 | - Provide both ESM and CommonJS exports 67 | 68 | ## 7.14.0 69 | 70 | - Add `maxEntrySize` option to prevent caching items above a 71 | given calculated size. 72 | 73 | ## 7.13.0 74 | 75 | - Add `forceRefresh` option to trigger a call to the 76 | `fetchMethod` even if the item is found in cache, and not 77 | older than its `ttl`. 78 | 79 | ## 7.12.0 80 | 81 | - Add `fetchContext` option to provide additional information to 82 | the `fetchMethod` 83 | - 7.12.1: Fix bug where adding an item with size greater than 84 | `maxSize` would cause bizarre behavior. 85 | 86 | ## 7.11.0 87 | 88 | - Add 'noDeleteOnStaleGet' option, to suppress behavior where a 89 | `get()` of a stale item would remove it from the cache. 90 | 91 | ## 7.10.0 92 | 93 | - Add `noDeleteOnFetchRejection` option, to suppress behavior 94 | where a failed `fetch` will delete a previous stale value. 95 | - Ship types along with the package, rather than relying on 96 | out of date types coming from DefinitelyTyped. 97 | 98 | ## 7.9.0 99 | 100 | - Better AbortController polyfill, supporting 101 | `signal.addEventListener('abort')` and `signal.onabort`. 102 | - (7.9.1) Drop item from cache instead of crashing with an 103 | `unhandledRejection` when the `fetchMethod` throws an error or 104 | returns a rejected Promise. 105 | 106 | ## 7.8.0 107 | 108 | - add `updateAgeOnHas` option 109 | - warnings sent to `console.error` if `process.emitWarning` unavailable 110 | 111 | ## 7.7.0 112 | 113 | - fetch: provide options and abort signal 114 | 115 | ## 7.6.0 116 | 117 | - add cache.getRemainingTTL(key) 118 | - Add async cache.fetch() method, fetchMethod option 119 | - Allow unbounded storage if maxSize or ttl set 120 | 121 | ## 7.5.0 122 | 123 | - defend against mutation while iterating 124 | - Add rentries, rkeys, rvalues 125 | - remove bundler and unnecessary package.json fields 126 | 127 | ## 7.4.0 128 | 129 | - Add browser optimized webpack bundle, exposed as `'lru-cache/browser'` 130 | - Track size of compiled bundle in CI ([@SuperOleg39](https://github.com/SuperOleg39)) 131 | - Add `noUpdateTTL` option for `set()` 132 | 133 | ## 7.3.0 134 | 135 | - Add `disposeAfter()` 136 | - `set()` returns the cache object 137 | - `delete()` returns boolean indicating whether anything was deleted 138 | 139 | ## 7.2.0 140 | 141 | - Add reason to dispose() calls. 142 | 143 | ## 7.1.0 144 | 145 | - Add `ttlResolution` option 146 | - Add `ttlAutopurge` option 147 | 148 | ## v7 - 2022-02 149 | 150 | This library changed to a different algorithm and internal data structure 151 | in version 7, yielding significantly better performance, albeit with 152 | some subtle changes as a result. 153 | 154 | If you were relying on the internals of LRUCache in version 6 or before, it 155 | probably will not work in version 7 and above. 156 | 157 | ### Specific API Changes 158 | 159 | For the most part, the feature set has been maintained as much as possible. 160 | 161 | However, some other cleanup and refactoring changes were made in v7 as 162 | well. 163 | 164 | - The `set()`, `get()`, and `has()` functions take options objects 165 | instead of positional booleans/integers for optional parameters. 166 | - `size` can be set explicitly on `set()`. 167 | - `cache.length` was renamed to the more fitting `cache.size`. 168 | - Deprecations: 169 | - `stale` option -> `allowStale` 170 | - `maxAge` option -> `ttl` 171 | - `length` option -> `sizeCalculation` 172 | - `length` property -> `size` 173 | - `del()` method -> `delete()` 174 | - `prune()` method -> `purgeStale()` 175 | - `reset()` method -> `clear()` 176 | - The objects used by `cache.load()` and `cache.dump()` are incompatible 177 | with previous versions. 178 | - `max` and `maxSize` are now two separate options. (Previously, they were 179 | a single `max` option, which would be based on either count or computed 180 | size.) 181 | - The function assigned to the `dispose` option is now expected to have signature 182 | `(value, key, reason)` rather than `(key, value)`, reversing the order of 183 | `value` and `key`. 184 | 185 | ## v6 - 2020-07 186 | 187 | - Drop support for node v8 and earlier 188 | 189 | ## v5 - 2018-11 190 | 191 | - Add updateAgeOnGet option 192 | - Guards around setting max/maxAge to non-numbers 193 | - Use classes, drop support for old nodes 194 | 195 | ## v4 - 2015-12 196 | 197 | - Improve performance 198 | - add noDisposeOnSet option 199 | - feat(prune): allow users to proactively prune old entries 200 | - Use Symbols for private members 201 | - Add maxAge setter/getter 202 | 203 | ## v3 - 2015-11 204 | 205 | - Add cache.rforEach 206 | - Allow non-string keys 207 | 208 | ## v2 - 2012-08 209 | 210 | - add cache.pop() 211 | - add cache.peek() 212 | - add cache.keys() 213 | - add cache.values() 214 | - fix memory leak 215 | - add `stale` option to return stale values before deleting 216 | - use null-prototype object to avoid hazards 217 | - make options argument an object 218 | 219 | ## v1 - 2010-05 220 | 221 | - initial implementation 222 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please consider signing [the neveragain.tech pledge](http://neveragain.tech/) 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # Authors, sorted by whether or not they are me 2 | Isaac Z. Schlueter 3 | Brian Cottingham 4 | Carlos Brito Lage 5 | Jesse Dailey 6 | Kevin O'Hara 7 | Marco Rogers 8 | Mark Cavage 9 | Marko Mikulicic 10 | Nathan Rajlich 11 | Satheesh Natesan 12 | Trent Mick 13 | ashleybrener 14 | n4kz 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors 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 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /package.json 4 | /package-lock.json 5 | /impls.txt 6 | /results 7 | /results.md 8 | /profiles 9 | /profile.txt 10 | /isolate*.log 11 | -------------------------------------------------------------------------------- /benchmark/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.1.0 (2017-10-02) 3 | 4 | * 1.0.0 ([4b43691](https://github.com/dominictarr/bench-lru/commit/4b43691)) 5 | * Add bench specification ([c55b726](https://github.com/dominictarr/bench-lru/commit/c55b726)) 6 | * add hashlru ([09e99a0](https://github.com/dominictarr/bench-lru/commit/09e99a0)) 7 | * Add ignore ([4f8b103](https://github.com/dominictarr/bench-lru/commit/4f8b103)) 8 | * Add linter and changelog automatization ([8eadb2d](https://github.com/dominictarr/bench-lru/commit/8eadb2d)) 9 | * Add UI feedback ([d4a7977](https://github.com/dominictarr/bench-lru/commit/d4a7977)) 10 | * Avoid store data ([bf49f44](https://github.com/dominictarr/bench-lru/commit/bf49f44)) 11 | * Calculate bundle size ([685dbfa](https://github.com/dominictarr/bench-lru/commit/685dbfa)) 12 | * deps ([4c8f827](https://github.com/dominictarr/bench-lru/commit/4c8f827)) 13 | * Fix find ([f8c979e](https://github.com/dominictarr/bench-lru/commit/f8c979e)) 14 | * Fix scope ([2be6027](https://github.com/dominictarr/bench-lru/commit/2be6027)) 15 | * fix typo @chentsulin found ([46eead9](https://github.com/dominictarr/bench-lru/commit/46eead9)) 16 | * Improve format ([7dec452](https://github.com/dominictarr/bench-lru/commit/7dec452)) 17 | * initial ([e945b02](https://github.com/dominictarr/bench-lru/commit/e945b02)) 18 | * Moar runs ([00133bd](https://github.com/dominictarr/bench-lru/commit/00133bd)) 19 | * Move round inside bench ([ba91f0c](https://github.com/dominictarr/bench-lru/commit/ba91f0c)) 20 | * new results ([cf2a362](https://github.com/dominictarr/bench-lru/commit/cf2a362)) 21 | * Re-testing with `tiny-lru`, fixes #4 ([8130d27](https://github.com/dominictarr/bench-lru/commit/8130d27)), closes [#4](https://github.com/dominictarr/bench-lru/issues/4) 22 | * results and discussion ([f566cd2](https://github.com/dominictarr/bench-lru/commit/f566cd2)) 23 | * Sort by name ([9c85fb2](https://github.com/dominictarr/bench-lru/commit/9c85fb2)) 24 | * Sort results ([60dbed3](https://github.com/dominictarr/bench-lru/commit/60dbed3)) 25 | * Sort results ([f294ccc](https://github.com/dominictarr/bench-lru/commit/f294ccc)) 26 | * Update ([5c244a6](https://github.com/dominictarr/bench-lru/commit/5c244a6)) 27 | * Update deps ([35ac9f7](https://github.com/dominictarr/bench-lru/commit/35ac9f7)) 28 | * update readme ([df5c278](https://github.com/dominictarr/bench-lru/commit/df5c278)) 29 | * Update README.md ([f5e6dd4](https://github.com/dominictarr/bench-lru/commit/f5e6dd4)) 30 | * Updating `data.csv` ([6103c7c](https://github.com/dominictarr/bench-lru/commit/6103c7c)) 31 | * Updating a typo ([8286afa](https://github.com/dominictarr/bench-lru/commit/8286afa)) 32 | * Updating tiny-lru & re-enabling it's test ([954e28a](https://github.com/dominictarr/bench-lru/commit/954e28a)) 33 | * use hashlru, and benchmark reads also ([6fea600](https://github.com/dominictarr/bench-lru/commit/6fea600)) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /benchmark/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 'Dominic Tarr' 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /benchmark/Makefile: -------------------------------------------------------------------------------- 1 | all: package.json index.js worker.js 2 | rm -rf results.txt results 3 | npm run benchmark | tee results.md 4 | 5 | impls.txt: fetch-impls.sh 6 | bash fetch-impls.sh 7 | 8 | profile: worker.js 9 | bash profile.sh 10 | 11 | package.json: make-deps.sh impls.txt 12 | bash make-deps.sh 13 | -------------------------------------------------------------------------------- /benchmark/fetch-impls.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # get the latest patch in each lru-cache 7.x and up, 4 | # plus mnemonist, hashlru, and lru-fast 5 | 6 | nvs=($( 7 | npm view 'lru-cache@>=7' name | awk -F. '{print $1 "." $2}' | sort -r -V | uniq 8 | ) 9 | 'mnemonist@0.39' 10 | 'hashlru@2' 11 | 'lru-fast@0.2') 12 | 13 | echo "lru-cache_CURRENT" > impls.txt 14 | for dep in "${nvs[@]}"; do 15 | name=${dep/@/_} 16 | echo $name >> impls.txt 17 | done 18 | -------------------------------------------------------------------------------- /benchmark/impls.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs') 2 | const impls = readFileSync(__dirname + '/impls.txt', 'utf8') 3 | .trim() 4 | .split('\n') 5 | for (const impl of impls) { 6 | if (impl.startsWith('lru-cache_')) { 7 | const LRUCache = require(impl) 8 | exports[impl] = max => new LRUCache({ max }) 9 | } else if (impl.startsWith('mnemonist_')) { 10 | MnemonistLRUMap = require(impl + '/lru-map-with-delete') 11 | MnemonistLRUCache = require(impl + '/lru-cache-with-delete') 12 | exports[impl + '_obj'] = max => new MnemonistLRUCache(max) 13 | exports[impl + '_map'] = max => new MnemonistLRUMap(max) 14 | } else if (impl.startsWith('hashlru_')) { 15 | exports[impl] = require(impl) 16 | } else if (impl.startsWith('lru-fast_')) { 17 | const { LRUCache } = require(impl) 18 | exports[impl] = max => new LRUCache(max) 19 | } else { 20 | throw new Error( 21 | 'found an impl i dont know how to create: ' + impl 22 | ) 23 | } 24 | } 25 | 26 | exports['just a Map'] = _ => new Map() 27 | 28 | exports['just a null obj'] = _ => { 29 | const data = Object.create(null) 30 | return { set: (k, v) => (data[k] = v), get: k => data[k] } 31 | } 32 | 33 | exports['just a {}'] = _ => { 34 | const data = {} 35 | return { set: (k, v) => (data[k] = v), get: k => data[k] } 36 | } 37 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.__LRU_BENCH_DIR = __dirname 4 | require('mkdirp').sync(__dirname + '/results') 5 | 6 | const Worker = require('tiny-worker') 7 | const ora = require('ora') 8 | const caches = Object.keys(require('./impls.js')) 9 | const nth = caches.length 10 | const { writeFileSync } = require('fs') 11 | 12 | const types = { 13 | int: 'just an integer', 14 | strint: 'stringified integer', 15 | str: 'string that is not a number', 16 | numstr: 'a mix of integers and strings that look like them', 17 | pi: 'multiples of pi', 18 | float: 'floating point values', 19 | obj: 'an object with a single key', 20 | rand: 'random floating point number', 21 | sym: 'a Symbol object', 22 | longstr: 'a very long string', 23 | mix: 'a mix of all the types', 24 | } 25 | 26 | if (!process.env.TYPE) { 27 | const spawn = require('child_process').spawn 28 | const todo = Object.keys(types) 29 | const run = () => 30 | new Promise(res => { 31 | const TYPE = todo.shift() 32 | if (!TYPE) return res() 33 | console.log(`${TYPE}: ${types[TYPE]}`) 34 | const child = spawn(process.execPath, [__filename], { 35 | env: { TYPE }, 36 | stdio: 'inherit', 37 | }) 38 | child.on('close', () => res(run())) 39 | }) 40 | run() 41 | } else { 42 | const spinner = ora(`Starting benchmark of ${nth} caches`).start(), 43 | promises = [] 44 | 45 | caches.forEach((i, idx) => { 46 | promises.push( 47 | new Promise((resolve, reject) => { 48 | return (idx === 0 ? Promise.resolve() : promises[idx - 1]) 49 | .then(() => { 50 | const worker = new Worker('worker.js') 51 | 52 | worker.onmessage = ev => { 53 | resolve(ev.data) 54 | worker.terminate() 55 | } 56 | 57 | worker.onerror = err => { 58 | reject(err) 59 | worker.terminate() 60 | } 61 | 62 | spinner.text = `Benchmarking ${ 63 | idx + 1 64 | } of ${nth} caches [${i}]` 65 | worker.postMessage(i) 66 | }) 67 | .catch(reject) 68 | }) 69 | ) 70 | }) 71 | 72 | Promise.all(promises) 73 | .then(results => { 74 | const toMD = require('markdown-tables') 75 | const keysort = require('keysort') 76 | spinner.stop() 77 | const data = keysort( 78 | results.map(i => { 79 | const obj = JSON.parse(i) 80 | obj.score = 81 | obj.evict * 5 + 82 | obj.get2 * 5 + 83 | obj.get1 * 3 + 84 | obj.set * 2 + 85 | obj.update 86 | return obj 87 | }), 88 | 'score desc' 89 | ) 90 | 91 | const heading = 'name,set,get1,update,get2,evict,score' 92 | const csv = 93 | [heading] 94 | .concat( 95 | data.map( 96 | i => 97 | `${i.name},${i.set},${i.get1},${i.update},${i.get2},${i.evict},${i.score}` 98 | ) 99 | ) 100 | .join('\n') + '\n' 101 | const resultsFile = `${__dirname}/results/${process.env.TYPE}.csv` 102 | writeFileSync(resultsFile, csv, 'utf8') 103 | console.log(toMD(csv)) 104 | }) 105 | .catch(err => { 106 | console.error(err.stack || err.message || err) 107 | process.exit(1) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /benchmark/make-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | deps="" 4 | install=() 5 | for name in $(cat impls.txt); do 6 | if [ "$name" = "lru-cache_CURRENT" ]; then 7 | continue 8 | fi 9 | dep=${name/_/@} 10 | deps="${deps}"' "'"$name"'": "'"npm:$dep"$'",\n' 11 | done 12 | 13 | cat >package.json < $d 8 | ln ${PWD}/$d profile.txt 9 | cat profile.txt 10 | -------------------------------------------------------------------------------- /benchmark/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const precise = require('precise') 4 | const retsu = require('retsu') 5 | const dir = process.env.__LRU_BENCH_DIR || __dirname 6 | const caches = require(dir + '/impls.js') 7 | const num = +process.env.N || 10_000 8 | const evict = num * 2 9 | const times = 10 10 | const x = 1e6 11 | const dataOrder = [] 12 | const data1 = new Array(evict) 13 | const data2 = new Array(evict) 14 | const data3 = new Array(evict) 15 | 16 | const typeGen = { 17 | numstr: z => (z % 2 === 0 ? z : String(z + 1)), 18 | pi: z => z * Math.PI, 19 | float: z => z + z / (evict + 1), 20 | obj: z => ({ z }), 21 | strint: z => String(z), 22 | str: z => 'foo' + z + 'bar', 23 | rand: z => z * Math.random(), 24 | sym: z => Symbol(String(z)), 25 | longstr: z => z + 'z'.repeat(1024 * 4), 26 | int: z => z, 27 | mix: z => typeGen[typeKeys[z % (typeKeys.length - 1)]](z), 28 | } 29 | const typeKeys = Object.keys(typeGen) 30 | 31 | ;(function seed() { 32 | let z = -1 33 | 34 | const t = process.env.TYPE || 'mix' 35 | while (++z < evict) { 36 | const x = typeGen[t](z) 37 | data1[z] = [x, Math.floor(Math.random() * 1e7)] 38 | dataOrder.push(z) 39 | } 40 | 41 | // shuffle up the key orders, so we're not just walking down the list. 42 | for (const key of dataOrder.sort(() => Math.random() - 0.5)) { 43 | data2[key] = [data1[key][0], Math.random() * 1e7] 44 | } 45 | 46 | for (const key of dataOrder.sort(() => Math.random() - 0.5)) { 47 | data3[key] = data1[key] 48 | } 49 | })() 50 | 51 | const runTest = id => { 52 | const time = { 53 | set: [], 54 | get1: [], 55 | update: [], 56 | get2: [], 57 | evict: [], 58 | } 59 | const results = { 60 | name: id, 61 | set: 0, 62 | get1: 0, 63 | update: 0, 64 | get2: 0, 65 | evict: 0, 66 | } 67 | 68 | let n = -1 69 | 70 | // super rudimentary correctness check 71 | // make sure that 5 puts get back the same 5 items we put 72 | // ignore stderr, some caches are complainy about some keys 73 | let error = console.error 74 | console.error = () => {} 75 | try { 76 | const s = Math.max(5, Math.min(Math.floor(num / 2), 50)) 77 | const m = Math.min(s * 5, num) 78 | const lru = caches[id](s) 79 | for (let i = 0; i < s; i++) lru.set(data1[i][0], data1[i][1]) 80 | for (let i = 0; i < s; i++) { 81 | if (lru.get(data1[i][0]) !== data1[i][1]) { 82 | if (!process.stdout.isTTY) process.stderr.write(id) 83 | error(' failed correctness check at key=%j', data1[i][0]) 84 | postMessage( 85 | JSON.stringify({ 86 | name: id, 87 | set: 0, 88 | get1: 0, 89 | update: 0, 90 | get2: 0, 91 | evict: 0, 92 | }) 93 | ) 94 | process.exit(1) 95 | } 96 | } 97 | if (!/^just a/.test(id) && !/unbounded$/.test(id)) { 98 | for (let i = s + 1; i < m; i++) 99 | lru.set(data1[i][0], data1[i][1]) 100 | if (lru.get(data1[0][0])) { 101 | if (!process.stdout.isTTY) process.stderr.write(id) 102 | error(' failed eviction correctness check') 103 | postMessage( 104 | JSON.stringify({ 105 | name: id, 106 | set: 0, 107 | get1: 0, 108 | update: 0, 109 | get2: 0, 110 | evict: 0, 111 | }) 112 | ) 113 | process.exit(1) 114 | } 115 | } 116 | lru.set('__proto__', { [__filename]: 'pwned' }) 117 | if (lru.get(__filename)) { 118 | error(' failed prototype pollution check') 119 | if (!/^just a/.test(id)) { 120 | postMessage( 121 | JSON.stringify({ 122 | name: id, 123 | set: 0, 124 | get1: 0, 125 | update: 0, 126 | get2: 0, 127 | evict: 0, 128 | }) 129 | ) 130 | process.exit(1) 131 | } 132 | } 133 | } catch (er) { 134 | if (!process.stdout.isTTY) process.stderr.write(id) 135 | error(' failed correctness check', er.stack) 136 | postMessage( 137 | JSON.stringify({ 138 | name: id, 139 | set: 0, 140 | get1: 0, 141 | update: 0, 142 | get2: 0, 143 | evict: 0, 144 | }) 145 | ) 146 | process.exit(1) 147 | } 148 | 149 | console.error = error 150 | 151 | while (++n < times) { 152 | const lru = caches[id](num) 153 | const stimer = precise().start() 154 | for (let i = 0; i < num; i++) lru.set(data1[i][0], data1[i][1]) 155 | time.set.push(stimer.stop().diff() / x) 156 | 157 | const gtimer = precise().start() 158 | for (let i = 0; i < num; i++) lru.get(data1[i][0]) 159 | time.get1.push(gtimer.stop().diff() / x) 160 | 161 | const utimer = precise().start() 162 | for (let i = 0; i < num; i++) lru.set(data2[i][0], data2[i][1]) 163 | time.update.push(utimer.stop().diff() / x) 164 | 165 | const g2timer = precise().start() 166 | for (let i = 0; i < num; i++) lru.get(data3[i][0]) 167 | time.get2.push(g2timer.stop().diff() / x) 168 | 169 | const etimer = precise().start() 170 | for (let i = num; i < evict; i++) 171 | lru.set(data1[i][0], data1[i][1]) 172 | time.evict.push(etimer.stop().diff() / x) 173 | } 174 | 175 | ;['set', 'get1', 'update', 'get2', 'evict'].forEach(i => { 176 | results[i] = Number( 177 | (num / retsu.median(time[i]).toFixed(2)).toFixed(0) 178 | ) 179 | }) 180 | 181 | postMessage(JSON.stringify(results)) 182 | } 183 | 184 | if (typeof self !== 'undefined') { 185 | self.onmessage = ev => runTest(ev.data) 186 | } else { 187 | global.postMessage = console.log 188 | runTest('lru-cache_CURRENT') 189 | } 190 | -------------------------------------------------------------------------------- /fixup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | esbuild --minify \ 4 | --sourcemap \ 5 | --bundle dist/commonjs/index.js \ 6 | --outfile=dist/commonjs/index.min.js \ 7 | --format=cjs 8 | 9 | esbuild --minify \ 10 | --sourcemap \ 11 | --bundle dist/esm/index.js \ 12 | --outfile=dist/esm/index.min.js \ 13 | --format=esm 14 | -------------------------------------------------------------------------------- /map.js: -------------------------------------------------------------------------------- 1 | module.exports = () => 'index.js' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lru-cache", 3 | "description": "A cache object that deletes the least-recently-used items.", 4 | "version": "10.2.0", 5 | "author": "Isaac Z. Schlueter ", 6 | "keywords": [ 7 | "mru", 8 | "lru", 9 | "cache" 10 | ], 11 | "sideEffects": false, 12 | "scripts": { 13 | "build": "npm run prepare", 14 | "prepare": "tshy", 15 | "postprepare": "bash fixup.sh", 16 | "pretest": "npm run prepare", 17 | "presnap": "npm run prepare", 18 | "test": "tap", 19 | "snap": "tap", 20 | "preversion": "npm test", 21 | "postversion": "npm publish", 22 | "prepublishOnly": "git push origin --follow-tags", 23 | "format": "prettier --write .", 24 | "typedoc": "typedoc --tsconfig ./.tshy/esm.json ./src/*.ts", 25 | "benchmark-results-typedoc": "bash scripts/benchmark-results-typedoc.sh", 26 | "prebenchmark": "npm run prepare", 27 | "benchmark": "make -C benchmark", 28 | "preprofile": "npm run prepare", 29 | "profile": "make -C benchmark profile" 30 | }, 31 | "main": "./dist/commonjs/index.js", 32 | "types": "./dist/commonjs/index.d.ts", 33 | "tshy": { 34 | "exports": { 35 | ".": "./src/index.ts", 36 | "./min": { 37 | "import": { 38 | "types": "./dist/mjs/index.d.ts", 39 | "default": "./dist/mjs/index.min.js" 40 | }, 41 | "require": { 42 | "types": "./dist/commonjs/index.d.ts", 43 | "default": "./dist/commonjs/index.min.js" 44 | } 45 | } 46 | } 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git://github.com/isaacs/node-lru-cache.git" 51 | }, 52 | "devDependencies": { 53 | "@tapjs/clock": "^1.1.16", 54 | "@types/node": "^20.2.5", 55 | "@types/tap": "^15.0.6", 56 | "benchmark": "^2.1.4", 57 | "clock-mock": "^2.0.2", 58 | "esbuild": "^0.17.11", 59 | "eslint-config-prettier": "^8.5.0", 60 | "marked": "^4.2.12", 61 | "mkdirp": "^2.1.5", 62 | "prettier": "^2.6.2", 63 | "tap": "^18.5.7", 64 | "tshy": "^1.8.0", 65 | "tslib": "^2.4.0", 66 | "typedoc": "^0.25.3", 67 | "typescript": "^5.2.2" 68 | }, 69 | "license": "ISC", 70 | "files": [ 71 | "dist" 72 | ], 73 | "engines": { 74 | "node": "14 || >=16.14" 75 | }, 76 | "prettier": { 77 | "semi": false, 78 | "printWidth": 70, 79 | "tabWidth": 2, 80 | "useTabs": false, 81 | "singleQuote": true, 82 | "jsxSingleQuote": false, 83 | "bracketSameLine": true, 84 | "arrowParens": "avoid", 85 | "endOfLine": "lf" 86 | }, 87 | "tap": { 88 | "node-arg": [ 89 | "--expose-gc" 90 | ], 91 | "plugin": [ 92 | "@tapjs/clock" 93 | ] 94 | }, 95 | "exports": { 96 | ".": { 97 | "import": { 98 | "types": "./dist/esm/index.d.ts", 99 | "default": "./dist/esm/index.js" 100 | }, 101 | "require": { 102 | "types": "./dist/commonjs/index.d.ts", 103 | "default": "./dist/commonjs/index.js" 104 | } 105 | }, 106 | "./min": { 107 | "import": { 108 | "types": "./dist/mjs/index.d.ts", 109 | "default": "./dist/mjs/index.min.js" 110 | }, 111 | "require": { 112 | "types": "./dist/commonjs/index.d.ts", 113 | "default": "./dist/commonjs/index.min.js" 114 | } 115 | } 116 | }, 117 | "type": "module" 118 | } 119 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | heapdump-* 2 | node_modules 3 | -------------------------------------------------------------------------------- /scripts/benchmark-results-typedoc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | set -e 4 | mkdir -p docs/benchmark/results 5 | cp -r benchmark/results/* docs/benchmark/results/ 6 | echo 'benchmark results overview' > docs/benchmark/index.html 7 | echo '' >> docs/benchmark/index.html 8 | echo '

raw CSV results

' >> docs/benchmark/index.html 9 | marked < benchmark/results.md >> docs/benchmark/index.html 10 | echo '' > docs/benchmark/results/index.html 11 | echo 'benchmark results' >> docs/benchmark/results/index.html 12 | echo '
    ' >> docs/benchmark/results/index.html 13 | ls docs/benchmark/results | while read p; do 14 | f=$(basename "$p") 15 | echo '
  • '$f'
  • ' >> docs/benchmark/results/index.html 16 | done 17 | echo '
' >> docs/benchmark/results/index.html 18 | -------------------------------------------------------------------------------- /scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "heapdump": "^0.3.15" 9 | } 10 | }, 11 | "node_modules/heapdump": { 12 | "version": "0.3.15", 13 | "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz", 14 | "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==", 15 | "hasInstallScript": true, 16 | "dependencies": { 17 | "nan": "^2.13.2" 18 | }, 19 | "engines": { 20 | "node": ">=0.10.0" 21 | } 22 | }, 23 | "node_modules/nan": { 24 | "version": "2.15.0", 25 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", 26 | "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" 27 | } 28 | }, 29 | "dependencies": { 30 | "heapdump": { 31 | "version": "0.3.15", 32 | "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz", 33 | "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==", 34 | "requires": { 35 | "nan": "^2.13.2" 36 | } 37 | }, 38 | "nan": { 39 | "version": "2.15.0", 40 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", 41 | "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "heapdump": "^0.3.15" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/transpile-to-esm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { readFileSync, writeFileSync } = require('fs') 4 | const { resolve } = require('path') 5 | const cjs = readFileSync(resolve(__dirname, '../index.js'), 'utf8') 6 | const esm = cjs.replace(/module.exports\s*=\s*/, 'export default ') 7 | writeFileSync(resolve(__dirname, '../index.mjs'), esm, 'utf8') 8 | -------------------------------------------------------------------------------- /tap-snapshots/test/basic.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/basic.ts > TAP > basic operation > must match snapshot 1`] = ` 9 | Generator [ 10 | Array [ 11 | 4, 12 | 4, 13 | ], 14 | Array [ 15 | 3, 16 | 3, 17 | ], 18 | Array [ 19 | 2, 20 | 2, 21 | ], 22 | Array [ 23 | 1, 24 | 1, 25 | ], 26 | Array [ 27 | 0, 28 | 0, 29 | ], 30 | ] 31 | ` 32 | 33 | exports[`test/basic.ts > TAP > basic operation > must match snapshot 2`] = ` 34 | Generator [ 35 | Array [ 36 | 9, 37 | 9, 38 | ], 39 | Array [ 40 | 8, 41 | 8, 42 | ], 43 | Array [ 44 | 7, 45 | 7, 46 | ], 47 | Array [ 48 | 6, 49 | 6, 50 | ], 51 | Array [ 52 | 5, 53 | 5, 54 | ], 55 | Array [ 56 | 4, 57 | 4, 58 | ], 59 | Array [ 60 | 3, 61 | 3, 62 | ], 63 | Array [ 64 | 2, 65 | 2, 66 | ], 67 | Array [ 68 | 1, 69 | 1, 70 | ], 71 | Array [ 72 | 0, 73 | 0, 74 | ], 75 | ] 76 | ` 77 | 78 | exports[`test/basic.ts > TAP > basic operation > must match snapshot 3`] = ` 79 | Generator [ 80 | Array [ 81 | 4, 82 | 4, 83 | ], 84 | Array [ 85 | 3, 86 | 3, 87 | ], 88 | Array [ 89 | 2, 90 | 2, 91 | ], 92 | Array [ 93 | 1, 94 | 1, 95 | ], 96 | Array [ 97 | 0, 98 | 0, 99 | ], 100 | Array [ 101 | 9, 102 | 9, 103 | ], 104 | Array [ 105 | 8, 106 | 8, 107 | ], 108 | Array [ 109 | 7, 110 | 7, 111 | ], 112 | Array [ 113 | 6, 114 | 6, 115 | ], 116 | Array [ 117 | 5, 118 | 5, 119 | ], 120 | ] 121 | ` 122 | 123 | exports[`test/basic.ts > TAP > basic operation > must match snapshot 4`] = ` 124 | Generator [ 125 | Array [ 126 | 14, 127 | 14, 128 | ], 129 | Array [ 130 | 13, 131 | 13, 132 | ], 133 | Array [ 134 | 12, 135 | 12, 136 | ], 137 | Array [ 138 | 11, 139 | 11, 140 | ], 141 | Array [ 142 | 10, 143 | 10, 144 | ], 145 | Array [ 146 | 9, 147 | 9, 148 | ], 149 | Array [ 150 | 8, 151 | 8, 152 | ], 153 | Array [ 154 | 7, 155 | 7, 156 | ], 157 | Array [ 158 | 6, 159 | 6, 160 | ], 161 | Array [ 162 | 5, 163 | 5, 164 | ], 165 | ] 166 | ` 167 | 168 | exports[`test/basic.ts > TAP > basic operation > must match snapshot 5`] = ` 169 | Generator [ 170 | Array [ 171 | 19, 172 | 19, 173 | ], 174 | Array [ 175 | 18, 176 | 18, 177 | ], 178 | Array [ 179 | 17, 180 | 17, 181 | ], 182 | Array [ 183 | 16, 184 | 16, 185 | ], 186 | Array [ 187 | 15, 188 | 15, 189 | ], 190 | Array [ 191 | 14, 192 | 14, 193 | ], 194 | Array [ 195 | 13, 196 | 13, 197 | ], 198 | Array [ 199 | 12, 200 | 12, 201 | ], 202 | Array [ 203 | 11, 204 | 11, 205 | ], 206 | Array [ 207 | 10, 208 | 10, 209 | ], 210 | ] 211 | ` 212 | 213 | exports[`test/basic.ts > TAP > basic operation > must match snapshot 6`] = ` 214 | Generator [ 215 | Array [ 216 | 19, 217 | 19, 218 | ], 219 | Array [ 220 | 18, 221 | 18, 222 | ], 223 | Array [ 224 | 17, 225 | 17, 226 | ], 227 | Array [ 228 | 16, 229 | 16, 230 | ], 231 | Array [ 232 | 15, 233 | 15, 234 | ], 235 | Array [ 236 | 14, 237 | 14, 238 | ], 239 | Array [ 240 | 13, 241 | 13, 242 | ], 243 | Array [ 244 | 12, 245 | 12, 246 | ], 247 | Array [ 248 | 11, 249 | 11, 250 | ], 251 | Array [ 252 | 10, 253 | 10, 254 | ], 255 | ] 256 | ` 257 | 258 | exports[`test/basic.ts > TAP > basic operation > status tracking 1`] = ` 259 | Array [ 260 | Object { 261 | "set": "add", 262 | }, 263 | Object { 264 | "set": "add", 265 | }, 266 | Object { 267 | "set": "add", 268 | }, 269 | Object { 270 | "set": "add", 271 | }, 272 | Object { 273 | "set": "add", 274 | }, 275 | Object { 276 | "get": "hit", 277 | }, 278 | Object { 279 | "get": "hit", 280 | }, 281 | Object { 282 | "get": "hit", 283 | }, 284 | Object { 285 | "get": "hit", 286 | }, 287 | Object { 288 | "get": "hit", 289 | }, 290 | Object { 291 | "set": "add", 292 | }, 293 | Object { 294 | "set": "add", 295 | }, 296 | Object { 297 | "set": "add", 298 | }, 299 | Object { 300 | "set": "add", 301 | }, 302 | Object { 303 | "set": "add", 304 | }, 305 | Object { 306 | "set": "update", 307 | }, 308 | Object { 309 | "set": "update", 310 | }, 311 | Object { 312 | "set": "update", 313 | }, 314 | Object { 315 | "set": "update", 316 | }, 317 | Object { 318 | "set": "update", 319 | }, 320 | Object { 321 | "get": "hit", 322 | }, 323 | Object { 324 | "get": "hit", 325 | }, 326 | Object { 327 | "get": "hit", 328 | }, 329 | Object { 330 | "get": "hit", 331 | }, 332 | Object { 333 | "get": "hit", 334 | }, 335 | Object { 336 | "get": "hit", 337 | }, 338 | Object { 339 | "get": "hit", 340 | }, 341 | Object { 342 | "get": "hit", 343 | }, 344 | Object { 345 | "get": "hit", 346 | }, 347 | Object { 348 | "get": "hit", 349 | }, 350 | Object { 351 | "set": "add", 352 | }, 353 | Object { 354 | "set": "add", 355 | }, 356 | Object { 357 | "set": "add", 358 | }, 359 | Object { 360 | "set": "add", 361 | }, 362 | Object { 363 | "set": "add", 364 | }, 365 | Object { 366 | "set": "add", 367 | }, 368 | Object { 369 | "set": "add", 370 | }, 371 | Object { 372 | "set": "add", 373 | }, 374 | Object { 375 | "set": "add", 376 | }, 377 | Object { 378 | "set": "add", 379 | }, 380 | Object { 381 | "get": "miss", 382 | }, 383 | Object { 384 | "get": "miss", 385 | }, 386 | Object { 387 | "get": "miss", 388 | }, 389 | Object { 390 | "get": "miss", 391 | }, 392 | Object { 393 | "get": "miss", 394 | }, 395 | Object { 396 | "get": "miss", 397 | }, 398 | Object { 399 | "get": "miss", 400 | }, 401 | Object { 402 | "get": "miss", 403 | }, 404 | Object { 405 | "get": "miss", 406 | }, 407 | Object { 408 | "get": "miss", 409 | }, 410 | Object { 411 | "set": "add", 412 | }, 413 | Object { 414 | "set": "add", 415 | }, 416 | Object { 417 | "set": "add", 418 | }, 419 | Object { 420 | "set": "add", 421 | }, 422 | Object { 423 | "set": "add", 424 | }, 425 | Object { 426 | "set": "add", 427 | }, 428 | Object { 429 | "set": "add", 430 | }, 431 | Object { 432 | "set": "add", 433 | }, 434 | Object { 435 | "set": "add", 436 | }, 437 | Object { 438 | "set": "add", 439 | }, 440 | Object { 441 | "set": "add", 442 | }, 443 | Object { 444 | "has": "hit", 445 | }, 446 | Object { 447 | "set": "add", 448 | }, 449 | Object { 450 | "has": "hit", 451 | }, 452 | Object { 453 | "get": "hit", 454 | }, 455 | Object { 456 | "has": "miss", 457 | }, 458 | ] 459 | ` 460 | 461 | exports[`test/basic.ts > TAP > re-use key before initial fill completed > must match snapshot 1`] = ` 462 | Array [ 463 | Object { 464 | "set": "add", 465 | }, 466 | Object { 467 | "set": "add", 468 | }, 469 | Object { 470 | "set": "add", 471 | }, 472 | Object { 473 | "oldValue": 1, 474 | "set": "replace", 475 | }, 476 | Object { 477 | "set": "add", 478 | }, 479 | ] 480 | ` 481 | -------------------------------------------------------------------------------- /tap-snapshots/test/deprecations.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/deprecations.ts TAP does not do deprecation warning without process object > warnings sent to console.error 1`] = ` 9 | Array [ 10 | Array [ 11 | "The stale option is deprecated. Please use options.allowStale instead.", 12 | "DeprecationWarning", 13 | "LRU_CACHE_OPTION_stale", 14 | Function LRUCache(classLRUCache), 15 | ], 16 | Array [ 17 | "The maxAge option is deprecated. Please use options.ttl instead.", 18 | "DeprecationWarning", 19 | "LRU_CACHE_OPTION_maxAge", 20 | Function LRUCache(classLRUCache), 21 | ], 22 | Array [ 23 | "The length option is deprecated. Please use options.sizeCalculation instead.", 24 | "DeprecationWarning", 25 | "LRU_CACHE_OPTION_length", 26 | Function LRUCache(classLRUCache), 27 | ], 28 | Array [ 29 | "The reset method is deprecated. Please use cache.clear() instead.", 30 | "DeprecationWarning", 31 | "LRU_CACHE_METHOD_reset", 32 | Function get reset(), 33 | ], 34 | Array [ 35 | "The length property is deprecated. Please use cache.size instead.", 36 | "DeprecationWarning", 37 | "LRU_CACHE_PROPERTY_length", 38 | Function get length(), 39 | ], 40 | Array [ 41 | "The prune method is deprecated. Please use cache.purgeStale() instead.", 42 | "DeprecationWarning", 43 | "LRU_CACHE_METHOD_prune", 44 | Function get prune(), 45 | ], 46 | Array [ 47 | "The del method is deprecated. Please use cache.delete() instead.", 48 | "DeprecationWarning", 49 | "LRU_CACHE_METHOD_del", 50 | Function get del(), 51 | ], 52 | ] 53 | ` 54 | 55 | exports[`test/deprecations.ts TAP warns exactly once for a given deprecation > must match snapshot 1`] = ` 56 | Array [ 57 | Array [ 58 | "The stale option is deprecated. Please use options.allowStale instead.", 59 | "DeprecationWarning", 60 | "LRU_CACHE_OPTION_stale", 61 | Function LRUCache(classLRUCache), 62 | ], 63 | Array [ 64 | "The maxAge option is deprecated. Please use options.ttl instead.", 65 | "DeprecationWarning", 66 | "LRU_CACHE_OPTION_maxAge", 67 | Function LRUCache(classLRUCache), 68 | ], 69 | Array [ 70 | "The length option is deprecated. Please use options.sizeCalculation instead.", 71 | "DeprecationWarning", 72 | "LRU_CACHE_OPTION_length", 73 | Function LRUCache(classLRUCache), 74 | ], 75 | Array [ 76 | "The reset method is deprecated. Please use cache.clear() instead.", 77 | "DeprecationWarning", 78 | "LRU_CACHE_METHOD_reset", 79 | Function get reset(), 80 | ], 81 | Array [ 82 | "The length property is deprecated. Please use cache.size instead.", 83 | "DeprecationWarning", 84 | "LRU_CACHE_PROPERTY_length", 85 | Function get length(), 86 | ], 87 | Array [ 88 | "The prune method is deprecated. Please use cache.purgeStale() instead.", 89 | "DeprecationWarning", 90 | "LRU_CACHE_METHOD_prune", 91 | Function get prune(), 92 | ], 93 | Array [ 94 | "The del method is deprecated. Please use cache.delete() instead.", 95 | "DeprecationWarning", 96 | "LRU_CACHE_METHOD_del", 97 | Function get del(), 98 | ], 99 | Array [ 100 | "TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.", 101 | "UnboundedCacheWarning", 102 | "LRU_CACHE_UNBOUNDED", 103 | Function LRUCache(classLRUCache), 104 | ], 105 | ] 106 | ` 107 | -------------------------------------------------------------------------------- /tap-snapshots/test/fetch.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/fetch.ts > TAP > asynchronous fetching > safe to stringify dump 1`] = ` 9 | [["key",{"value":1,"ttl":5,"start":12}]] 10 | ` 11 | 12 | exports[`test/fetch.ts > TAP > asynchronous fetching > status 1 1`] = ` 13 | Object { 14 | "fetch": "miss", 15 | "fetchDispatched": true, 16 | "fetchResolved": true, 17 | "fetchUpdated": true, 18 | "now": 2, 19 | "remainingTTL": 5, 20 | "set": "replace", 21 | "start": 2, 22 | "ttl": 5, 23 | } 24 | ` 25 | 26 | exports[`test/fetch.ts > TAP > asynchronous fetching > status 2 1`] = ` 27 | Object { 28 | "fetch": "hit", 29 | "now": 2, 30 | "remainingTTL": 5, 31 | "start": 2, 32 | "ttl": 5, 33 | } 34 | ` 35 | 36 | exports[`test/fetch.ts > TAP > asynchronous fetching > status 3 1`] = ` 37 | Object { 38 | "fetch": "stale", 39 | "fetchDispatched": true, 40 | "returnedStale": true, 41 | } 42 | ` 43 | 44 | exports[`test/fetch.ts > TAP > asynchronous fetching > status 3.1 1`] = ` 45 | Object { 46 | "fetch": "inflight", 47 | "returnedStale": true, 48 | } 49 | ` 50 | 51 | exports[`test/fetch.ts > TAP > asynchronous fetching > status 4 1`] = ` 52 | Object { 53 | "fetch": "inflight", 54 | } 55 | ` 56 | 57 | exports[`test/fetch.ts > TAP > asynchronous fetching > status 5 1`] = ` 58 | Object { 59 | "fetch": "hit", 60 | "now": 12, 61 | "remainingTTL": 5, 62 | "start": 12, 63 | "ttl": 5, 64 | } 65 | ` 66 | 67 | exports[`test/fetch.ts > TAP > fetch options, signal > status updates 1`] = ` 68 | Array [ 69 | Object { 70 | "fetch": "miss", 71 | "fetchAborted": true, 72 | "fetchDispatched": true, 73 | "fetchError": Error: deleted { 74 | "name": "Error", 75 | }, 76 | }, 77 | Object { 78 | "fetch": "miss", 79 | "fetchAborted": true, 80 | "fetchDispatched": true, 81 | "fetchError": Error: replaced { 82 | "name": "Error", 83 | }, 84 | }, 85 | Object { 86 | "fetch": "miss", 87 | "fetchAborted": true, 88 | "fetchDispatched": true, 89 | "fetchError": Error: evicted { 90 | "name": "Error", 91 | }, 92 | }, 93 | Object { 94 | "now": 722, 95 | "remainingTTL": 100, 96 | "set": "add", 97 | "start": 722, 98 | "ttl": 100, 99 | }, 100 | Object { 101 | "now": 722, 102 | "remainingTTL": 100, 103 | "set": "add", 104 | "start": 722, 105 | "ttl": 100, 106 | }, 107 | Object { 108 | "now": 722, 109 | "remainingTTL": 100, 110 | "set": "add", 111 | "start": 722, 112 | "ttl": 100, 113 | }, 114 | Object { 115 | "fetch": "miss", 116 | "fetchDispatched": true, 117 | "fetchResolved": true, 118 | "fetchUpdated": true, 119 | "now": 722, 120 | "remainingTTL": 1000, 121 | "set": "replace", 122 | "start": 722, 123 | "ttl": 1000, 124 | }, 125 | Object { 126 | "fetch": "miss", 127 | "fetchDispatched": true, 128 | "fetchResolved": true, 129 | "fetchUpdated": true, 130 | "now": 722, 131 | "remainingTTL": 25, 132 | "set": "replace", 133 | "start": 722, 134 | "ttl": 25, 135 | }, 136 | ] 137 | ` 138 | 139 | exports[`test/fetch.ts > TAP > fetch without fetch method > status update 1`] = ` 140 | Object { 141 | "fetch": "get", 142 | "get": "hit", 143 | } 144 | ` 145 | 146 | exports[`test/fetch.ts > TAP > fetchMethod throws > status updates 1`] = ` 147 | Array [ 148 | Object { 149 | "now": 722, 150 | "remainingTTL": 10, 151 | "set": "add", 152 | "start": 722, 153 | "ttl": 10, 154 | }, 155 | Object { 156 | "now": 722, 157 | "remainingTTL": 10, 158 | "set": "add", 159 | "start": 722, 160 | "ttl": 10, 161 | }, 162 | Object { 163 | "fetch": "stale", 164 | "fetchDispatched": true, 165 | "fetchError": Error: fetch failure, 166 | "fetchRejected": true, 167 | "returnedStale": true, 168 | }, 169 | Object { 170 | "fetch": "inflight", 171 | "returnedStale": true, 172 | }, 173 | Object { 174 | "fetch": "inflight", 175 | "returnedStale": true, 176 | }, 177 | Object { 178 | "get": "miss", 179 | }, 180 | Object { 181 | "fetch": "stale", 182 | "fetchDispatched": true, 183 | "fetchError": Error: fetch failure, 184 | "fetchRejected": true, 185 | "returnedStale": true, 186 | }, 187 | Object { 188 | "fetch": "inflight", 189 | "returnedStale": true, 190 | }, 191 | Object { 192 | "fetch": "inflight", 193 | "returnedStale": true, 194 | }, 195 | Object { 196 | "get": "miss", 197 | }, 198 | Object { 199 | "fetch": "miss", 200 | "fetchAborted": true, 201 | "fetchDispatched": true, 202 | "fetchError": Error: replaced { 203 | "name": "Error", 204 | }, 205 | }, 206 | Object { 207 | "now": 782, 208 | "remainingTTL": 10, 209 | "set": "replace", 210 | "start": 782, 211 | "ttl": 10, 212 | }, 213 | Object { 214 | "get": "hit", 215 | "now": 782, 216 | "remainingTTL": 10, 217 | "start": 782, 218 | "ttl": 10, 219 | }, 220 | Object { 221 | "fetch": "miss", 222 | "fetchDispatched": true, 223 | }, 224 | ] 225 | ` 226 | 227 | exports[`test/fetch.ts > TAP > forceRefresh > status updates 1`] = ` 228 | Array [ 229 | Object { 230 | "fetch": "refresh", 231 | "fetchDispatched": true, 232 | "fetchResolved": true, 233 | "fetchUpdated": true, 234 | "now": 942, 235 | "oldValue": 2, 236 | "remainingTTL": 100, 237 | "set": "replace", 238 | "start": 942, 239 | "ttl": 100, 240 | }, 241 | Object { 242 | "fetch": "inflight", 243 | }, 244 | Object { 245 | "fetch": "refresh", 246 | "fetchDispatched": true, 247 | "fetchResolved": true, 248 | "fetchUpdated": true, 249 | "now": 942, 250 | "oldValue": 100, 251 | "remainingTTL": 100, 252 | "set": "replace", 253 | "start": 942, 254 | "ttl": 100, 255 | }, 256 | ] 257 | ` 258 | 259 | exports[`test/fetch.ts > TAP > send a signal > status updates 1`] = ` 260 | Array [ 261 | Object { 262 | "fetch": "miss", 263 | "fetchAborted": true, 264 | "fetchDispatched": true, 265 | "fetchError": Error: custom abort signal { 266 | "name": "Error", 267 | }, 268 | }, 269 | Object { 270 | "get": "miss", 271 | }, 272 | ] 273 | ` 274 | 275 | exports[`test/fetch.ts > TAP > verify inflight works as expected > status updates 1`] = ` 276 | Array [ 277 | Object { 278 | "fetch": "inflight", 279 | }, 280 | Object { 281 | "fetch": "inflight", 282 | }, 283 | Object { 284 | "get": "hit", 285 | }, 286 | ] 287 | ` 288 | -------------------------------------------------------------------------------- /tap-snapshots/test/map-like.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/map-like.ts > TAP > bunch of iteration things > dump 1`] = ` 9 | Array [ 10 | Array [ 11 | 3, 12 | Object { 13 | "size": 1, 14 | "value": "3", 15 | }, 16 | ], 17 | Array [ 18 | 4, 19 | Object { 20 | "size": 1, 21 | "value": "4", 22 | }, 23 | ], 24 | Array [ 25 | 5, 26 | Object { 27 | "size": 1, 28 | "value": "5", 29 | }, 30 | ], 31 | Array [ 32 | 6, 33 | Object { 34 | "size": 1, 35 | "value": "6", 36 | }, 37 | ], 38 | Array [ 39 | 7, 40 | Object { 41 | "size": 1, 42 | "value": "7", 43 | }, 44 | ], 45 | ] 46 | ` 47 | 48 | exports[`test/map-like.ts > TAP > bunch of iteration things > dump, 7 stale 1`] = ` 49 | Array [ 50 | Array [ 51 | 3, 52 | Object { 53 | "size": 1, 54 | "start": 0, 55 | "ttl": 0, 56 | "value": "3", 57 | }, 58 | ], 59 | Array [ 60 | 5, 61 | Object { 62 | "size": 1, 63 | "start": 0, 64 | "ttl": 0, 65 | "value": "5", 66 | }, 67 | ], 68 | Array [ 69 | 6, 70 | Object { 71 | "size": 1, 72 | "start": 0, 73 | "ttl": 0, 74 | "value": "6", 75 | }, 76 | ], 77 | Array [ 78 | 4, 79 | Object { 80 | "size": 1, 81 | "start": 0, 82 | "ttl": 0, 83 | "value": "new value 4", 84 | }, 85 | ], 86 | Array [ 87 | 7, 88 | Object { 89 | "size": 1, 90 | "start": -9999, 91 | "ttl": 1, 92 | "value": "stale", 93 | }, 94 | ], 95 | ] 96 | ` 97 | 98 | exports[`test/map-like.ts > TAP > bunch of iteration things > dump, new value 4 1`] = ` 99 | Array [ 100 | Array [ 101 | 3, 102 | Object { 103 | "size": 1, 104 | "value": "3", 105 | }, 106 | ], 107 | Array [ 108 | 5, 109 | Object { 110 | "size": 1, 111 | "value": "5", 112 | }, 113 | ], 114 | Array [ 115 | 6, 116 | Object { 117 | "size": 1, 118 | "value": "6", 119 | }, 120 | ], 121 | Array [ 122 | 7, 123 | Object { 124 | "size": 1, 125 | "value": "7", 126 | }, 127 | ], 128 | Array [ 129 | 4, 130 | Object { 131 | "size": 1, 132 | "value": "new value 4", 133 | }, 134 | ], 135 | ] 136 | ` 137 | 138 | exports[`test/map-like.ts > TAP > bunch of iteration things > dump, resolved fetch 99 too late 1`] = ` 139 | Array [ 140 | Array [ 141 | 3, 142 | Object { 143 | "size": 1, 144 | "value": "3", 145 | }, 146 | ], 147 | Array [ 148 | 5, 149 | Object { 150 | "size": 1, 151 | "value": "5", 152 | }, 153 | ], 154 | Array [ 155 | 6, 156 | Object { 157 | "size": 1, 158 | "value": "6", 159 | }, 160 | ], 161 | Array [ 162 | 7, 163 | Object { 164 | "size": 1, 165 | "value": "7", 166 | }, 167 | ], 168 | Array [ 169 | 4, 170 | Object { 171 | "size": 1, 172 | "value": "new value 4", 173 | }, 174 | ], 175 | ] 176 | ` 177 | 178 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, dump 1`] = ` 179 | Array [] 180 | ` 181 | 182 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, entries 1`] = ` 183 | Generator [] 184 | ` 185 | 186 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, foreach 1`] = ` 187 | Array [] 188 | ` 189 | 190 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, keys 1`] = ` 191 | Generator [] 192 | ` 193 | 194 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, rentries 1`] = ` 195 | Generator [] 196 | ` 197 | 198 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, rforeach 1`] = ` 199 | Array [] 200 | ` 201 | 202 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, rkeys 1`] = ` 203 | Generator [] 204 | ` 205 | 206 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, rvalues 1`] = ` 207 | Generator [] 208 | ` 209 | 210 | exports[`test/map-like.ts > TAP > bunch of iteration things > empty, values 1`] = ` 211 | Generator [] 212 | ` 213 | 214 | exports[`test/map-like.ts > TAP > bunch of iteration things > entries 1`] = ` 215 | Generator [ 216 | Array [ 217 | 7, 218 | "7", 219 | ], 220 | Array [ 221 | 6, 222 | "6", 223 | ], 224 | Array [ 225 | 5, 226 | "5", 227 | ], 228 | Array [ 229 | 4, 230 | "4", 231 | ], 232 | Array [ 233 | 3, 234 | "3", 235 | ], 236 | ] 237 | ` 238 | 239 | exports[`test/map-like.ts > TAP > bunch of iteration things > entries, 7 stale 1`] = ` 240 | Generator [ 241 | Array [ 242 | 4, 243 | "new value 4", 244 | ], 245 | Array [ 246 | 6, 247 | "6", 248 | ], 249 | Array [ 250 | 5, 251 | "5", 252 | ], 253 | Array [ 254 | 3, 255 | "3", 256 | ], 257 | ] 258 | ` 259 | 260 | exports[`test/map-like.ts > TAP > bunch of iteration things > entries, new value 4 1`] = ` 261 | Generator [ 262 | Array [ 263 | 4, 264 | "new value 4", 265 | ], 266 | Array [ 267 | 7, 268 | "7", 269 | ], 270 | Array [ 271 | 6, 272 | "6", 273 | ], 274 | Array [ 275 | 5, 276 | "5", 277 | ], 278 | Array [ 279 | 3, 280 | "3", 281 | ], 282 | ] 283 | ` 284 | 285 | exports[`test/map-like.ts > TAP > bunch of iteration things > entries, resolved fetch 99 too late 1`] = ` 286 | Generator [ 287 | Array [ 288 | 4, 289 | "new value 4", 290 | ], 291 | Array [ 292 | 7, 293 | "7", 294 | ], 295 | Array [ 296 | 6, 297 | "6", 298 | ], 299 | Array [ 300 | 5, 301 | "5", 302 | ], 303 | Array [ 304 | 3, 305 | "3", 306 | ], 307 | ] 308 | ` 309 | 310 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, dump 1`] = ` 311 | Array [ 312 | Array [ 313 | 0, 314 | Object { 315 | "size": 1, 316 | "value": "0", 317 | }, 318 | ], 319 | Array [ 320 | 1, 321 | Object { 322 | "size": 1, 323 | "value": "1", 324 | }, 325 | ], 326 | Array [ 327 | 2, 328 | Object { 329 | "size": 1, 330 | "value": "2", 331 | }, 332 | ], 333 | Array [ 334 | 123, 335 | Object { 336 | "size": 1, 337 | "value": "123", 338 | }, 339 | ], 340 | ] 341 | ` 342 | 343 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, entries 1`] = ` 344 | Generator [ 345 | Array [ 346 | 123, 347 | "123", 348 | ], 349 | Array [ 350 | 2, 351 | "2", 352 | ], 353 | Array [ 354 | 1, 355 | "1", 356 | ], 357 | Array [ 358 | 0, 359 | "0", 360 | ], 361 | ] 362 | ` 363 | 364 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, foreach 1`] = ` 365 | Array [ 366 | Array [ 367 | 123, 368 | "123", 369 | ], 370 | Array [ 371 | 2, 372 | "2", 373 | ], 374 | Array [ 375 | 1, 376 | "1", 377 | ], 378 | Array [ 379 | 0, 380 | "0", 381 | ], 382 | ] 383 | ` 384 | 385 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, keys 1`] = ` 386 | Generator [ 387 | 123, 388 | 2, 389 | 1, 390 | 0, 391 | ] 392 | ` 393 | 394 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, rentries 1`] = ` 395 | Generator [ 396 | Array [ 397 | 0, 398 | "0", 399 | ], 400 | Array [ 401 | 1, 402 | "1", 403 | ], 404 | Array [ 405 | 2, 406 | "2", 407 | ], 408 | Array [ 409 | 123, 410 | "123", 411 | ], 412 | ] 413 | ` 414 | 415 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, rforeach 1`] = ` 416 | Array [ 417 | Array [ 418 | 0, 419 | "0", 420 | ], 421 | Array [ 422 | 1, 423 | "1", 424 | ], 425 | Array [ 426 | 2, 427 | "2", 428 | ], 429 | Array [ 430 | 123, 431 | "123", 432 | ], 433 | ] 434 | ` 435 | 436 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, rkeys 1`] = ` 437 | Generator [ 438 | 0, 439 | 1, 440 | 2, 441 | 123, 442 | ] 443 | ` 444 | 445 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, rvalues 1`] = ` 446 | Generator [ 447 | "0", 448 | "1", 449 | "2", 450 | "123", 451 | ] 452 | ` 453 | 454 | exports[`test/map-like.ts > TAP > bunch of iteration things > fetch 123 resolved, values 1`] = ` 455 | Generator [ 456 | "123", 457 | "2", 458 | "1", 459 | "0", 460 | ] 461 | ` 462 | 463 | exports[`test/map-like.ts > TAP > bunch of iteration things > forEach, no thisp 1`] = ` 464 | Array [ 465 | Array [ 466 | "new value 4", 467 | 4, 468 | ], 469 | Array [ 470 | "6", 471 | 6, 472 | ], 473 | Array [ 474 | "5", 475 | 5, 476 | ], 477 | Array [ 478 | "3", 479 | 3, 480 | ], 481 | ] 482 | ` 483 | 484 | exports[`test/map-like.ts > TAP > bunch of iteration things > forEach, with thisp 1`] = ` 485 | Array [ 486 | Array [ 487 | "new value 4", 488 | 4, 489 | Object { 490 | "a": 1, 491 | }, 492 | ], 493 | Array [ 494 | "6", 495 | 6, 496 | Object { 497 | "a": 1, 498 | }, 499 | ], 500 | Array [ 501 | "5", 502 | 5, 503 | Object { 504 | "a": 1, 505 | }, 506 | ], 507 | Array [ 508 | "3", 509 | 3, 510 | Object { 511 | "a": 1, 512 | }, 513 | ], 514 | ] 515 | ` 516 | 517 | exports[`test/map-like.ts > TAP > bunch of iteration things > forEach, with thisp 2`] = ` 518 | Array [ 519 | Array [ 520 | "3", 521 | 3, 522 | Object { 523 | "r": 1, 524 | }, 525 | ], 526 | Array [ 527 | "5", 528 | 5, 529 | Object { 530 | "r": 1, 531 | }, 532 | ], 533 | Array [ 534 | "6", 535 | 6, 536 | Object { 537 | "r": 1, 538 | }, 539 | ], 540 | Array [ 541 | "new value 4", 542 | 4, 543 | Object { 544 | "r": 1, 545 | }, 546 | ], 547 | ] 548 | ` 549 | 550 | exports[`test/map-like.ts > TAP > bunch of iteration things > keys 1`] = ` 551 | Generator [ 552 | 7, 553 | 6, 554 | 5, 555 | 4, 556 | 3, 557 | ] 558 | ` 559 | 560 | exports[`test/map-like.ts > TAP > bunch of iteration things > keys, 7 stale 1`] = ` 561 | Generator [ 562 | 4, 563 | 6, 564 | 5, 565 | 3, 566 | ] 567 | ` 568 | 569 | exports[`test/map-like.ts > TAP > bunch of iteration things > keys, new value 4 1`] = ` 570 | Generator [ 571 | 4, 572 | 7, 573 | 6, 574 | 5, 575 | 3, 576 | ] 577 | ` 578 | 579 | exports[`test/map-like.ts > TAP > bunch of iteration things > keys, resolved fetch 99 too late 1`] = ` 580 | Generator [ 581 | 4, 582 | 7, 583 | 6, 584 | 5, 585 | 3, 586 | ] 587 | ` 588 | 589 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, dump 1`] = ` 590 | Array [] 591 | ` 592 | 593 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, entries 1`] = ` 594 | Generator [] 595 | ` 596 | 597 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, foreach 1`] = ` 598 | Array [] 599 | ` 600 | 601 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, keys 1`] = ` 602 | Generator [] 603 | ` 604 | 605 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, rentries 1`] = ` 606 | Generator [] 607 | ` 608 | 609 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, rforeach 1`] = ` 610 | Array [] 611 | ` 612 | 613 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, rkeys 1`] = ` 614 | Generator [] 615 | ` 616 | 617 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, rvalues 1`] = ` 618 | Generator [] 619 | ` 620 | 621 | exports[`test/map-like.ts > TAP > bunch of iteration things > pending fetch, values 1`] = ` 622 | Generator [] 623 | ` 624 | 625 | exports[`test/map-like.ts > TAP > bunch of iteration things > rentries 1`] = ` 626 | Generator [ 627 | Array [ 628 | 3, 629 | "3", 630 | ], 631 | Array [ 632 | 4, 633 | "4", 634 | ], 635 | Array [ 636 | 5, 637 | "5", 638 | ], 639 | Array [ 640 | 6, 641 | "6", 642 | ], 643 | Array [ 644 | 7, 645 | "7", 646 | ], 647 | ] 648 | ` 649 | 650 | exports[`test/map-like.ts > TAP > bunch of iteration things > rentries, 7 stale 1`] = ` 651 | Generator [ 652 | Array [ 653 | 3, 654 | "3", 655 | ], 656 | Array [ 657 | 5, 658 | "5", 659 | ], 660 | Array [ 661 | 6, 662 | "6", 663 | ], 664 | Array [ 665 | 4, 666 | "new value 4", 667 | ], 668 | ] 669 | ` 670 | 671 | exports[`test/map-like.ts > TAP > bunch of iteration things > rentries, new value 4 1`] = ` 672 | Generator [ 673 | Array [ 674 | 3, 675 | "3", 676 | ], 677 | Array [ 678 | 5, 679 | "5", 680 | ], 681 | Array [ 682 | 6, 683 | "6", 684 | ], 685 | Array [ 686 | 7, 687 | "7", 688 | ], 689 | Array [ 690 | 4, 691 | "new value 4", 692 | ], 693 | ] 694 | ` 695 | 696 | exports[`test/map-like.ts > TAP > bunch of iteration things > rentries, resolved fetch 99 too late 1`] = ` 697 | Generator [ 698 | Array [ 699 | 3, 700 | "3", 701 | ], 702 | Array [ 703 | 5, 704 | "5", 705 | ], 706 | Array [ 707 | 6, 708 | "6", 709 | ], 710 | Array [ 711 | 7, 712 | "7", 713 | ], 714 | Array [ 715 | 4, 716 | "new value 4", 717 | ], 718 | ] 719 | ` 720 | 721 | exports[`test/map-like.ts > TAP > bunch of iteration things > rforEach, no thisp 1`] = ` 722 | Array [ 723 | Array [ 724 | "3", 725 | 3, 726 | ], 727 | Array [ 728 | "5", 729 | 5, 730 | ], 731 | Array [ 732 | "6", 733 | 6, 734 | ], 735 | Array [ 736 | "new value 4", 737 | 4, 738 | ], 739 | ] 740 | ` 741 | 742 | exports[`test/map-like.ts > TAP > bunch of iteration things > rkeys 1`] = ` 743 | Generator [ 744 | 3, 745 | 4, 746 | 5, 747 | 6, 748 | 7, 749 | ] 750 | ` 751 | 752 | exports[`test/map-like.ts > TAP > bunch of iteration things > rkeys, 7 stale 1`] = ` 753 | Generator [ 754 | 3, 755 | 5, 756 | 6, 757 | 4, 758 | ] 759 | ` 760 | 761 | exports[`test/map-like.ts > TAP > bunch of iteration things > rkeys, new value 4 1`] = ` 762 | Generator [ 763 | 3, 764 | 5, 765 | 6, 766 | 7, 767 | 4, 768 | ] 769 | ` 770 | 771 | exports[`test/map-like.ts > TAP > bunch of iteration things > rkeys, resolved fetch 99 too late 1`] = ` 772 | Generator [ 773 | 3, 774 | 5, 775 | 6, 776 | 7, 777 | 4, 778 | ] 779 | ` 780 | 781 | exports[`test/map-like.ts > TAP > bunch of iteration things > rvalues 1`] = ` 782 | Generator [ 783 | "3", 784 | "4", 785 | "5", 786 | "6", 787 | "7", 788 | ] 789 | ` 790 | 791 | exports[`test/map-like.ts > TAP > bunch of iteration things > rvalues, 7 stale 1`] = ` 792 | Generator [ 793 | "3", 794 | "5", 795 | "6", 796 | "new value 4", 797 | ] 798 | ` 799 | 800 | exports[`test/map-like.ts > TAP > bunch of iteration things > rvalues, new value 4 1`] = ` 801 | Generator [ 802 | "3", 803 | "5", 804 | "6", 805 | "7", 806 | "new value 4", 807 | ] 808 | ` 809 | 810 | exports[`test/map-like.ts > TAP > bunch of iteration things > rvalues, resolved fetch 99 too late 1`] = ` 811 | Generator [ 812 | "3", 813 | "5", 814 | "6", 815 | "7", 816 | "new value 4", 817 | ] 818 | ` 819 | 820 | exports[`test/map-like.ts > TAP > bunch of iteration things > values 1`] = ` 821 | Generator [ 822 | "7", 823 | "6", 824 | "5", 825 | "4", 826 | "3", 827 | ] 828 | ` 829 | 830 | exports[`test/map-like.ts > TAP > bunch of iteration things > values, 7 stale 1`] = ` 831 | Generator [ 832 | "new value 4", 833 | "6", 834 | "5", 835 | "3", 836 | ] 837 | ` 838 | 839 | exports[`test/map-like.ts > TAP > bunch of iteration things > values, new value 4 1`] = ` 840 | Generator [ 841 | "new value 4", 842 | "7", 843 | "6", 844 | "5", 845 | "3", 846 | ] 847 | ` 848 | 849 | exports[`test/map-like.ts > TAP > bunch of iteration things > values, resolved fetch 99 too late 1`] = ` 850 | Generator [ 851 | "new value 4", 852 | "7", 853 | "6", 854 | "5", 855 | "3", 856 | ] 857 | ` 858 | -------------------------------------------------------------------------------- /tap-snapshots/test/move-to-tail.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/move-to-tail.ts > TAP > list integrity > list after initial fill 1`] = ` 9 | Array [ 10 | Object { 11 | "_": "H", 12 | "head": 0, 13 | "index": 0, 14 | "next": 1, 15 | "prev": 0, 16 | "tail": 4, 17 | }, 18 | Object { 19 | "_": "1", 20 | "head": 0, 21 | "index": 1, 22 | "next": 2, 23 | "prev": 0, 24 | "tail": 4, 25 | }, 26 | Object { 27 | "_": "2", 28 | "head": 0, 29 | "index": 2, 30 | "next": 3, 31 | "prev": 1, 32 | "tail": 4, 33 | }, 34 | Object { 35 | "_": "3", 36 | "head": 0, 37 | "index": 3, 38 | "next": 4, 39 | "prev": 2, 40 | "tail": 4, 41 | }, 42 | Object { 43 | "_": "T", 44 | "head": 0, 45 | "index": 4, 46 | "next": 0, 47 | "prev": 3, 48 | "tail": 4, 49 | }, 50 | ] 51 | ` 52 | 53 | exports[`test/move-to-tail.ts > TAP > list integrity > list after moveToTail 2 1`] = ` 54 | Array [ 55 | Object { 56 | "_": "H", 57 | "head": 0, 58 | "index": 0, 59 | "next": 1, 60 | "prev": 0, 61 | "tail": 2, 62 | }, 63 | Object { 64 | "_": "1", 65 | "head": 0, 66 | "index": 1, 67 | "next": 3, 68 | "prev": 0, 69 | "tail": 2, 70 | }, 71 | Object { 72 | "_": "T", 73 | "head": 0, 74 | "index": 2, 75 | "next": 3, 76 | "prev": 4, 77 | "tail": 2, 78 | }, 79 | Object { 80 | "_": "3", 81 | "head": 0, 82 | "index": 3, 83 | "next": 4, 84 | "prev": 1, 85 | "tail": 2, 86 | }, 87 | Object { 88 | "_": "4", 89 | "head": 0, 90 | "index": 4, 91 | "next": 2, 92 | "prev": 3, 93 | "tail": 2, 94 | }, 95 | ] 96 | ` 97 | 98 | exports[`test/move-to-tail.ts > TAP > list integrity > list after moveToTail 4 1`] = ` 99 | Array [ 100 | Object { 101 | "_": "H", 102 | "head": 0, 103 | "index": 0, 104 | "next": 1, 105 | "prev": 0, 106 | "tail": 4, 107 | }, 108 | Object { 109 | "_": "1", 110 | "head": 0, 111 | "index": 1, 112 | "next": 3, 113 | "prev": 0, 114 | "tail": 4, 115 | }, 116 | Object { 117 | "_": "2", 118 | "head": 0, 119 | "index": 2, 120 | "next": 4, 121 | "prev": 3, 122 | "tail": 4, 123 | }, 124 | Object { 125 | "_": "3", 126 | "head": 0, 127 | "index": 3, 128 | "next": 2, 129 | "prev": 1, 130 | "tail": 4, 131 | }, 132 | Object { 133 | "_": "T", 134 | "head": 0, 135 | "index": 4, 136 | "next": 2, 137 | "prev": 2, 138 | "tail": 4, 139 | }, 140 | ] 141 | ` 142 | -------------------------------------------------------------------------------- /tap-snapshots/test/size-calculation.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/size-calculation.ts > TAP > large item falls out of cache because maxEntrySize > status updates 1`] = ` 9 | Array [ 10 | Object { 11 | "entrySize": 2, 12 | "set": "add", 13 | "totalCalculatedSize": 2, 14 | }, 15 | Object { 16 | "maxEntrySizeExceeded": true, 17 | "set": "miss", 18 | }, 19 | Object { 20 | "entrySize": 3, 21 | "set": "add", 22 | "totalCalculatedSize": 3, 23 | }, 24 | Object { 25 | "maxEntrySizeExceeded": true, 26 | "set": "miss", 27 | }, 28 | ] 29 | ` 30 | 31 | exports[`test/size-calculation.ts > TAP > large item falls out of cache, sizes are kept correct > status updates 1`] = ` 32 | Array [ 33 | Object { 34 | "entrySize": 2, 35 | "set": "add", 36 | "totalCalculatedSize": 2, 37 | }, 38 | Object { 39 | "maxEntrySizeExceeded": true, 40 | "set": "miss", 41 | }, 42 | Object { 43 | "entrySize": 3, 44 | "set": "add", 45 | "totalCalculatedSize": 3, 46 | }, 47 | Object { 48 | "maxEntrySizeExceeded": true, 49 | "set": "miss", 50 | }, 51 | ] 52 | ` 53 | 54 | exports[`test/size-calculation.ts > TAP > store strings, size = length > dump 1`] = ` 55 | Array [ 56 | Array [ 57 | "repeated", 58 | Object { 59 | "size": 10, 60 | "value": "jjjjjjjjjj", 61 | }, 62 | ], 63 | ] 64 | ` 65 | -------------------------------------------------------------------------------- /tap-snapshots/test/ttl.ts.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/ttl.ts > TAP > tests using Date.now() > set item pre-stale > dump with stale values 1`] = ` 9 | Array [ 10 | Array [ 11 | 1, 12 | Object { 13 | "start": 3022, 14 | "ttl": 10, 15 | "value": 1, 16 | }, 17 | ], 18 | Array [ 19 | 2, 20 | Object { 21 | "start": 3011, 22 | "ttl": 10, 23 | "value": 2, 24 | }, 25 | ], 26 | ] 27 | ` 28 | 29 | exports[`test/ttl.ts > TAP > tests using Date.now() > ttl on set, not on cache > status updates 1`] = ` 30 | Array [ 31 | Object { 32 | "now": 1965, 33 | "remainingTTL": 10, 34 | "set": "add", 35 | "start": 1965, 36 | "ttl": 10, 37 | }, 38 | Object { 39 | "get": "hit", 40 | "now": 1965, 41 | "remainingTTL": 10, 42 | "start": 1965, 43 | "ttl": 10, 44 | }, 45 | Object { 46 | "get": "hit", 47 | "now": 1970, 48 | "remainingTTL": 5, 49 | "start": 1965, 50 | "ttl": 10, 51 | }, 52 | Object { 53 | "get": "hit", 54 | "now": 1975, 55 | "remainingTTL": 0, 56 | "start": 1965, 57 | "ttl": 10, 58 | }, 59 | Object { 60 | "has": "stale", 61 | "now": 1976, 62 | "remainingTTL": -1, 63 | "start": 1965, 64 | "ttl": 10, 65 | }, 66 | Object { 67 | "get": "stale", 68 | "now": 1976, 69 | "remainingTTL": -1, 70 | "start": 1965, 71 | "ttl": 10, 72 | }, 73 | Object { 74 | "now": 1976, 75 | "remainingTTL": 100, 76 | "set": "add", 77 | "start": 1976, 78 | "ttl": 100, 79 | }, 80 | Object { 81 | "has": "hit", 82 | "now": 2026, 83 | "remainingTTL": 50, 84 | "start": 1976, 85 | "ttl": 100, 86 | }, 87 | Object { 88 | "get": "hit", 89 | "now": 2026, 90 | "remainingTTL": 50, 91 | "start": 1976, 92 | "ttl": 100, 93 | }, 94 | Object { 95 | "has": "stale", 96 | "now": 2077, 97 | "remainingTTL": -1, 98 | "start": 1976, 99 | "ttl": 100, 100 | }, 101 | Object { 102 | "get": "stale", 103 | "now": 2077, 104 | "remainingTTL": -1, 105 | "start": 1976, 106 | "ttl": 100, 107 | }, 108 | Object { 109 | "now": 2077, 110 | "remainingTTL": 10, 111 | "set": "add", 112 | "start": 2077, 113 | "ttl": 10, 114 | }, 115 | Object { 116 | "now": 2077, 117 | "remainingTTL": 10, 118 | "set": "add", 119 | "start": 2077, 120 | "ttl": 10, 121 | }, 122 | Object { 123 | "now": 2077, 124 | "remainingTTL": 10, 125 | "set": "add", 126 | "start": 2077, 127 | "ttl": 10, 128 | }, 129 | Object { 130 | "now": 2077, 131 | "remainingTTL": 10, 132 | "set": "add", 133 | "start": 2077, 134 | "ttl": 10, 135 | }, 136 | Object { 137 | "now": 2077, 138 | "remainingTTL": 10, 139 | "set": "add", 140 | "start": 2077, 141 | "ttl": 10, 142 | }, 143 | Object { 144 | "now": 2077, 145 | "remainingTTL": 10, 146 | "set": "add", 147 | "start": 2077, 148 | "ttl": 10, 149 | }, 150 | Object { 151 | "now": 2077, 152 | "remainingTTL": 10, 153 | "set": "add", 154 | "start": 2077, 155 | "ttl": 10, 156 | }, 157 | Object { 158 | "now": 2077, 159 | "remainingTTL": 10, 160 | "set": "add", 161 | "start": 2077, 162 | "ttl": 10, 163 | }, 164 | Object { 165 | "now": 2077, 166 | "remainingTTL": 10, 167 | "set": "add", 168 | "start": 2077, 169 | "ttl": 10, 170 | }, 171 | Object { 172 | "has": "stale", 173 | "now": 2088, 174 | "remainingTTL": -1, 175 | "start": 2077, 176 | "ttl": 10, 177 | }, 178 | Object { 179 | "get": "stale", 180 | "now": 2088, 181 | "remainingTTL": -1, 182 | "start": 2077, 183 | "ttl": 10, 184 | }, 185 | ] 186 | ` 187 | 188 | exports[`test/ttl.ts > TAP > tests using Date.now() > ttl tests defaults > status updates 1`] = ` 189 | Array [ 190 | Object { 191 | "now": 1518, 192 | "remainingTTL": 10, 193 | "set": "add", 194 | "start": 1518, 195 | "ttl": 10, 196 | }, 197 | Object { 198 | "get": "hit", 199 | "now": 1518, 200 | "remainingTTL": 10, 201 | "start": 1518, 202 | "ttl": 10, 203 | }, 204 | Object { 205 | "get": "hit", 206 | "now": 1523, 207 | "remainingTTL": 5, 208 | "start": 1518, 209 | "ttl": 10, 210 | }, 211 | Object { 212 | "get": "hit", 213 | "now": 1528, 214 | "remainingTTL": 0, 215 | "start": 1518, 216 | "ttl": 10, 217 | }, 218 | Object { 219 | "has": "stale", 220 | "now": 1530, 221 | "remainingTTL": -2, 222 | "start": 1518, 223 | "ttl": 10, 224 | }, 225 | Object { 226 | "get": "stale", 227 | "now": 1530, 228 | "remainingTTL": -2, 229 | "start": 1518, 230 | "ttl": 10, 231 | }, 232 | Object { 233 | "has": "hit", 234 | "now": 1580, 235 | "remainingTTL": 50, 236 | "start": 1530, 237 | "ttl": 100, 238 | }, 239 | Object { 240 | "get": "hit", 241 | "now": 1580, 242 | "remainingTTL": 50, 243 | "start": 1530, 244 | "ttl": 100, 245 | }, 246 | Object { 247 | "get": "stale", 248 | "now": 1631, 249 | "remainingTTL": -1, 250 | "start": 1530, 251 | "ttl": 100, 252 | }, 253 | Object { 254 | "now": 1631, 255 | "remainingTTL": 10, 256 | "set": "add", 257 | "start": 1631, 258 | "ttl": 10, 259 | }, 260 | Object { 261 | "now": 1631, 262 | "remainingTTL": 10, 263 | "set": "add", 264 | "start": 1631, 265 | "ttl": 10, 266 | }, 267 | Object { 268 | "now": 1631, 269 | "remainingTTL": 10, 270 | "set": "add", 271 | "start": 1631, 272 | "ttl": 10, 273 | }, 274 | Object { 275 | "now": 1631, 276 | "remainingTTL": 10, 277 | "set": "add", 278 | "start": 1631, 279 | "ttl": 10, 280 | }, 281 | Object { 282 | "now": 1631, 283 | "remainingTTL": 10, 284 | "set": "add", 285 | "start": 1631, 286 | "ttl": 10, 287 | }, 288 | Object { 289 | "now": 1631, 290 | "remainingTTL": 10, 291 | "set": "add", 292 | "start": 1631, 293 | "ttl": 10, 294 | }, 295 | Object { 296 | "now": 1631, 297 | "remainingTTL": 10, 298 | "set": "add", 299 | "start": 1631, 300 | "ttl": 10, 301 | }, 302 | Object { 303 | "now": 1631, 304 | "remainingTTL": 10, 305 | "set": "add", 306 | "start": 1631, 307 | "ttl": 10, 308 | }, 309 | Object { 310 | "now": 1631, 311 | "remainingTTL": 10, 312 | "set": "add", 313 | "start": 1631, 314 | "ttl": 10, 315 | }, 316 | Object { 317 | "has": "stale", 318 | "now": 1642, 319 | "remainingTTL": -1, 320 | "start": 1631, 321 | "ttl": 10, 322 | }, 323 | Object { 324 | "get": "stale", 325 | "now": 1642, 326 | "remainingTTL": -1, 327 | "start": 1631, 328 | "ttl": 10, 329 | }, 330 | Object { 331 | "get": "hit", 332 | }, 333 | Object { 334 | "get": "hit", 335 | }, 336 | ] 337 | ` 338 | 339 | exports[`test/ttl.ts > TAP > tests using Date.now() > ttl tests with ttlResolution=100 > status updates 1`] = ` 340 | Array [ 341 | Object { 342 | "now": 1842, 343 | "remainingTTL": 10, 344 | "set": "add", 345 | "start": 1842, 346 | "ttl": 10, 347 | }, 348 | Object { 349 | "get": "hit", 350 | "now": 1842, 351 | "remainingTTL": 10, 352 | "start": 1842, 353 | "ttl": 10, 354 | }, 355 | Object { 356 | "get": "hit", 357 | "now": 1842, 358 | "remainingTTL": 10, 359 | "start": 1842, 360 | "ttl": 10, 361 | }, 362 | Object { 363 | "get": "hit", 364 | "now": 1842, 365 | "remainingTTL": 10, 366 | "start": 1842, 367 | "ttl": 10, 368 | }, 369 | Object { 370 | "has": "hit", 371 | "now": 1842, 372 | "remainingTTL": 10, 373 | "start": 1842, 374 | "ttl": 10, 375 | }, 376 | Object { 377 | "get": "hit", 378 | "now": 1842, 379 | "remainingTTL": 10, 380 | "start": 1842, 381 | "ttl": 10, 382 | }, 383 | Object { 384 | "has": "stale", 385 | "now": 1953, 386 | "remainingTTL": -101, 387 | "start": 1842, 388 | "ttl": 10, 389 | }, 390 | Object { 391 | "get": "stale", 392 | "now": 1953, 393 | "remainingTTL": -101, 394 | "start": 1842, 395 | "ttl": 10, 396 | }, 397 | ] 398 | ` 399 | 400 | exports[`test/ttl.ts > TAP > tests using Date.now() > ttlAutopurge > status updates 1`] = ` 401 | Array [ 402 | Object { 403 | "now": 1953, 404 | "remainingTTL": 10, 405 | "set": "add", 406 | "start": 1953, 407 | "ttl": 10, 408 | }, 409 | Object { 410 | "now": 1953, 411 | "remainingTTL": 10, 412 | "set": "add", 413 | "start": 1953, 414 | "ttl": 10, 415 | }, 416 | Object { 417 | "now": 1953, 418 | "oldValue": 2, 419 | "remainingTTL": 11, 420 | "set": "replace", 421 | "start": 1953, 422 | "ttl": 11, 423 | }, 424 | ] 425 | ` 426 | 427 | exports[`test/ttl.ts > TAP > tests with perf_hooks.performance.now() > set item pre-stale > dump with stale values 1`] = ` 428 | Array [ 429 | Array [ 430 | 1, 431 | Object { 432 | "start": 1506, 433 | "ttl": 10, 434 | "value": 1, 435 | }, 436 | ], 437 | Array [ 438 | 2, 439 | Object { 440 | "start": 1495, 441 | "ttl": 10, 442 | "value": 2, 443 | }, 444 | ], 445 | ] 446 | ` 447 | 448 | exports[`test/ttl.ts > TAP > tests with perf_hooks.performance.now() > ttl on set, not on cache > status updates 1`] = ` 449 | Array [ 450 | Object { 451 | "now": 449, 452 | "remainingTTL": 10, 453 | "set": "add", 454 | "start": 449, 455 | "ttl": 10, 456 | }, 457 | Object { 458 | "get": "hit", 459 | "now": 449, 460 | "remainingTTL": 10, 461 | "start": 449, 462 | "ttl": 10, 463 | }, 464 | Object { 465 | "get": "hit", 466 | "now": 454, 467 | "remainingTTL": 5, 468 | "start": 449, 469 | "ttl": 10, 470 | }, 471 | Object { 472 | "get": "hit", 473 | "now": 459, 474 | "remainingTTL": 0, 475 | "start": 449, 476 | "ttl": 10, 477 | }, 478 | Object { 479 | "has": "stale", 480 | "now": 460, 481 | "remainingTTL": -1, 482 | "start": 449, 483 | "ttl": 10, 484 | }, 485 | Object { 486 | "get": "stale", 487 | "now": 460, 488 | "remainingTTL": -1, 489 | "start": 449, 490 | "ttl": 10, 491 | }, 492 | Object { 493 | "now": 460, 494 | "remainingTTL": 100, 495 | "set": "add", 496 | "start": 460, 497 | "ttl": 100, 498 | }, 499 | Object { 500 | "has": "hit", 501 | "now": 510, 502 | "remainingTTL": 50, 503 | "start": 460, 504 | "ttl": 100, 505 | }, 506 | Object { 507 | "get": "hit", 508 | "now": 510, 509 | "remainingTTL": 50, 510 | "start": 460, 511 | "ttl": 100, 512 | }, 513 | Object { 514 | "has": "stale", 515 | "now": 561, 516 | "remainingTTL": -1, 517 | "start": 460, 518 | "ttl": 100, 519 | }, 520 | Object { 521 | "get": "stale", 522 | "now": 561, 523 | "remainingTTL": -1, 524 | "start": 460, 525 | "ttl": 100, 526 | }, 527 | Object { 528 | "now": 561, 529 | "remainingTTL": 10, 530 | "set": "add", 531 | "start": 561, 532 | "ttl": 10, 533 | }, 534 | Object { 535 | "now": 561, 536 | "remainingTTL": 10, 537 | "set": "add", 538 | "start": 561, 539 | "ttl": 10, 540 | }, 541 | Object { 542 | "now": 561, 543 | "remainingTTL": 10, 544 | "set": "add", 545 | "start": 561, 546 | "ttl": 10, 547 | }, 548 | Object { 549 | "now": 561, 550 | "remainingTTL": 10, 551 | "set": "add", 552 | "start": 561, 553 | "ttl": 10, 554 | }, 555 | Object { 556 | "now": 561, 557 | "remainingTTL": 10, 558 | "set": "add", 559 | "start": 561, 560 | "ttl": 10, 561 | }, 562 | Object { 563 | "now": 561, 564 | "remainingTTL": 10, 565 | "set": "add", 566 | "start": 561, 567 | "ttl": 10, 568 | }, 569 | Object { 570 | "now": 561, 571 | "remainingTTL": 10, 572 | "set": "add", 573 | "start": 561, 574 | "ttl": 10, 575 | }, 576 | Object { 577 | "now": 561, 578 | "remainingTTL": 10, 579 | "set": "add", 580 | "start": 561, 581 | "ttl": 10, 582 | }, 583 | Object { 584 | "now": 561, 585 | "remainingTTL": 10, 586 | "set": "add", 587 | "start": 561, 588 | "ttl": 10, 589 | }, 590 | Object { 591 | "has": "stale", 592 | "now": 572, 593 | "remainingTTL": -1, 594 | "start": 561, 595 | "ttl": 10, 596 | }, 597 | Object { 598 | "get": "stale", 599 | "now": 572, 600 | "remainingTTL": -1, 601 | "start": 561, 602 | "ttl": 10, 603 | }, 604 | ] 605 | ` 606 | 607 | exports[`test/ttl.ts > TAP > tests with perf_hooks.performance.now() > ttl tests defaults > status updates 1`] = ` 608 | Array [ 609 | Object { 610 | "now": 2, 611 | "remainingTTL": 10, 612 | "set": "add", 613 | "start": 2, 614 | "ttl": 10, 615 | }, 616 | Object { 617 | "get": "hit", 618 | "now": 2, 619 | "remainingTTL": 10, 620 | "start": 2, 621 | "ttl": 10, 622 | }, 623 | Object { 624 | "get": "hit", 625 | "now": 7, 626 | "remainingTTL": 5, 627 | "start": 2, 628 | "ttl": 10, 629 | }, 630 | Object { 631 | "get": "hit", 632 | "now": 12, 633 | "remainingTTL": 0, 634 | "start": 2, 635 | "ttl": 10, 636 | }, 637 | Object { 638 | "has": "stale", 639 | "now": 14, 640 | "remainingTTL": -2, 641 | "start": 2, 642 | "ttl": 10, 643 | }, 644 | Object { 645 | "get": "stale", 646 | "now": 14, 647 | "remainingTTL": -2, 648 | "start": 2, 649 | "ttl": 10, 650 | }, 651 | Object { 652 | "has": "hit", 653 | "now": 64, 654 | "remainingTTL": 50, 655 | "start": 14, 656 | "ttl": 100, 657 | }, 658 | Object { 659 | "get": "hit", 660 | "now": 64, 661 | "remainingTTL": 50, 662 | "start": 14, 663 | "ttl": 100, 664 | }, 665 | Object { 666 | "get": "stale", 667 | "now": 115, 668 | "remainingTTL": -1, 669 | "start": 14, 670 | "ttl": 100, 671 | }, 672 | Object { 673 | "now": 115, 674 | "remainingTTL": 10, 675 | "set": "add", 676 | "start": 115, 677 | "ttl": 10, 678 | }, 679 | Object { 680 | "now": 115, 681 | "remainingTTL": 10, 682 | "set": "add", 683 | "start": 115, 684 | "ttl": 10, 685 | }, 686 | Object { 687 | "now": 115, 688 | "remainingTTL": 10, 689 | "set": "add", 690 | "start": 115, 691 | "ttl": 10, 692 | }, 693 | Object { 694 | "now": 115, 695 | "remainingTTL": 10, 696 | "set": "add", 697 | "start": 115, 698 | "ttl": 10, 699 | }, 700 | Object { 701 | "now": 115, 702 | "remainingTTL": 10, 703 | "set": "add", 704 | "start": 115, 705 | "ttl": 10, 706 | }, 707 | Object { 708 | "now": 115, 709 | "remainingTTL": 10, 710 | "set": "add", 711 | "start": 115, 712 | "ttl": 10, 713 | }, 714 | Object { 715 | "now": 115, 716 | "remainingTTL": 10, 717 | "set": "add", 718 | "start": 115, 719 | "ttl": 10, 720 | }, 721 | Object { 722 | "now": 115, 723 | "remainingTTL": 10, 724 | "set": "add", 725 | "start": 115, 726 | "ttl": 10, 727 | }, 728 | Object { 729 | "now": 115, 730 | "remainingTTL": 10, 731 | "set": "add", 732 | "start": 115, 733 | "ttl": 10, 734 | }, 735 | Object { 736 | "has": "stale", 737 | "now": 126, 738 | "remainingTTL": -1, 739 | "start": 115, 740 | "ttl": 10, 741 | }, 742 | Object { 743 | "get": "stale", 744 | "now": 126, 745 | "remainingTTL": -1, 746 | "start": 115, 747 | "ttl": 10, 748 | }, 749 | Object { 750 | "get": "hit", 751 | }, 752 | Object { 753 | "get": "hit", 754 | }, 755 | ] 756 | ` 757 | 758 | exports[`test/ttl.ts > TAP > tests with perf_hooks.performance.now() > ttl tests with ttlResolution=100 > status updates 1`] = ` 759 | Array [ 760 | Object { 761 | "now": 326, 762 | "remainingTTL": 10, 763 | "set": "add", 764 | "start": 326, 765 | "ttl": 10, 766 | }, 767 | Object { 768 | "get": "hit", 769 | "now": 326, 770 | "remainingTTL": 10, 771 | "start": 326, 772 | "ttl": 10, 773 | }, 774 | Object { 775 | "get": "hit", 776 | "now": 326, 777 | "remainingTTL": 10, 778 | "start": 326, 779 | "ttl": 10, 780 | }, 781 | Object { 782 | "get": "hit", 783 | "now": 326, 784 | "remainingTTL": 10, 785 | "start": 326, 786 | "ttl": 10, 787 | }, 788 | Object { 789 | "has": "hit", 790 | "now": 326, 791 | "remainingTTL": 10, 792 | "start": 326, 793 | "ttl": 10, 794 | }, 795 | Object { 796 | "get": "hit", 797 | "now": 326, 798 | "remainingTTL": 10, 799 | "start": 326, 800 | "ttl": 10, 801 | }, 802 | Object { 803 | "has": "stale", 804 | "now": 437, 805 | "remainingTTL": -101, 806 | "start": 326, 807 | "ttl": 10, 808 | }, 809 | Object { 810 | "get": "stale", 811 | "now": 437, 812 | "remainingTTL": -101, 813 | "start": 326, 814 | "ttl": 10, 815 | }, 816 | ] 817 | ` 818 | 819 | exports[`test/ttl.ts > TAP > tests with perf_hooks.performance.now() > ttlAutopurge > status updates 1`] = ` 820 | Array [ 821 | Object { 822 | "now": 437, 823 | "remainingTTL": 10, 824 | "set": "add", 825 | "start": 437, 826 | "ttl": 10, 827 | }, 828 | Object { 829 | "now": 437, 830 | "remainingTTL": 10, 831 | "set": "add", 832 | "start": 437, 833 | "ttl": 10, 834 | }, 835 | Object { 836 | "now": 437, 837 | "oldValue": 2, 838 | "remainingTTL": 11, 839 | "set": "replace", 840 | "start": 437, 841 | "ttl": 11, 842 | }, 843 | ] 844 | ` 845 | -------------------------------------------------------------------------------- /test/avoid-memory-leak.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --no-warnings --loader=ts-node/esm --expose-gc 2 | 3 | // https://github.com/isaacs/node-lru-cache/issues/227 4 | 5 | import t, { Test } from 'tap' 6 | import { expose } from './fixtures/expose.js' 7 | 8 | const maxSize = 100_000 9 | const itemSize = 1_000 10 | const profEvery = 10_000 11 | const n = 1_000_000 12 | 13 | if (typeof gc !== 'function') { 14 | t.plan(0, 'run with --expose-gc') 15 | process.exit(0) 16 | } 17 | 18 | const req = createRequire(import.meta.url) 19 | const tryReq = (mod: string) => { 20 | try { 21 | return req(mod) 22 | } catch (er) { 23 | t.plan(0, `need ${mod} module`) 24 | process.exit(0) 25 | } 26 | } 27 | 28 | const v8 = tryReq('v8') 29 | 30 | import { LRUCache } from '../dist/esm/index.js' 31 | import { createRequire } from 'module' 32 | const expectItemCount = Math.ceil(maxSize / itemSize) 33 | const max = expectItemCount + 1 34 | const keyRange = expectItemCount * 2 35 | 36 | // fine to alloc unsafe, we don't ever look at the data 37 | const makeItem = () => Buffer.allocUnsafe(itemSize) 38 | 39 | const prof = (i: number, cache: LRUCache) => { 40 | // run gc so that we know if we're actually leaking memory, or just 41 | // that the gc is being lazy and not responding until there's memory 42 | // pressure. 43 | // @ts-ignore 44 | gc() 45 | return { 46 | i, 47 | ...v8.getHeapStatistics(), 48 | valListLength: expose(cache).valList.length, 49 | freeLength: expose(cache).free.length, 50 | } 51 | } 52 | 53 | const runTest = async (t: Test, cache: LRUCache) => { 54 | // first, fill to expected size 55 | for (let i = 0; i < expectItemCount; i++) { 56 | cache.set(i, makeItem()) 57 | } 58 | 59 | // now start the setting and profiling 60 | const profiles: ReturnType[] = [] 61 | for (let i = 0; i < n; i++) { 62 | if (i % profEvery === 0) { 63 | const profile = prof(i, cache) 64 | t.ok( 65 | profile.valListLength <= max, 66 | `expect valList to have fewer than ${max} items`, 67 | { found: profile.valListLength } 68 | ) 69 | t.ok( 70 | profile.freeLength <= 1, 71 | 'expect free stack to have <= 1 item', 72 | { found: profile.freeLength } 73 | ) 74 | t.ok( 75 | profile.number_of_native_contexts <= 2, 76 | 'expect only 1 or 2 native contexts', 77 | { found: profile.number_of_native_contexts } 78 | ) 79 | t.equal( 80 | profile.number_of_detached_contexts, 81 | 0, 82 | '0 detached contexts' 83 | ) 84 | profiles.push(profile) 85 | } 86 | 87 | const item = makeItem() 88 | cache.set(i % keyRange, item) 89 | } 90 | 91 | const profile = prof(n, cache) 92 | profiles.push(profile) 93 | 94 | // Warning: kludgey inexact test! 95 | // memory leaks can be hard to catch deterministically. 96 | // The first few items will tend to be lower, and we'll see 97 | // *some* modest increase in heap usage from tap itself as it 98 | // runs the test and builds up its internal results data. 99 | // But, after the initial few profiles, it should be modest. 100 | // Considering that the reported bug showed a 10x increase in 101 | // memory in this reproduction case, 2x is still pretty aggressive, 102 | // without risking false hits from other node or tap stuff. 103 | 104 | const start = Math.floor(profiles.length / 2) 105 | const initial = profiles[start] 106 | for (let i = start; i < profiles.length; i++) { 107 | const current = profiles[i] 108 | const delta = current.total_heap_size / initial.total_heap_size 109 | t.ok(delta < 2, 'memory growth should not be unbounded', { 110 | delta, 111 | current, 112 | initial, 113 | }) 114 | } 115 | } 116 | 117 | t.test('both max and maxSize', t => 118 | runTest( 119 | t, 120 | new LRUCache({ 121 | maxSize, 122 | sizeCalculation: s => s.length, 123 | max, 124 | }) 125 | ) 126 | ) 127 | 128 | t.test('no max, only maxSize', t => 129 | runTest( 130 | t, 131 | new LRUCache({ 132 | maxSize, 133 | sizeCalculation: s => s.length, 134 | }) 135 | ) 136 | ) 137 | 138 | t.test('only max, no maxSize', t => runTest(t, new LRUCache({ max }))) 139 | -------------------------------------------------------------------------------- /test/basic.ts: -------------------------------------------------------------------------------- 1 | if (typeof performance === 'undefined') { 2 | global.performance = require('perf_hooks').performance 3 | } 4 | 5 | import { createRequire } from 'module' 6 | import t from 'tap' 7 | import { LRUCache as LRU } from '../dist/esm/index.js' 8 | import { expose } from './fixtures/expose.js' 9 | 10 | t.test('verify require works as expected', async t => { 11 | const req = createRequire(import.meta.url) 12 | t.equal( 13 | req.resolve('../'), 14 | req.resolve('../dist/commonjs/index.js'), 15 | 'require resolves to expected module' 16 | ) 17 | const { LRUCache } = await t.mockImport('../dist/esm/index.js', {}) 18 | t.equal( 19 | LRUCache.toString().split(/\r?\n/)[0].trim(), 20 | 'class LRUCache {' 21 | ) 22 | }) 23 | 24 | t.test('basic operation', t => { 25 | const statuses: LRU.Status[] = [] 26 | const s = (): LRU.Status => { 27 | const status: LRU.Status = {} 28 | statuses.push(status) 29 | return status 30 | } 31 | const c = new LRU({ max: 10 }) 32 | for (let i = 0; i < 5; i++) { 33 | t.equal(c.set(i, i, { status: s() }), c) 34 | } 35 | for (let i = 0; i < 5; i++) { 36 | t.equal(c.get(i, { status: s() }), i) 37 | } 38 | t.equal(c.size, 5) 39 | t.matchSnapshot(c.entries()) 40 | t.equal( 41 | c.getRemainingTTL(1), 42 | Infinity, 43 | 'no ttl, so returns Infinity' 44 | ) 45 | t.equal( 46 | c.getRemainingTTL('not in cache'), 47 | 0, 48 | 'not in cache, no ttl' 49 | ) 50 | 51 | for (let i = 5; i < 10; i++) { 52 | c.set(i, i, { status: s() }) 53 | } 54 | // second time to get the update statuses 55 | for (let i = 5; i < 10; i++) { 56 | c.set(i, i, { status: s() }) 57 | } 58 | t.equal(c.size, 10) 59 | t.matchSnapshot(c.entries()) 60 | 61 | for (let i = 0; i < 5; i++) { 62 | // this doesn't do anything, but shouldn't be a problem. 63 | c.get(i, { updateAgeOnGet: true, status: s() }) 64 | } 65 | t.equal(c.size, 10) 66 | t.matchSnapshot(c.entries()) 67 | 68 | for (let i = 5; i < 10; i++) { 69 | c.get(i, { status: s() }) 70 | } 71 | for (let i = 10; i < 15; i++) { 72 | c.set(i, i, { status: s() }) 73 | } 74 | t.equal(c.size, 10) 75 | t.matchSnapshot(c.entries()) 76 | 77 | for (let i = 15; i < 20; i++) { 78 | c.set(i, i, { status: s() }) 79 | } 80 | // got pruned and replaced 81 | t.equal(c.size, 10) 82 | t.matchSnapshot(c.entries()) 83 | 84 | for (let i = 0; i < 10; i++) { 85 | t.equal(c.get(i, { status: s() }), undefined) 86 | } 87 | t.matchSnapshot(c.entries()) 88 | 89 | for (let i = 0; i < 9; i++) { 90 | c.set(i, i) 91 | } 92 | t.equal(c.size, 10) 93 | t.equal(c.delete(19), true) 94 | t.equal(c.delete(19), false) 95 | t.equal(c.size, 9) 96 | c.set(10, 10, { status: s() }) 97 | t.equal(c.size, 10) 98 | 99 | c.clear() 100 | t.equal(c.size, 0) 101 | for (let i = 0; i < 10; i++) { 102 | c.set(i, i, { status: s() }) 103 | } 104 | t.equal(c.size, 10) 105 | t.equal(c.has(0, { status: s() }), true) 106 | t.equal(c.size, 10) 107 | c.set(true, 'true', { status: s() }) 108 | t.equal(c.has(true, { status: s() }), true) 109 | t.equal(c.get(true, { status: s() }), 'true') 110 | c.set(true, undefined) 111 | t.equal(c.has(true, { status: s() }), false) 112 | 113 | t.matchSnapshot(statuses, 'status tracking') 114 | t.end() 115 | }) 116 | 117 | t.test('bad max values', t => { 118 | // @ts-expect-error 119 | t.throws(() => new LRU()) 120 | // @ts-expect-error 121 | t.throws(() => new LRU(123)) 122 | // @ts-expect-error 123 | t.throws(() => new LRU({})) 124 | // @ts-expect-error 125 | t.throws(() => new LRU(null)) 126 | t.throws(() => new LRU({ max: -123 })) 127 | t.throws(() => new LRU({ max: 0 })) 128 | t.throws(() => new LRU({ max: 2.5 })) 129 | t.throws(() => new LRU({ max: Infinity })) 130 | t.throws(() => new LRU({ max: Number.MAX_SAFE_INTEGER * 2 })) 131 | 132 | // ok to have a max of 0 if maxSize or ttl are set 133 | const sizeOnly = new LRU({ maxSize: 100 }) 134 | 135 | // setting the size to invalid values 136 | t.throws(() => sizeOnly.set('foo', 'bar'), TypeError) 137 | t.throws(() => sizeOnly.set('foo', 'bar', { size: 0 }), TypeError) 138 | t.throws(() => sizeOnly.set('foo', 'bar', { size: -1 }), TypeError) 139 | t.throws( 140 | () => 141 | sizeOnly.set('foo', 'bar', { 142 | sizeCalculation: () => -1, 143 | }), 144 | TypeError 145 | ) 146 | t.throws( 147 | () => 148 | sizeOnly.set('foo', 'bar', { 149 | sizeCalculation: () => 0, 150 | }), 151 | TypeError 152 | ) 153 | 154 | const ttlOnly = new LRU({ ttl: 1000, ttlAutopurge: true }) 155 | // cannot set size when not tracking size 156 | t.throws(() => ttlOnly.set('foo', 'bar', { size: 1 }), TypeError) 157 | t.throws(() => ttlOnly.set('foo', 'bar', { size: 1 }), TypeError) 158 | 159 | const sizeTTL = new LRU({ maxSize: 100, ttl: 1000 }) 160 | t.type(sizeTTL, LRU) 161 | t.end() 162 | }) 163 | 164 | t.test('setting ttl with non-integer values', t => { 165 | t.throws(() => new LRU({ max: 10, ttl: 10.5 }), TypeError) 166 | t.throws(() => new LRU({ max: 10, ttl: -10 }), TypeError) 167 | // @ts-expect-error 168 | t.throws(() => new LRU({ max: 10, ttl: 'banana' }), TypeError) 169 | t.throws(() => new LRU({ max: 10, ttl: Infinity }), TypeError) 170 | t.end() 171 | }) 172 | 173 | t.test('setting maxSize with non-integer values', t => { 174 | t.throws(() => new LRU({ max: 10, maxSize: 10.5 }), TypeError) 175 | t.throws(() => new LRU({ max: 10, maxSize: -10 }), TypeError) 176 | t.throws(() => new LRU({ max: 10, maxEntrySize: 10.5 }), TypeError) 177 | t.throws(() => new LRU({ max: 10, maxEntrySize: -10 }), TypeError) 178 | t.throws( 179 | // @ts-expect-error 180 | () => new LRU({ max: 10, maxEntrySize: 'banana' }), 181 | TypeError 182 | ) 183 | t.throws( 184 | () => new LRU({ max: 10, maxEntrySize: Infinity }), 185 | TypeError 186 | ) 187 | // @ts-expect-error 188 | t.throws(() => new LRU({ max: 10, maxSize: 'banana' }), TypeError) 189 | t.throws(() => new LRU({ max: 10, maxSize: Infinity }), TypeError) 190 | t.end() 191 | }) 192 | 193 | t.test('bad sizeCalculation', t => { 194 | t.throws(() => { 195 | // @ts-expect-error 196 | new LRU({ max: 1, sizeCalculation: true }) 197 | }, TypeError) 198 | t.throws(() => { 199 | // @ts-expect-error 200 | new LRU({ max: 1, maxSize: 1, sizeCalculation: true }) 201 | }, TypeError) 202 | t.end() 203 | }) 204 | 205 | t.test('delete from middle, reuses that index', t => { 206 | const c = new LRU({ max: 5 }) 207 | for (let i = 0; i < 5; i++) { 208 | c.set(i, i) 209 | } 210 | c.delete(2) 211 | c.set(5, 5) 212 | t.strictSame(expose(c).valList, [0, 1, 5, 3, 4]) 213 | t.end() 214 | }) 215 | 216 | t.test('peek does not disturb order', t => { 217 | const c = new LRU({ max: 5 }) 218 | for (let i = 0; i < 5; i++) { 219 | c.set(i, i) 220 | } 221 | t.equal(c.peek(2), 2) 222 | t.strictSame([...c.values()], [4, 3, 2, 1, 0]) 223 | t.end() 224 | }) 225 | 226 | t.test('re-use key before initial fill completed', t => { 227 | const statuses: LRU.Status[] = [] 228 | const s = (): LRU.Status => { 229 | const status: LRU.Status = {} 230 | statuses.push(status) 231 | return status 232 | } 233 | 234 | const c = new LRU({ max: 5 }) 235 | c.set(0, 0, { status: s() }) 236 | c.set(1, 1, { status: s() }) 237 | c.set(2, 2, { status: s() }) 238 | c.set(1, 2, { status: s() }) 239 | c.set(3, 3, { status: s() }) 240 | t.same( 241 | [...c.entries()], 242 | [ 243 | [3, 3], 244 | [1, 2], 245 | [2, 2], 246 | [0, 0], 247 | ] 248 | ) 249 | t.matchSnapshot(statuses) 250 | t.end() 251 | }) 252 | -------------------------------------------------------------------------------- /test/delete-while-iterating.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache as LRU } from '../dist/esm/index.js' 3 | 4 | t.beforeEach(t => { 5 | const c = new LRU({ max: 5 }) 6 | c.set(0, 0) 7 | c.set(1, 1) 8 | c.set(2, 2) 9 | c.set(3, 3) 10 | c.set(4, 4) 11 | t.context = c 12 | }) 13 | 14 | t.test('delete evens', t => { 15 | const c = t.context 16 | t.same([...c.keys()], [4, 3, 2, 1, 0]) 17 | 18 | for (const k of c.keys()) { 19 | if (k % 2 === 0) { 20 | c.delete(k) 21 | } 22 | } 23 | 24 | t.same([...c.keys()], [3, 1]) 25 | t.end() 26 | }) 27 | 28 | t.test('delete odds', t => { 29 | const c = t.context 30 | t.same([...c.keys()], [4, 3, 2, 1, 0]) 31 | 32 | for (const k of c.keys()) { 33 | if (k % 2 === 1) { 34 | c.delete(k) 35 | } 36 | } 37 | 38 | t.same([...c.keys()], [4, 2, 0]) 39 | t.end() 40 | }) 41 | 42 | t.test('rdelete evens', t => { 43 | const c = t.context 44 | t.same([...c.keys()], [4, 3, 2, 1, 0]) 45 | 46 | for (const k of c.rkeys()) { 47 | if (k % 2 === 0) { 48 | c.delete(k) 49 | } 50 | } 51 | 52 | t.same([...c.keys()], [3, 1]) 53 | t.end() 54 | }) 55 | 56 | t.test('rdelete odds', t => { 57 | const c = t.context 58 | t.same([...c.keys()], [4, 3, 2, 1, 0]) 59 | 60 | for (const k of c.rkeys()) { 61 | if (k % 2 === 1) { 62 | c.delete(k) 63 | } 64 | } 65 | 66 | t.same([...c.keys()], [4, 2, 0]) 67 | t.end() 68 | }) 69 | 70 | t.test('delete two of them', t => { 71 | const c = t.context 72 | t.same([...c.keys()], [4, 3, 2, 1, 0]) 73 | for (const k of c.keys()) { 74 | if (k === 3) { 75 | c.delete(3) 76 | c.delete(4) 77 | } else if (k === 1) { 78 | c.delete(1) 79 | c.delete(0) 80 | } 81 | } 82 | t.same([...c.keys()], [2]) 83 | t.end() 84 | }) 85 | 86 | t.test('rdelete two of them', t => { 87 | const c = t.context 88 | t.same([...c.keys()], [4, 3, 2, 1, 0]) 89 | for (const k of c.rkeys()) { 90 | if (k === 3) { 91 | c.delete(3) 92 | c.delete(4) 93 | } else if (k === 1) { 94 | c.delete(1) 95 | c.delete(0) 96 | } 97 | } 98 | t.same([...c.keys()], [2]) 99 | t.end() 100 | }) 101 | -------------------------------------------------------------------------------- /test/dispose.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache as LRU } from '../dist/esm/index.js' 3 | 4 | t.test('disposal', t => { 5 | const disposed: any[] = [] 6 | const c = new LRU({ 7 | max: 5, 8 | dispose: (k, v, r) => disposed.push([k, v, r]), 9 | }) 10 | for (let i = 0; i < 9; i++) { 11 | c.set(i, i) 12 | } 13 | t.strictSame(disposed, [ 14 | [0, 0, 'evict'], 15 | [1, 1, 'evict'], 16 | [2, 2, 'evict'], 17 | [3, 3, 'evict'], 18 | ]) 19 | t.equal(c.size, 5) 20 | 21 | c.set(9, 9) 22 | t.strictSame(disposed, [ 23 | [0, 0, 'evict'], 24 | [1, 1, 'evict'], 25 | [2, 2, 'evict'], 26 | [3, 3, 'evict'], 27 | [4, 4, 'evict'], 28 | ]) 29 | 30 | disposed.length = 0 31 | c.set('asdf', 'foo') 32 | c.set('asdf', 'asdf') 33 | t.strictSame(disposed, [ 34 | [5, 5, 'evict'], 35 | ['foo', 'asdf', 'set'], 36 | ]) 37 | 38 | disposed.length = 0 39 | for (let i = 0; i < 5; i++) { 40 | c.set(i, i) 41 | } 42 | t.strictSame(disposed, [ 43 | [6, 6, 'evict'], 44 | [7, 7, 'evict'], 45 | [8, 8, 'evict'], 46 | [9, 9, 'evict'], 47 | ['asdf', 'asdf', 'evict'], 48 | ]) 49 | 50 | // dispose both old and current 51 | disposed.length = 0 52 | c.set('asdf', 'foo') 53 | c.delete('asdf') 54 | t.strictSame(disposed, [ 55 | [0, 0, 'evict'], 56 | ['foo', 'asdf', 'delete'], 57 | ]) 58 | 59 | // delete non-existing key, no disposal 60 | disposed.length = 0 61 | c.delete('asdf') 62 | t.strictSame(disposed, []) 63 | 64 | // delete via clear() 65 | disposed.length = 0 66 | c.clear() 67 | t.strictSame(disposed, [ 68 | [1, 1, 'delete'], 69 | [2, 2, 'delete'], 70 | [3, 3, 'delete'], 71 | [4, 4, 'delete'], 72 | ]) 73 | 74 | disposed.length = 0 75 | c.set(3, 3) 76 | t.equal(c.get(3), 3) 77 | c.delete(3) 78 | t.strictSame(disposed, [[3, 3, 'delete']]) 79 | 80 | // disposed because of being overwritten 81 | c.clear() 82 | disposed.length = 0 83 | for (let i = 0; i < 5; i++) { 84 | c.set(i, i) 85 | } 86 | c.set(2, 'two') 87 | t.strictSame(disposed, [[2, 2, 'set']]) 88 | for (let i = 0; i < 5; i++) { 89 | t.equal(c.get(i), i === 2 ? 'two' : i) 90 | } 91 | t.strictSame(disposed, [[2, 2, 'set']]) 92 | 93 | c.noDisposeOnSet = true 94 | c.clear() 95 | disposed.length = 0 96 | for (let i = 0; i < 5; i++) { 97 | c.set(i, i) 98 | } 99 | c.set(2, 'two') 100 | for (let i = 0; i < 5; i++) { 101 | t.equal(c.get(i), i === 2 ? 'two' : i) 102 | } 103 | t.strictSame(disposed, []) 104 | 105 | t.end() 106 | }) 107 | 108 | t.test('noDisposeOnSet with delete()', t => { 109 | const disposed: [any, any][] = [] 110 | const dispose = (v: any, k: any) => disposed.push([v, k]) 111 | 112 | const c = new LRU({ max: 5, dispose, noDisposeOnSet: true }) 113 | for (let i = 0; i < 5; i++) { 114 | c.set(i, i) 115 | } 116 | for (let i = 0; i < 4; i++) { 117 | c.set(i, `new ${i}`) 118 | } 119 | t.strictSame(disposed, []) 120 | c.delete(0) 121 | c.delete(4) 122 | t.strictSame(disposed, [ 123 | ['new 0', 0], 124 | [4, 4], 125 | ]) 126 | disposed.length = 0 127 | 128 | const d = new LRU({ max: 5, dispose }) 129 | for (let i = 0; i < 5; i++) { 130 | d.set(i, i) 131 | } 132 | for (let i = 0; i < 4; i++) { 133 | d.set(i, `new ${i}`) 134 | } 135 | t.strictSame(disposed, [ 136 | [0, 0], 137 | [1, 1], 138 | [2, 2], 139 | [3, 3], 140 | ]) 141 | d.delete(0) 142 | d.delete(4) 143 | t.strictSame(disposed, [ 144 | [0, 0], 145 | [1, 1], 146 | [2, 2], 147 | [3, 3], 148 | ['new 0', 0], 149 | [4, 4], 150 | ]) 151 | 152 | t.end() 153 | }) 154 | 155 | t.test('disposeAfter', t => { 156 | const c = new LRU({ 157 | max: 5, 158 | disposeAfter: (v, k) => { 159 | if (k === 2) { 160 | // increment it every time it gets disposed, but only one time 161 | c.set(k, (v as number) + 1, { noDisposeOnSet: true }) 162 | } 163 | }, 164 | }) 165 | 166 | for (let i = 0; i < 100; i++) { 167 | c.set(i, i) 168 | } 169 | t.same( 170 | [...c.entries()], 171 | [ 172 | [99, 99], 173 | [98, 98], 174 | [2, 21], 175 | [97, 97], 176 | [96, 96], 177 | ] 178 | ) 179 | c.delete(2) 180 | t.same( 181 | [...c.entries()], 182 | [ 183 | [2, 22], 184 | [99, 99], 185 | [98, 98], 186 | [97, 97], 187 | [96, 96], 188 | ] 189 | ) 190 | for (let i = 96; i < 100; i++) { 191 | c.set(i, i + 1) 192 | } 193 | t.same( 194 | [...c.entries()], 195 | [ 196 | [99, 100], 197 | [98, 99], 198 | [97, 98], 199 | [96, 97], 200 | [2, 22], 201 | ] 202 | ) 203 | c.clear() 204 | t.same([...c.entries()], [[2, 23]]) 205 | 206 | t.end() 207 | }) 208 | -------------------------------------------------------------------------------- /test/esm-load.mjs: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache } from '../dist/esm/index.js' 3 | const c = new LRUCache({ max: 2 }) 4 | t.type(c, LRUCache) 5 | c.set(1, 1) 6 | c.set(2, 2) 7 | c.set(3, 3) 8 | t.equal(c.get(1), undefined) 9 | t.equal(c.get(2), 2) 10 | t.equal(c.get(3), 3) 11 | -------------------------------------------------------------------------------- /test/fetch.ts: -------------------------------------------------------------------------------- 1 | if (typeof performance === 'undefined') { 2 | global.performance = require('perf_hooks').performance 3 | } 4 | import t from 'tap' 5 | import { BackgroundFetch, LRUCache } from '../dist/esm/index.js' 6 | import { expose } from './fixtures/expose.js' 7 | 8 | const fn: LRUCache.Fetcher = async (_, v) => 9 | new Promise(res => 10 | queueMicrotask(() => res(v === undefined ? 0 : v + 1)) 11 | ) 12 | 13 | import { Clock } from 'clock-mock' 14 | const clock = new Clock() 15 | t.teardown(clock.enter()) 16 | clock.advance(1) 17 | 18 | let LRU = LRUCache 19 | 20 | const c = new LRU({ 21 | fetchMethod: fn, 22 | max: 5, 23 | ttl: 5, 24 | }) 25 | 26 | const getStatusObj = (): LRUCache.Status => ({}) 27 | 28 | t.test('asynchronous fetching', async t => { 29 | const s1 = getStatusObj() 30 | const v1 = await c.fetch('key', { status: s1 }) 31 | t.equal(v1, 0, 'first fetch, no stale data, wait for initial value') 32 | t.matchSnapshot(s1, 'status 1') 33 | const s2 = getStatusObj() 34 | const v2 = await c.fetch('key', { status: s2 }) 35 | t.equal(v2, 0, 'got same cached value') 36 | t.matchSnapshot(s2, 'status 2') 37 | 38 | clock.advance(10) 39 | 40 | const s3 = getStatusObj() 41 | const v3 = await c.fetch('key', { allowStale: true, status: s3 }) 42 | t.equal(v3, 0, 'fetch while stale, allowStale, get stale data') 43 | t.matchSnapshot(s3, 'status 3') 44 | const s31 = getStatusObj() 45 | t.equal( 46 | await c.fetch('key', { allowStale: true, status: s31 }), 47 | 0, 48 | 'get stale data again while re-fetching because stale previously' 49 | ) 50 | t.matchSnapshot(s31, 'status 3.1') 51 | const s4 = getStatusObj() 52 | const v4 = await c.fetch('key', { status: s4 }) 53 | t.equal(v4, 1, 'no allow stale, wait until fresh data available') 54 | t.matchSnapshot(s4, 'status 4') 55 | const s5 = getStatusObj() 56 | const v5 = await c.fetch('key', { status: s5 }) 57 | t.equal(v5, 1, 'fetch while not stale, just get from cache') 58 | t.matchSnapshot(s5, 'status 5') 59 | 60 | clock.advance(10) 61 | 62 | const v6 = await c.fetch('key', { allowStale: true }) 63 | t.equal( 64 | v6, 65 | 1, 66 | 'fetch while stale, starts new fetch, return stale data' 67 | ) 68 | const e = expose(c) 69 | const v = e.valList[0] 70 | 71 | // should not have any promises or cycles in the dump 72 | const dump = c.dump() 73 | for (const [_, entry] of dump) { 74 | t.type(entry.value, 'number') 75 | } 76 | t.matchSnapshot(JSON.stringify(dump), 'safe to stringify dump') 77 | 78 | t.equal(e.isBackgroundFetch(v), true) 79 | t.equal(e.backgroundFetch('key', 0, {}, undefined), v) 80 | await v 81 | const v7 = await c.fetch('key', { 82 | allowStale: true, 83 | updateAgeOnGet: true, 84 | }) 85 | t.equal(v7, 2, 'fetch completed, so get new data') 86 | 87 | clock.advance(100) 88 | 89 | const v8 = await c.fetch('key', { allowStale: true }) 90 | const v9 = c.get('key', { allowStale: true }) 91 | t.equal(v8, 2, 'fetch returned stale while fetching') 92 | t.equal(v9, 2, 'get() returned stale while fetching') 93 | 94 | const v10 = c.fetch('key2') 95 | const v11 = c.get('key2') 96 | t.equal(v11, undefined, 'get while fetching but not yet returned') 97 | t.equal(await v10, 0, 'eventually 0 is returned') 98 | const v12 = c.get('key2') 99 | t.equal(v12, 0, 'get cached value after fetch') 100 | 101 | const v13 = c.fetch('key3') 102 | c.delete('key3') 103 | await t.rejects(v13, 'rejects, because it was deleted') 104 | t.equal(c.has('key3'), false, 'not inserted into cache') 105 | 106 | c.fetch('key4') 107 | clock.advance(100) 108 | const v15 = await c.fetch('key4', { allowStale: true }) 109 | t.equal( 110 | v15, 111 | 0, 112 | 'there was no stale data, even though we were ok with that' 113 | ) 114 | 115 | c.set('key5', 0) 116 | clock.advance(100) 117 | const v16 = await c.fetch('key5') 118 | t.equal(v16, 1, 'waited for new data, data in cache was stale') 119 | 120 | c.fetch('key4') 121 | await Promise.resolve().then(() => {}) 122 | clock.advance(100) 123 | const v18 = c.get('key4') 124 | t.equal( 125 | v18, 126 | undefined, 127 | 'get while fetching, but did not want stale data' 128 | ) 129 | 130 | const p6 = c.fetch('key6') 131 | await Promise.resolve().then(() => {}) 132 | clock.advance(100) 133 | const v20 = c.get('key6', { allowStale: true }) 134 | t.equal( 135 | v20, 136 | undefined, 137 | 'get while fetching, but no stale data to return' 138 | ) 139 | t.equal(await p6, 0) 140 | clock.advance(100) 141 | const p7 = c.fetch('key6') 142 | const status: LRUCache.Status = {} 143 | const v21 = c.get('key6', { allowStale: true, status }) 144 | t.equal(v21, 0, 'allowStale, got stale data while fetching') 145 | t.equal( 146 | status.returnedStale, 147 | true, 148 | 'status reflects stale data returned' 149 | ) 150 | clock.advance(100) 151 | t.equal(await p7, 1, 'eventually updated') 152 | }) 153 | 154 | t.test('fetchMethod must be a function', async t => { 155 | // @ts-expect-error 156 | t.throws(() => new LRU({ fetchMethod: true, max: 2 })) 157 | }) 158 | 159 | t.test('fetch without fetch method', async t => { 160 | const c = new LRU({ max: 3 }) 161 | c.set(0, 0) 162 | c.set(1, 1) 163 | const status: LRUCache.Status = {} 164 | t.same( 165 | await Promise.all([c.fetch(0, { status }), c.fetch(1)]), 166 | [0, 1] 167 | ) 168 | t.matchSnapshot(status, 'status update') 169 | }) 170 | 171 | t.test('fetch options, signal', async t => { 172 | const statuses: LRUCache.Status[] = [] 173 | const s = (): LRUCache.Status => { 174 | const status: LRUCache.Status = {} 175 | statuses.push(status) 176 | return status 177 | } 178 | 179 | let aborted = false 180 | const disposed: any[] = [] 181 | const disposedAfter: any[] = [] 182 | const c = new LRU({ 183 | max: 3, 184 | ttl: 100, 185 | fetchMethod: async (k, oldVal, { signal, options }) => { 186 | t.ok(options.status, 'received status object') 187 | // do something async 188 | await new Promise(res => queueMicrotask(res)) 189 | if (signal.aborted) { 190 | aborted = true 191 | return 192 | } 193 | if (k === 2) { 194 | options.ttl = 25 195 | } 196 | return (oldVal || 0) + 1 197 | }, 198 | dispose: (v, k, reason) => { 199 | disposed.push([v, k, reason]) 200 | }, 201 | disposeAfter: (v, k, reason) => { 202 | disposedAfter.push([v, k, reason]) 203 | }, 204 | }) 205 | 206 | const v1 = c.fetch(2, { status: s() }) 207 | const testp1 = t.rejects(v1, 'aborted by clearing the cache') 208 | c.delete(2) 209 | await testp1 210 | await new Promise(res => queueMicrotask(res)) 211 | t.equal(aborted, true) 212 | t.same(disposed, [], 'no disposals for aborted promises') 213 | t.same(disposedAfter, [], 'no disposals for aborted promises') 214 | 215 | aborted = false 216 | const v2 = c.fetch(2, { status: s() }) 217 | const testp2 = t.rejects(v2, 'rejected, replaced') 218 | c.set(2, 2) 219 | await testp2 220 | await new Promise(res => queueMicrotask(res)) 221 | t.equal(aborted, true) 222 | t.same(disposed, [], 'no disposals for aborted promises') 223 | t.same(disposedAfter, [], 'no disposals for aborted promises') 224 | c.delete(2) 225 | disposed.length = 0 226 | disposedAfter.length = 0 227 | 228 | aborted = false 229 | const v3 = c.fetch(2, { status: s() }) 230 | const testp3 = t.rejects(v3, 'rejected, aborted by evict') 231 | c.set(3, 3, { status: s() }) 232 | c.set(4, 4, { status: s() }) 233 | c.set(5, 5, { status: s() }) 234 | await testp3 235 | await new Promise(res => queueMicrotask(res)) 236 | t.equal(aborted, true) 237 | t.same(disposed, [], 'no disposals for aborted promises') 238 | t.same(disposedAfter, [], 'no disposals for aborted promises') 239 | 240 | aborted = false 241 | await c.fetch(6, { ttl: 1000, status: s() }) 242 | t.equal( 243 | c.getRemainingTTL(6), 244 | 1000, 245 | 'overridden ttl in fetch() opts' 246 | ) 247 | await c.fetch(2, { ttl: 1, status: s() }) 248 | t.equal(c.getRemainingTTL(2), 25, 'overridden ttl in fetchMethod') 249 | t.matchSnapshot(statuses, 'status updates') 250 | }) 251 | 252 | t.test('fetchMethod throws', async t => { 253 | const statuses: LRUCache.Status[] = [] 254 | const s = (): LRUCache.Status => { 255 | const status: LRUCache.Status = {} 256 | statuses.push(status) 257 | return status 258 | } 259 | 260 | // make sure that even if there's no one to sit around and wait for it, 261 | // the background fetch throwing doesn't blow anything up. 262 | const cache = new LRU({ 263 | max: 10, 264 | ttl: 10, 265 | allowStale: true, 266 | fetchMethod: async () => { 267 | throw new Error('fetch failure') 268 | }, 269 | }) 270 | // seed the cache, and make the values stale. 271 | // this simulates the case where the fetch() DID work, 272 | // and replaced the promise with the resolution, but 273 | // then they got stale. 274 | cache.set('a', 1, { status: s() }) 275 | cache.set('b', 2, { status: s() }) 276 | clock.advance(20) 277 | await Promise.resolve().then(() => {}) 278 | const a = await Promise.all([ 279 | cache.fetch('a', { status: s() }), 280 | cache.fetch('a', { status: s() }), 281 | cache.fetch('a', { status: s() }), 282 | ]) 283 | t.strictSame(a, [1, 1, 1]) 284 | // clock advances, promise rejects 285 | clock.advance(20) 286 | await Promise.resolve().then(() => {}) 287 | t.equal( 288 | cache.get('a', { status: s() }), 289 | undefined, 290 | 'removed from cache' 291 | ) 292 | const b = await Promise.all([ 293 | cache.fetch('b', { status: s() }), 294 | cache.fetch('b', { status: s() }), 295 | cache.fetch('b', { status: s() }), 296 | ]) 297 | t.strictSame(b, [2, 2, 2]) 298 | clock.advance(20) 299 | await Promise.resolve().then(() => {}) 300 | t.equal( 301 | cache.get('b', { status: s() }), 302 | undefined, 303 | 'removed from cache' 304 | ) 305 | const ap = cache.fetch('a', { status: s() }) 306 | const testap = t.rejects(ap, 'aborted by replace') 307 | cache.set('a', 99, { status: s() }) 308 | await testap 309 | t.equal( 310 | cache.get('a', { status: s() }), 311 | 99, 312 | 'did not delete new value' 313 | ) 314 | t.rejects(cache.fetch('b', { status: s() }), { 315 | message: 'fetch failure', 316 | }) 317 | t.matchSnapshot(statuses, 'status updates') 318 | }) 319 | 320 | t.test( 321 | 'fetchMethod throws, noDeleteOnFetchRejection option', 322 | async t => { 323 | // make sure that even if there's no one to sit around and wait for it, 324 | // the background fetch throwing doesn't blow anything up. 325 | let fetchFail = true 326 | const cache = new LRU({ 327 | max: 10, 328 | ttl: 10, 329 | allowStale: true, 330 | noDeleteOnFetchRejection: true, 331 | fetchMethod: async () => { 332 | if (fetchFail) { 333 | throw new Error('fetch failure') 334 | } else { 335 | return 1 336 | } 337 | }, 338 | }) 339 | 340 | // seed the cache, and make the values stale. 341 | // this simulates the case where the fetch() DID work, 342 | // and replaced the promise with the resolution, but 343 | // then they got stale. 344 | cache.set('a', 1) 345 | cache.set('b', 2) 346 | clock.advance(20) 347 | await Promise.resolve().then(() => {}) 348 | const a = await Promise.all([ 349 | cache.fetch('a'), 350 | cache.fetch('a'), 351 | cache.fetch('a'), 352 | ]) 353 | t.strictSame(a, [1, 1, 1]) 354 | // clock advances, promise rejects 355 | clock.advance(20) 356 | await Promise.resolve().then(() => {}) 357 | const e = expose(cache) 358 | t.equal(e.keyMap.get('a'), 0) 359 | t.equal(e.valList[0], 1, 'promise replaced with stale value') 360 | const b = await Promise.all([ 361 | cache.fetch('b'), 362 | cache.fetch('b'), 363 | cache.fetch('b'), 364 | ]) 365 | t.strictSame(b, [2, 2, 2]) 366 | clock.advance(20) 367 | await Promise.resolve().then(() => {}) 368 | t.equal(e.keyMap.get('b'), 1) 369 | t.equal(e.valList[1], 2, 'promise replaced with stale value') 370 | cache.delete('a') 371 | cache.delete('b') 372 | 373 | // even though we don't noDeleteOnFetchRejection, 374 | // if there's no stale, we still remove the *promise*. 375 | const ap = cache.fetch('a') 376 | const testap = t.rejects(ap, 'aborted by replace') 377 | cache.set('a', 99) 378 | await testap 379 | t.equal(cache.get('a'), 99, 'did not delete, was replaced') 380 | await t.rejects(cache.fetch('b'), { message: 'fetch failure' }) 381 | t.equal(e.keyMap.get('b'), undefined, 'not in cache') 382 | t.equal(e.valList[1], undefined, 'not in cache') 383 | } 384 | ) 385 | 386 | t.test('fetch context', async t => { 387 | const cache = new LRU({ 388 | max: 10, 389 | ttl: 10, 390 | allowStale: true, 391 | noDeleteOnFetchRejection: true, 392 | fetchMethod: async (k, _, { context, options }) => { 393 | //@ts-expect-error 394 | t.equal(options.context, undefined) 395 | t.equal(context, expectContext) 396 | return [k, context] 397 | }, 398 | }) 399 | 400 | let expectContext = 'overridden' 401 | t.strictSame(await cache.fetch('y', { context: 'overridden' }), [ 402 | 'y', 403 | 'overridden', 404 | ]) 405 | expectContext = 'first context' 406 | t.strictSame(await cache.fetch('x', { context: 'first context' }), [ 407 | 'x', 408 | 'first context', 409 | ]) 410 | // if still in cache, doesn't call fetchMethod again 411 | t.strictSame(await cache.fetch('x', { context: 'ignored' }), [ 412 | 'x', 413 | 'first context', 414 | ]) 415 | }) 416 | 417 | t.test('forceRefresh', async t => { 418 | const statuses: LRUCache.Status[] = [] 419 | const s = (): LRUCache.Status => { 420 | const status: LRUCache.Status = {} 421 | statuses.push(status) 422 | return status 423 | } 424 | 425 | const cache = new LRU({ 426 | max: 10, 427 | allowStale: true, 428 | ttl: 100, 429 | fetchMethod: async (k, _, { options }) => { 430 | t.equal( 431 | //@ts-expect-error 432 | options.forceRefresh, 433 | undefined, 434 | 'do not expose forceRefresh' 435 | ) 436 | return new Promise(res => queueMicrotask(() => res(k))) 437 | }, 438 | }) 439 | 440 | // put in some values that don't match what fetchMethod returns 441 | cache.set(1, 100) 442 | cache.set(2, 200) 443 | t.equal(await cache.fetch(1), 100) 444 | // still there, because we're allowing stale, and it's not stale 445 | const status: LRUCache.Status = {} 446 | t.equal( 447 | await cache.fetch(2, { 448 | forceRefresh: true, 449 | allowStale: false, 450 | status, 451 | }), 452 | 2 453 | ) 454 | t.equal(status.fetch, 'refresh', 'status reflects forced refresh') 455 | t.equal(await cache.fetch(1, { forceRefresh: true }), 100) 456 | clock.advance(100) 457 | t.equal( 458 | await cache.fetch(2, { forceRefresh: true, status: s() }), 459 | 2 460 | ) 461 | t.equal(cache.peek(1), 100) 462 | // if we don't allow stale though, then that means that we wait 463 | // for the background fetch to complete, so we get the updated value. 464 | t.equal(await cache.fetch(1, { allowStale: false, status: s() }), 1) 465 | 466 | cache.set(1, 100) 467 | t.equal(await cache.fetch(1, { allowStale: false }), 100) 468 | t.equal( 469 | await cache.fetch(1, { 470 | forceRefresh: true, 471 | allowStale: false, 472 | status: s(), 473 | }), 474 | 1 475 | ) 476 | 477 | t.matchSnapshot(statuses, 'status updates') 478 | }) 479 | 480 | t.test('allowStaleOnFetchRejection', async t => { 481 | let fetchFail = false 482 | const c = new LRU({ 483 | ttl: 10, 484 | max: 10, 485 | allowStaleOnFetchRejection: true, 486 | fetchMethod: async k => { 487 | if (fetchFail) throw new Error('fetch rejection') 488 | return k 489 | }, 490 | }) 491 | t.equal(await c.fetch(1), 1) 492 | clock.advance(11) 493 | fetchFail = true 494 | const status: LRUCache.Status = {} 495 | t.equal(await c.fetch(1, { status }), 1) 496 | t.equal( 497 | status.returnedStale, 498 | true, 499 | 'status reflects returned stale value' 500 | ) 501 | t.equal(await c.fetch(1), 1) 502 | // if we override it, no go 503 | await t.rejects(c.fetch(1, { allowStaleOnFetchRejection: false })) 504 | // that also deletes from the cache 505 | t.equal(c.get(1), undefined) 506 | }) 507 | 508 | t.test( 509 | 'placeholder promise is not removed when resolving', 510 | async t => { 511 | const resolves: Record void> = {} 512 | const c = new LRU({ 513 | maxSize: 10, 514 | sizeCalculation(v) { 515 | return v 516 | }, 517 | fetchMethod: k => { 518 | return new Promise(resolve => (resolves[k] = resolve)) 519 | }, 520 | }) 521 | const p3 = c.fetch(3) 522 | const p4 = c.fetch(4) 523 | const p5 = c.fetch(5) 524 | 525 | resolves[4]?.(4) 526 | await p4 527 | 528 | t.match([...c], [[4, 4]]) 529 | resolves[5]?.(5) 530 | await p5 531 | t.match( 532 | [...c], 533 | [ 534 | [5, 5], 535 | [4, 4], 536 | ] 537 | ) 538 | 539 | resolves[3]?.(3) 540 | await p3 541 | t.same( 542 | [...c], 543 | [ 544 | [3, 3], 545 | [5, 5], 546 | ] 547 | ) 548 | 549 | t.equal(c.size, 2) 550 | t.equal([...c].length, 2) 551 | } 552 | ) 553 | 554 | t.test('send a signal', async t => { 555 | const statuses: LRUCache.Status[] = [] 556 | const s = (): LRUCache.Status => { 557 | const status: LRUCache.Status = {} 558 | statuses.push(status) 559 | return status 560 | } 561 | 562 | let aborted: Error | undefined = undefined 563 | let resolved: boolean = false 564 | const c = new LRU({ 565 | max: 10, 566 | fetchMethod: async (k, _, { signal, options }) => { 567 | t.ok(options.status, 'has a status object') 568 | signal.addEventListener('abort', () => { 569 | aborted = signal.reason 570 | }) 571 | return new Promise(res => 572 | setTimeout(() => { 573 | resolved = true 574 | res(k) 575 | }, 100) 576 | ) 577 | }, 578 | }) 579 | const ac = new AbortController() 580 | const p = c.fetch(1, { signal: ac.signal, status: s() }) 581 | const er = new Error('custom abort signal') 582 | const testp = t.rejects(p, er) 583 | ac.abort(er) 584 | await testp 585 | t.equal( 586 | resolved, 587 | false, 588 | 'should have aborted before fetchMethod resolved' 589 | ) 590 | t.equal(aborted, er) 591 | t.equal(ac.signal.reason, er) 592 | t.equal(c.get(1, { status: s() }), undefined) 593 | t.matchSnapshot(statuses, 'status updates') 594 | }) 595 | 596 | t.test('verify inflight works as expected', async t => { 597 | const statuses: LRUCache.Status[] = [] 598 | const s = (): LRUCache.Status => { 599 | const status: LRUCache.Status = {} 600 | statuses.push(status) 601 | return status 602 | } 603 | let called = 0 604 | const c = new LRUCache({ 605 | max: 5, 606 | fetchMethod: async () => { 607 | called++ 608 | await new Promise(res => queueMicrotask(res)) 609 | return {} 610 | }, 611 | }) 612 | const e = expose(c) 613 | c.fetch(1) 614 | const promises: Promise[] = [ 615 | c.fetch(1, { status: s() }), 616 | c.fetch(1), 617 | c.fetch(1, { status: s() }), 618 | c.fetch(1), 619 | ] 620 | t.match(e.valList, [Promise, null, null, null, null]) 621 | t.equal( 622 | e.isBackgroundFetch(e.valList[0]), 623 | true, 624 | 'is background fetch' 625 | ) 626 | t.equal(c.get(1, { status: s() }), undefined, 'get while fetching') 627 | const a = await Promise.all(promises) 628 | for (let i = 1; i < a.length; i++) { 629 | t.equal(a[i], a[0], `index ${i} equal to first returned value`) 630 | } 631 | t.equal(called, 1, 'called one time') 632 | t.matchSnapshot(statuses, 'status updates') 633 | }) 634 | 635 | t.test('abort, but then keep on fetching anyway', async t => { 636 | let aborted: Error | undefined = undefined 637 | let resolved: boolean = false 638 | let returnUndefined: boolean = false 639 | const cache = new LRU({ 640 | max: 10, 641 | ignoreFetchAbort: true, 642 | fetchMethod: async (k, _, { signal, options }) => { 643 | t.equal(options.ignoreFetchAbort, true, 'aborts ignored') 644 | signal.addEventListener('abort', () => { 645 | aborted = signal.reason 646 | }) 647 | return new Promise(res => 648 | setTimeout(() => { 649 | resolved = true 650 | if (returnUndefined) res() 651 | else res(k) 652 | }, 100) 653 | ) 654 | }, 655 | }) 656 | const ac = new AbortController() 657 | const status: LRUCache.Status = {} 658 | const p = cache.fetch(1, { signal: ac.signal, status }) 659 | const er = new Error('ignored abort signal') 660 | ac.abort(er) 661 | clock.advance(100) 662 | t.equal(await p, 1) 663 | t.equal( 664 | status.fetchAbortIgnored, 665 | true, 666 | 'status reflects ignored abort' 667 | ) 668 | t.equal(status.fetchError, er) 669 | t.equal(status.fetchUpdated, true) 670 | 671 | t.equal(resolved, true, 'aborted, but resolved anyway') 672 | t.equal(aborted, er) 673 | t.equal(ac.signal.reason, er) 674 | t.equal(cache.get(1), 1) 675 | 676 | const p2 = cache.fetch(2) 677 | t.equal(cache.get(2), undefined) 678 | cache.delete(2) 679 | t.equal(cache.get(2), undefined) 680 | clock.advance(100) 681 | t.equal(await p2, 2) 682 | t.equal(cache.get(2), undefined) 683 | 684 | // if aborted for cause, we don't save the fetched value 685 | const p3 = cache.fetch(3) 686 | t.equal(cache.get(3), undefined) 687 | cache.set(3, 33) 688 | t.equal(cache.get(3), 33) 689 | clock.advance(100) 690 | t.equal(await p3, 3) 691 | t.equal(cache.get(3), 33) 692 | 693 | const e = expose(cache) 694 | returnUndefined = true 695 | const before = e.valList.slice() 696 | const p4 = cache.fetch(4) 697 | clock.advance(100) 698 | t.equal(await p4, undefined) 699 | t.same(e.valList, before, 'did not update values with undefined') 700 | }) 701 | 702 | t.test('allowStaleOnFetchAbort', async t => { 703 | const c = new LRUCache({ 704 | ttl: 10, 705 | max: 10, 706 | allowStaleOnFetchAbort: true, 707 | fetchMethod: async (k, _, { signal }) => { 708 | return new Promise(res => { 709 | const t = setTimeout(() => res(k), 100) 710 | signal.addEventListener('abort', () => { 711 | clearTimeout(t) 712 | res() 713 | }) 714 | }) 715 | }, 716 | }) 717 | c.set(1, 10) 718 | clock.advance(100) 719 | const ac = new AbortController() 720 | const p = c.fetch(1, { signal: ac.signal }) 721 | ac.abort(new Error('gimme the stale value')) 722 | t.equal(await p, 10) 723 | t.equal( 724 | c.get(1, { allowStale: true, noDeleteOnStaleGet: true }), 725 | 10 726 | ) 727 | const p2 = c.fetch(1) 728 | c.set(1, 100) 729 | t.equal(await p2, 10) 730 | t.equal(c.get(1), 100) 731 | }) 732 | 733 | t.test('background update on timeout, return stale', async t => { 734 | let returnUndefined = false 735 | const c = new LRUCache({ 736 | ttl: 10, 737 | max: 10, 738 | ignoreFetchAbort: true, 739 | allowStaleOnFetchAbort: true, 740 | fetchMethod: async k => { 741 | return new Promise(res => { 742 | setTimeout(() => { 743 | res(returnUndefined ? undefined : k) 744 | }, 100) 745 | }) 746 | }, 747 | }) 748 | const e = expose(c) 749 | c.set(1, 10) 750 | clock.advance(100) 751 | const ac = new AbortController() 752 | const p = c.fetch(1, { signal: ac.signal }) 753 | await new Promise(res => queueMicrotask(res)) 754 | t.match(e.valList[0], { __staleWhileFetching: 10 }) 755 | ac.abort(new Error('gimme the stale value')) 756 | t.equal(await p, 10) 757 | t.equal(c.get(1, { allowStale: true }), 10) 758 | clock.advance(200) 759 | await new Promise(res => queueMicrotask(res)).then(() => {}) 760 | t.equal(e.valList[0], 1, 'got updated value later') 761 | 762 | c.set(1, 99) 763 | clock.advance(100) 764 | returnUndefined = true 765 | const ac2 = new AbortController() 766 | const p2 = c.fetch(1, { signal: ac2.signal }) 767 | await new Promise(res => queueMicrotask(res)) 768 | t.match(e.valList[0], { __staleWhileFetching: 99 }) 769 | ac2.abort(new Error('gimme stale 99')) 770 | t.equal(await p2, 99) 771 | t.match(e.valList[0], { __staleWhileFetching: 99 }) 772 | t.equal(c.get(1, { allowStale: true }), 99) 773 | t.match(e.valList[0], { __staleWhileFetching: 99 }) 774 | clock.advance(200) 775 | await new Promise(res => queueMicrotask(res)) 776 | t.equal(e.valList[0], 99) 777 | }) 778 | 779 | t.test('fetch context required if set in ctor type', async t => { 780 | const c = new LRUCache({ 781 | max: 5, 782 | fetchMethod: async (k, _, { context }) => { 783 | if (k === 'y') t.equal(context, undefined) 784 | else if (k === 'z') t.same(context, { x: 1 }) 785 | else t.same(context, { a: 1 }) 786 | return k 787 | }, 788 | }) 789 | c.fetch('x', { context: { a: 1 } }) 790 | //@ts-expect-error 791 | c.fetch('y') 792 | //@ts-expect-error 793 | c.fetch('z', { context: { x: 1 } }) 794 | 795 | const c2 = new LRUCache({ 796 | max: 5, 797 | fetchMethod: async (k, _, { context }) => { 798 | if (k === 'y') t.equal(context, undefined) 799 | else if (k === 'z') t.same(context, { x: 1 }) 800 | else t.same(context, { a: 1 }) 801 | return k 802 | }, 803 | }) 804 | //@ts-expect-error 805 | c2.fetch('x', { context: { a: 1 } }) 806 | c2.fetch('y') 807 | c2.fetch('y', { allowStale: true }) 808 | //@ts-expect-error 809 | c2.fetch('z', { context: { x: 1 } }) 810 | 811 | t.end() 812 | }) 813 | 814 | t.test('has false for pending fetch without stale val', async t => { 815 | const c = new LRUCache({ 816 | max: 10, 817 | fetchMethod: async (key: number) => 818 | new Promise(r => setTimeout(() => r(key), 10)), 819 | }) 820 | const e = expose(c) 821 | { 822 | const p = c.fetch(1) 823 | const index = e.keyMap.get(1) as number 824 | t.not(index, undefined) 825 | const bf = e.valList[index] as BackgroundFetch 826 | t.type(bf, Promise, 'pending fetch') 827 | t.equal(bf.hasOwnProperty('__staleWhileFetching'), true) 828 | t.equal(c.has(1), false) 829 | clock.advance(10) 830 | const res = await p 831 | t.equal(res, 1) 832 | t.equal(c.has(1), true) 833 | } 834 | 835 | { 836 | // background fetch that DOES have a __staleWhileFetching value 837 | const p = c.fetch(1, { forceRefresh: true }) 838 | const index = e.keyMap.get(1) as number 839 | t.not(index, undefined) 840 | const bf = e.valList[index] as BackgroundFetch 841 | t.type(bf, Promise, 'pending fetch') 842 | t.equal(bf.__staleWhileFetching, 1) 843 | t.equal(c.has(1), true) 844 | clock.advance(10) 845 | const res = await p 846 | t.equal(res, 1) 847 | t.equal(c.has(1), true) 848 | } 849 | }) 850 | 851 | t.test('properly dispose when using fetch', async t => { 852 | const disposes: [number, number, string][] = [] 853 | const disposeAfters: [number, number, string][] = [] 854 | let i = 0 855 | const c = new LRUCache({ 856 | max: 3, 857 | ttl: 10, 858 | dispose: (key, val, reason) => disposes.push([key, val, reason]), 859 | disposeAfter: (key, val, reason) => 860 | disposeAfters.push([key, val, reason]), 861 | fetchMethod: async () => Promise.resolve(i++), 862 | }) 863 | t.equal(await c.fetch(1), 0) 864 | clock.advance(20) 865 | t.equal(await c.fetch(1), 1) 866 | t.strictSame(disposes, [[0, 1, 'set']]) 867 | t.strictSame(disposeAfters, [[0, 1, 'set']]) 868 | }) 869 | -------------------------------------------------------------------------------- /test/find.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache as LRU } from '../dist/esm/index.js' 3 | 4 | const resolves: Record< 5 | number, 6 | (v: { value: number } | Promise<{ value: number }>) => void 7 | > = {} 8 | const c = new LRU({ 9 | max: 5, 10 | ttl: 1, 11 | fetchMethod: k => 12 | new Promise<{ value: number }>(res => (resolves[k] = res)), 13 | allowStale: true, 14 | noDeleteOnStaleGet: true, 15 | }) 16 | 17 | for (let i = 0; i < 9; i++) { 18 | c.set(i, { value: i }) 19 | } 20 | 21 | const p = c.fetch(8, { forceRefresh: true }) 22 | 23 | t.equal( 24 | c.find(o => o.value === 4), 25 | c.get(4) 26 | ) 27 | 28 | t.equal( 29 | c.find(o => o.value === 9), 30 | undefined 31 | ) 32 | 33 | t.same( 34 | c.find(o => o.value === 8), 35 | { value: 8 } 36 | ) 37 | 38 | resolves[8]?.({ value: 10 }) 39 | 40 | new Promise(setImmediate) 41 | .then(() => p) 42 | .then(() => { 43 | t.same( 44 | c.find(o => o.value === 10), 45 | c.get(8) 46 | ) 47 | }) 48 | 49 | const p99 = c.fetch(99) 50 | t.equal( 51 | c.find(o => o.value === 99), 52 | undefined 53 | ) 54 | resolves[99]?.({ value: 99 }) 55 | t.equal( 56 | c.find(o => o.value === 99), 57 | undefined 58 | ) 59 | new Promise(setImmediate) 60 | .then(() => p99) 61 | .then(() => { 62 | t.same( 63 | c.find(o => o.value === 99), 64 | { value: 99 } 65 | ) 66 | t.equal( 67 | c.find(o => o.value === 99), 68 | c.get(99) 69 | ) 70 | }) 71 | -------------------------------------------------------------------------------- /test/fixtures/expose.ts: -------------------------------------------------------------------------------- 1 | import { LRUCache } from '../../dist/esm/index.js' 2 | export const expose = < 3 | K extends {}, 4 | V extends {}, 5 | FC extends unknown = unknown 6 | >( 7 | cache: LRUCache, 8 | LRU = LRUCache 9 | ) => { 10 | return Object.assign(LRU.unsafeExposeInternals(cache), cache) 11 | } 12 | -------------------------------------------------------------------------------- /test/import.mjs: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | t.test('import', async t => { 3 | const imp = await import('../dist/esm/index.js') 4 | t.equal(Object.getPrototypeOf(imp), null, 'import returns null obj') 5 | t.equal( 6 | typeof imp.LRUCache, 7 | 'function', 8 | 'LRUCache export is function' 9 | ) 10 | t.equal( 11 | imp.LRUCache.toString().split(/\r?\n/)[0].trim(), 12 | 'class LRUCache {' 13 | ) 14 | }) 15 | -------------------------------------------------------------------------------- /test/info.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache } from '../dist/esm/index.js' 3 | 4 | t.test('just kv', t => { 5 | const c = new LRUCache({ max: 2 }) 6 | c.set(1, 10) 7 | c.set(2, 20) 8 | c.set(3, 30) 9 | t.equal(c.info(1), undefined) 10 | t.strictSame(c.info(2), { value: 20 }) 11 | t.strictSame(c.info(3), { value: 30 }) 12 | t.end() 13 | }) 14 | 15 | t.test('other info', t => { 16 | const c = new LRUCache({ 17 | max: 2, 18 | ttl: 1000, 19 | maxSize: 10000, 20 | }) 21 | c.set(1, 10, { size: 100 }) 22 | c.set(2, 20, { size: 200 }) 23 | c.set(3, 30, { size: 300 }) 24 | t.equal(c.info(1), undefined) 25 | t.match(c.info(2), { 26 | value: 20, 27 | size: 200, 28 | ttl: Number, 29 | start: Number, 30 | }) 31 | t.match(c.info(3), { 32 | value: 30, 33 | size: 300, 34 | ttl: Number, 35 | start: Number, 36 | }) 37 | t.end() 38 | }) 39 | -------------------------------------------------------------------------------- /test/load-check.ts: -------------------------------------------------------------------------------- 1 | process.env.TAP_BAIL = '1' 2 | import t from 'tap' 3 | import { LRUCache as LRU } from '../dist/esm/index.js' 4 | import { expose } from './fixtures/expose.js' 5 | 6 | const max = 10000 7 | const cache = new LRU({ max }) 8 | 9 | import crypto from 'crypto' 10 | const getVal = () => [ 11 | crypto.randomBytes(12).toString('hex'), 12 | crypto.randomBytes(12).toString('hex'), 13 | crypto.randomBytes(12).toString('hex'), 14 | crypto.randomBytes(12).toString('hex'), 15 | ] 16 | 17 | const seeds = new Array(max * 3) 18 | // fill up the cache to start 19 | for (let i = 0; i < max * 3; i++) { 20 | const v = getVal() 21 | seeds[i] = [v.join(':'), v] 22 | } 23 | t.pass('generated seed data') 24 | 25 | const verifyCache = () => { 26 | // walk down the internal list ensuring that every key is the key to that 27 | // index in the keyMap, and the value matches. 28 | const e = expose(cache) 29 | for (const [k, i] of e.keyMap.entries()) { 30 | const v = e.valList[i] as number[] 31 | const key = e.keyList[i] 32 | if (k !== key) { 33 | t.equal(k, key, 'key at proper index', { k, i }) 34 | } 35 | if (v.join(':') !== k) { 36 | t.equal(k, v.join(':'), 'proper value at index', { v, i }) 37 | } 38 | } 39 | } 40 | 41 | let cycles = 0 42 | const cycleLength = Math.floor(max / 100) 43 | while (cycles < max * 5) { 44 | const r = Math.floor(Math.random() * seeds.length) 45 | const seed = seeds[r] 46 | const v = cache.get(seed[0]) 47 | if (v === undefined) { 48 | cache.set(seed[0], seed[1]) 49 | } else { 50 | t.equal(v.join(':'), seed[0], 'correct get ' + cycles, { 51 | seed, 52 | v, 53 | }) 54 | } 55 | if (++cycles % cycleLength === 0) { 56 | verifyCache() 57 | t.pass('cycle check ' + cycles) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/load.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache as LRU } from '../dist/esm/index.js' 3 | 4 | const c = new LRU({ max: 5 }) 5 | for (let i = 0; i < 9; i++) { 6 | c.set(i, i) 7 | } 8 | 9 | const d = new LRU(c) 10 | d.load(c.dump()) 11 | 12 | t.strictSame(d, c) 13 | -------------------------------------------------------------------------------- /test/map-like.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { Clock } from 'clock-mock' 3 | const clock = new Clock() 4 | t.teardown(clock.enter()) 5 | 6 | import { LRUCache as LRU } from '../dist/esm/index.js' 7 | import { expose } from './fixtures/expose.js' 8 | 9 | const entriesFromForeach = ( 10 | c: LRU 11 | ): [k: K, v: V][] => { 12 | const e: [k: K, v: V][] = [] 13 | c.forEach((v, k) => e.push([k, v])) 14 | return e 15 | } 16 | const entriesFromRForeach = ( 17 | c: LRU 18 | ): [k: K, v: V][] => { 19 | const e: [k: K, v: V][] = [] 20 | c.rforEach((v, k) => e.push([k, v])) 21 | return e 22 | } 23 | 24 | t.test('bunch of iteration things', async t => { 25 | const resolves: Record void> = {} 26 | 27 | const c = new LRU({ 28 | max: 5, 29 | maxSize: 5, 30 | sizeCalculation: () => 1, 31 | fetchMethod: k => new Promise(resolve => (resolves[k] = resolve)), 32 | }) 33 | 34 | t.matchSnapshot(c.keys(), 'empty, keys') 35 | t.matchSnapshot(c.values(), 'empty, values') 36 | t.matchSnapshot(c.entries(), 'empty, entries') 37 | t.matchSnapshot(entriesFromForeach(c), 'empty, foreach') 38 | t.matchSnapshot(c.rkeys(), 'empty, rkeys') 39 | t.matchSnapshot(c.rvalues(), 'empty, rvalues') 40 | t.matchSnapshot(c.rentries(), 'empty, rentries') 41 | t.matchSnapshot(entriesFromRForeach(c), 'empty, rforeach') 42 | t.matchSnapshot(c.dump(), 'empty, dump') 43 | 44 | const p99 = c.fetch(99) 45 | const testp99 = t.rejects(p99, 'aborted by eviction') 46 | const p123 = c.fetch(123) 47 | 48 | t.matchSnapshot(c.keys(), 'pending fetch, keys') 49 | t.matchSnapshot(c.values(), 'pending fetch, values') 50 | t.matchSnapshot(c.entries(), 'pending fetch, entries') 51 | t.matchSnapshot(entriesFromForeach(c), 'pending fetch, foreach') 52 | t.matchSnapshot(c.rkeys(), 'pending fetch, rkeys') 53 | t.matchSnapshot(c.rvalues(), 'pending fetch, rvalues') 54 | t.matchSnapshot(c.rentries(), 'pending fetch, rentries') 55 | t.matchSnapshot(entriesFromRForeach(c), 'pending fetch, rforeach') 56 | t.matchSnapshot(c.dump(), 'pending fetch, dump') 57 | 58 | for (let i = 0; i < 3; i++) { 59 | c.set(i, String(i)) 60 | } 61 | 62 | resolves[123]?.('123') 63 | t.equal(await p123, '123') 64 | t.matchSnapshot(c.keys(), 'fetch 123 resolved, keys') 65 | t.matchSnapshot(c.values(), 'fetch 123 resolved, values') 66 | t.matchSnapshot(c.entries(), 'fetch 123 resolved, entries') 67 | t.matchSnapshot( 68 | entriesFromForeach(c), 69 | 'fetch 123 resolved, foreach' 70 | ) 71 | t.matchSnapshot(c.rkeys(), 'fetch 123 resolved, rkeys') 72 | t.matchSnapshot(c.rvalues(), 'fetch 123 resolved, rvalues') 73 | t.matchSnapshot(c.rentries(), 'fetch 123 resolved, rentries') 74 | t.matchSnapshot( 75 | entriesFromRForeach(c), 76 | 'fetch 123 resolved, rforeach' 77 | ) 78 | t.matchSnapshot(c.dump(), 'fetch 123 resolved, dump') 79 | 80 | for (let i = 3; i < 8; i++) { 81 | c.set(i, String(i)) 82 | } 83 | 84 | t.matchSnapshot(c.keys(), 'keys') 85 | t.matchSnapshot(c.values(), 'values') 86 | t.matchSnapshot(c.entries(), 'entries') 87 | t.matchSnapshot(c.rkeys(), 'rkeys') 88 | t.matchSnapshot(c.rvalues(), 'rvalues') 89 | t.matchSnapshot(c.rentries(), 'rentries') 90 | t.matchSnapshot(c.dump(), 'dump') 91 | 92 | c.set(4, 'new value 4') 93 | t.matchSnapshot(c.keys(), 'keys, new value 4') 94 | t.matchSnapshot(c.values(), 'values, new value 4') 95 | t.matchSnapshot(c.entries(), 'entries, new value 4') 96 | t.matchSnapshot(c.rkeys(), 'rkeys, new value 4') 97 | t.matchSnapshot(c.rvalues(), 'rvalues, new value 4') 98 | t.matchSnapshot(c.rentries(), 'rentries, new value 4') 99 | t.matchSnapshot(c.dump(), 'dump, new value 4') 100 | 101 | resolves[99]?.('99') 102 | await testp99 103 | t.matchSnapshot(c.keys(), 'keys, resolved fetch 99 too late') 104 | t.matchSnapshot(c.values(), 'values, resolved fetch 99 too late') 105 | t.matchSnapshot(c.entries(), 'entries, resolved fetch 99 too late') 106 | t.matchSnapshot(c.rkeys(), 'rkeys, resolved fetch 99 too late') 107 | t.matchSnapshot(c.rvalues(), 'rvalues, resolved fetch 99 too late') 108 | t.matchSnapshot( 109 | c.rentries(), 110 | 'rentries, resolved fetch 99 too late' 111 | ) 112 | t.matchSnapshot(c.dump(), 'dump, resolved fetch 99 too late') 113 | 114 | // pretend an entry is stale for some reason 115 | c.set(7, 'stale', { ttl: 1, size: 1 }) 116 | const e = expose(c) 117 | const idx = e.keyMap.get(7) 118 | if (!e.starts) throw new Error('no starts??') 119 | e.starts[idx as number] = clock.now() - 10000 120 | const seen: number[] = [] 121 | for (const i of e.indexes()) { 122 | seen[i] = seen[i] || 0 123 | seen[i]++ 124 | if ((seen[i] as number) > 2) { 125 | throw new Error('cycle on ' + i) 126 | } 127 | } 128 | seen.length = 0 129 | for (const i of e.rindexes()) { 130 | seen[i] = seen[i] || 0 131 | seen[i]++ 132 | if ((seen[i] as number) > 2) { 133 | throw new Error('cycle on ' + i) 134 | } 135 | } 136 | t.matchSnapshot(c.keys(), 'keys, 7 stale') 137 | t.matchSnapshot(c.values(), 'values, 7 stale') 138 | t.matchSnapshot(c.entries(), 'entries, 7 stale') 139 | t.matchSnapshot(c.rkeys(), 'rkeys, 7 stale') 140 | t.matchSnapshot(c.rvalues(), 'rvalues, 7 stale') 141 | t.matchSnapshot(c.rentries(), 'rentries, 7 stale') 142 | t.matchSnapshot(c.dump(), 'dump, 7 stale') 143 | 144 | const feArr: any[] = [] 145 | c.forEach((value, key) => feArr.push([value, key])) 146 | t.matchSnapshot(feArr, 'forEach, no thisp') 147 | const rfeArr: any[] = [] 148 | c.rforEach((value, key) => rfeArr.push([value, key])) 149 | t.matchSnapshot(rfeArr, 'rforEach, no thisp') 150 | const feArrThisp: any[] = [] 151 | const thisp = { a: 1 } 152 | c.forEach(function (this: typeof thisp, value, key) { 153 | feArrThisp.push([value, key, this]) 154 | }, thisp) 155 | t.matchSnapshot(feArrThisp, 'forEach, with thisp') 156 | const rfeArrThisp: any[] = [] 157 | const rthisp = { r: 1 } 158 | c.rforEach(function (this: typeof thisp, value, key) { 159 | rfeArrThisp.push([value, key, this]) 160 | }, rthisp) 161 | t.matchSnapshot(rfeArrThisp, 'forEach, with thisp') 162 | 163 | // when cache is empty, these should do nothing 164 | const empty = new LRU({ max: 10 }) 165 | empty.forEach(() => { 166 | throw new Error('fail empty forEach') 167 | }) 168 | empty.rforEach(() => { 169 | throw new Error('fail empty rforEach') 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /test/move-to-tail.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache as LRU } from '../dist/esm/index.js' 3 | import { expose } from './fixtures/expose.js' 4 | 5 | const c = new LRU({ max: 5 }) 6 | const exp = expose(c) 7 | 8 | t.test('list integrity', { bail: true }, t => { 9 | const e = (index: number) => ({ 10 | index, 11 | prev: exp.prev[index], 12 | _: 13 | index === exp.tail 14 | ? 'T' 15 | : index === exp.head 16 | ? 'H' 17 | : '' + index, 18 | next: exp.next[index], 19 | head: exp.head, 20 | tail: exp.tail, 21 | }) 22 | const snap = () => { 23 | const a: ReturnType[] = [] 24 | for (let i = 0; i < 5; i++) { 25 | a.push(e(i)) 26 | } 27 | return a 28 | } 29 | const integrity = (msg: string) => { 30 | t.test(msg, { bail: false }, t => { 31 | for (let i = 0; i < c.max; i++) { 32 | if (i !== exp.head) { 33 | t.equal(exp.next[exp.prev[i] as number], i, 'n[p[i]] === i') 34 | } 35 | if (i !== exp.tail) { 36 | t.equal(exp.prev[exp.next[i] as number], i, 'p[n[i]] === i') 37 | } 38 | } 39 | t.end() 40 | }) 41 | } 42 | 43 | for (let i = 0; i < 5; i++) { 44 | c.set(i, i) 45 | } 46 | 47 | t.matchSnapshot(snap(), 'list after initial fill') 48 | integrity('after initial fill') 49 | exp.moveToTail(2) 50 | t.matchSnapshot(snap(), 'list after moveToTail 2') 51 | integrity('after moveToTail 2') 52 | exp.moveToTail(4) 53 | t.matchSnapshot(snap(), 'list after moveToTail 4') 54 | integrity('after moveToTail 4') 55 | 56 | t.end() 57 | }) 58 | -------------------------------------------------------------------------------- /test/pop.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache as LRU } from '../dist/esm/index.js' 3 | 4 | const cache = new LRU({ max: 5 }) 5 | for (let i = 0; i < 5; i++) { 6 | cache.set(i, i) 7 | } 8 | cache.get(2) 9 | const popped: (number | undefined)[] = [] 10 | let p: number | undefined 11 | do { 12 | p = cache.pop() 13 | popped.push(p) 14 | } while (p !== undefined) 15 | t.same(popped, [0, 1, 3, 4, 2, undefined]) 16 | 17 | t.test('pop with background fetches', async t => { 18 | const resolves: Record void> = {} 19 | let aborted = false 20 | const f = new LRU({ 21 | max: 5, 22 | ttl: 10, 23 | fetchMethod: (k: number, _v, { signal }) => { 24 | signal.addEventListener('abort', () => (aborted = true)) 25 | return new Promise(res => (resolves[k] = res)) 26 | }, 27 | }) 28 | 29 | // a fetch that's in progress with no stale val gets popped 30 | // without returning anything 31 | f.set(0, 0) 32 | let pf = f.fetch(1) 33 | f.set(2, 2) 34 | t.equal(f.size, 3) 35 | t.equal(f.pop(), 0) 36 | t.equal(f.size, 2) 37 | t.equal(f.pop(), 2) 38 | t.equal(f.size, 0) 39 | t.equal(aborted, true) 40 | resolves[1]?.(1) 41 | await t.rejects(pf) 42 | 43 | f.set(0, 0, { ttl: 0 }) 44 | f.set(1, 111) 45 | await new Promise(r => setTimeout(r, 20)) 46 | pf = f.fetch(1) 47 | f.set(2, 2, { ttl: 0 }) 48 | t.equal(f.size, 3) 49 | t.equal(f.pop(), 0) 50 | t.equal(f.size, 2) 51 | t.equal(f.pop(), 111) 52 | t.equal(f.size, 1) 53 | t.equal(f.pop(), 2) 54 | t.equal(f.size, 0) 55 | resolves[1]?.(1) 56 | await t.rejects(pf) 57 | }) 58 | 59 | t.test('pop calls dispose and disposeAfter', t => { 60 | let disposeCalled = 0 61 | let disposeAfterCalled = 0 62 | const c = new LRU({ 63 | max: 5, 64 | dispose: () => disposeCalled++, 65 | disposeAfter: () => disposeAfterCalled++, 66 | }) 67 | c.set(0, 0) 68 | c.set(1, 1) 69 | c.set(2, 2) 70 | t.equal(c.pop(), 0) 71 | t.equal(c.pop(), 1) 72 | t.equal(c.pop(), 2) 73 | t.equal(c.pop(), undefined) 74 | t.equal(c.size, 0) 75 | t.equal(disposeCalled, 3) 76 | t.equal(disposeAfterCalled, 3) 77 | t.end() 78 | }) 79 | -------------------------------------------------------------------------------- /test/purge-stale-exhaustive.ts: -------------------------------------------------------------------------------- 1 | if (typeof performance === 'undefined') { 2 | Object.assign(global, { 3 | performance: (await import('perf_hooks')).performance, 4 | }) 5 | } 6 | 7 | import { Clock } from 'clock-mock' 8 | import assert from 'node:assert' 9 | import t from 'tap' 10 | import { LRUCache as LRU } from '../dist/esm/index.js' 11 | import { expose } from './fixtures/expose.js' 12 | 13 | const clock = new Clock() 14 | clock.advance(1) 15 | 16 | const boolOpts = (n: number): number[][] => { 17 | const mask = Math.pow(2, n) 18 | const arr: number[][] = [] 19 | for (let i = 0; i < mask; i++) { 20 | arr.push( 21 | (mask + i) 22 | .toString(2) 23 | .slice(1) 24 | .split('') 25 | .map(n => +n) 26 | ) 27 | } 28 | return arr 29 | } 30 | 31 | const permute = (arr: number[] | number): number[][] => { 32 | if (typeof arr === 'number') { 33 | return permute(Object.keys(new Array(arr).fill('')).map(n => +n)) 34 | } 35 | if (arr.length === 1) { 36 | return [arr] 37 | } 38 | const permutations = [] 39 | // recurse over selecting any of the items 40 | for (let i = 0; i < arr.length; i++) { 41 | const items = arr.slice(0) 42 | const item = items.splice(i, 1) 43 | permutations.push( 44 | ...permute(items).map(perm => item.concat(perm)) 45 | ) 46 | } 47 | return permutations 48 | } 49 | 50 | const runTestStep = ({ 51 | order, 52 | stales = -1, 53 | len, 54 | }: { 55 | order: number[] 56 | stales?: number[] | -1 57 | len: number 58 | }) => { 59 | // generate stales at this level because it's faster that way, 60 | // fewer tap pieces to prop it all up. 61 | if (stales === -1) { 62 | for (const stales of boolOpts(len)) { 63 | runTestStep({ order, stales, len }) 64 | } 65 | return true 66 | } 67 | 68 | clock.enter() 69 | const c = new LRU({ max: len, ttl: 100 }) 70 | const e = expose(c) 71 | // fill the array with index matching k/v 72 | for (let i = 0; i < len; i++) { 73 | if (stales[i]) { 74 | c.set(i, i, { ttl: 1 }) 75 | } else { 76 | c.set(i, i) 77 | } 78 | } 79 | 80 | // now get() items to reorder 81 | for (const index of order) { 82 | c.get(index) 83 | } 84 | 85 | assert.deepEqual([...e.rindexes()], order, 'got expected ordering') 86 | 87 | // advance clock so masked go stale 88 | clock.advance(10) 89 | c.purgeStale() 90 | assert.deepEqual( 91 | [...e.rindexes()], 92 | [...e.rindexes({ allowStale: true })] 93 | ) 94 | // make all go stale 95 | clock.advance(100) 96 | c.purgeStale() 97 | assert.deepEqual([...e.rindexes({ allowStale: true })], []) 98 | clock.exit() 99 | return true 100 | } 101 | 102 | t.test('exhaustive tests', t => { 103 | // this is a brutal test. 104 | // Generate every possible ordering of indexes. 105 | // then for each ordering, generate every possible arrangement of staleness 106 | // Verify that purgeStale produces the correct result every time. 107 | const len = 5 108 | for (const order of permute(len)) { 109 | const name = `order=${order.join('')}` 110 | t.test(name, t => { 111 | t.plan(1) 112 | runTestStep({ order, len }) 113 | t.pass('no problems') 114 | }) 115 | } 116 | t.end() 117 | }) 118 | -------------------------------------------------------------------------------- /test/reverse-iterate-delete-all.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/isaacs/node-lru-cache/issues/278 2 | import t from 'tap' 3 | import { LRUCache as LRU } from '../dist/esm/index.js' 4 | const lru = new LRU({ 5 | maxSize: 2, 6 | sizeCalculation: () => 1, 7 | }) 8 | lru.set('x', 'x') 9 | lru.set('y', 'y') 10 | for (const key of lru.rkeys()) { 11 | lru.delete(key) 12 | } 13 | t.equal(lru.size, 0) 14 | -------------------------------------------------------------------------------- /test/size-calculation.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache as LRU } from '../dist/esm/index.js' 3 | 4 | import { expose } from './fixtures/expose.js' 5 | 6 | const checkSize = (c: LRU) => { 7 | const e = expose(c) 8 | const sizes = e.sizes 9 | if (!sizes) throw new Error('no sizes??') 10 | const { calculatedSize, maxSize } = c 11 | const sum = [...sizes].reduce((a, b) => a + b, 0) 12 | if (sum !== calculatedSize) { 13 | console.error({ sum, calculatedSize, sizes }, c, e) 14 | throw new Error('calculatedSize does not equal sum of sizes') 15 | } 16 | if (calculatedSize > maxSize) { 17 | throw new Error('max size exceeded') 18 | } 19 | } 20 | 21 | t.test('store strings, size = length', t => { 22 | const c = new LRU({ 23 | max: 100, 24 | maxSize: 100, 25 | sizeCalculation: n => n.length, 26 | }) 27 | 28 | checkSize(c) 29 | c.set(5, 'x'.repeat(5)) 30 | checkSize(c) 31 | c.set(10, 'x'.repeat(10)) 32 | checkSize(c) 33 | c.set(20, 'x'.repeat(20)) 34 | checkSize(c) 35 | t.equal(c.calculatedSize, 35) 36 | c.delete(20) 37 | checkSize(c) 38 | t.equal(c.calculatedSize, 15) 39 | c.delete(5) 40 | checkSize(c) 41 | t.equal(c.calculatedSize, 10) 42 | c.clear() 43 | checkSize(c) 44 | t.equal(c.calculatedSize, 0) 45 | 46 | const s = 'x'.repeat(10) 47 | for (let i = 0; i < 5; i++) { 48 | c.set(i, s) 49 | checkSize(c) 50 | } 51 | t.equal(c.calculatedSize, 50) 52 | 53 | // the big item goes in, but triggers a prune 54 | // we don't preemptively prune until we *cross* the max 55 | c.set('big', 'x'.repeat(100)) 56 | checkSize(c) 57 | t.equal(c.calculatedSize, 100) 58 | // override the size on set 59 | c.set('big', 'y'.repeat(100), { sizeCalculation: () => 10 }) 60 | checkSize(c) 61 | t.equal(c.size, 1) 62 | checkSize(c) 63 | t.equal(c.calculatedSize, 10) 64 | checkSize(c) 65 | c.delete('big') 66 | checkSize(c) 67 | t.equal(c.size, 0) 68 | t.equal(c.calculatedSize, 0) 69 | 70 | c.set('repeated', 'i'.repeat(10)) 71 | checkSize(c) 72 | c.set('repeated', 'j'.repeat(10)) 73 | checkSize(c) 74 | c.set('repeated', 'i'.repeat(10)) 75 | checkSize(c) 76 | c.set('repeated', 'j'.repeat(10)) 77 | checkSize(c) 78 | c.set('repeated', 'i'.repeat(10)) 79 | checkSize(c) 80 | c.set('repeated', 'j'.repeat(10)) 81 | checkSize(c) 82 | c.set('repeated', 'i'.repeat(10)) 83 | checkSize(c) 84 | c.set('repeated', 'j'.repeat(10)) 85 | checkSize(c) 86 | t.equal(c.size, 1) 87 | t.equal(c.calculatedSize, 10) 88 | t.equal(c.get('repeated'), 'j'.repeat(10)) 89 | t.matchSnapshot(c.dump(), 'dump') 90 | 91 | t.end() 92 | }) 93 | 94 | t.test('bad size calculation fn throws on set()', t => { 95 | const c = new LRU({ 96 | max: 5, 97 | maxSize: 5, 98 | // @ts-expect-error 99 | sizeCalculation: () => { 100 | return 'asdf' 101 | }, 102 | }) 103 | t.throws( 104 | () => c.set(1, '1'.repeat(100)), 105 | new TypeError( 106 | 'sizeCalculation return invalid (expect positive integer)' 107 | ) 108 | ) 109 | t.throws(() => { 110 | // @ts-expect-error 111 | c.set(1, '1', { size: 'asdf', sizeCalculation: null }) 112 | }, new TypeError('invalid size value (must be positive integer)')) 113 | t.throws(() => { 114 | // @ts-expect-error 115 | c.set(1, '1', { sizeCalculation: 'asdf' }) 116 | }, new TypeError('sizeCalculation must be a function')) 117 | t.end() 118 | }) 119 | 120 | t.test('delete while empty, or missing key, is no-op', t => { 121 | const c = new LRU({ max: 5, maxSize: 10, sizeCalculation: () => 2 }) 122 | checkSize(c) 123 | c.set(1, 1) 124 | checkSize(c) 125 | t.equal(c.size, 1) 126 | t.equal(c.calculatedSize, 2) 127 | c.clear() 128 | checkSize(c) 129 | t.equal(c.size, 0) 130 | t.equal(c.calculatedSize, 0) 131 | c.delete(1) 132 | checkSize(c) 133 | t.equal(c.size, 0) 134 | t.equal(c.calculatedSize, 0) 135 | 136 | c.set(1, 1) 137 | checkSize(c) 138 | c.set(1, 1) 139 | checkSize(c) 140 | c.set(1, 1) 141 | checkSize(c) 142 | t.equal(c.size, 1) 143 | t.equal(c.calculatedSize, 2) 144 | c.delete(99) 145 | checkSize(c) 146 | t.equal(c.size, 1) 147 | t.equal(c.calculatedSize, 2) 148 | c.delete(1) 149 | checkSize(c) 150 | t.equal(c.size, 0) 151 | t.equal(c.calculatedSize, 0) 152 | c.delete(1) 153 | checkSize(c) 154 | t.equal(c.size, 0) 155 | t.equal(c.calculatedSize, 0) 156 | t.end() 157 | }) 158 | 159 | t.test('large item falls out of cache, sizes are kept correct', t => { 160 | const statuses: LRU.Status[] = [] 161 | const s = (): LRU.Status => { 162 | const status: LRU.Status = {} 163 | statuses.push(status) 164 | return status 165 | } 166 | 167 | const c = new LRU({ 168 | maxSize: 10, 169 | sizeCalculation: () => 100, 170 | }) 171 | const sizes = expose(c).sizes 172 | 173 | checkSize(c) 174 | t.equal(c.size, 0) 175 | t.equal(c.calculatedSize, 0) 176 | t.same(sizes, []) 177 | 178 | c.set(2, 2, { size: 2, status: s() }) 179 | checkSize(c) 180 | t.equal(c.size, 1) 181 | t.equal(c.calculatedSize, 2) 182 | t.same(sizes, [2]) 183 | 184 | c.delete(2) 185 | checkSize(c) 186 | t.equal(c.size, 0) 187 | t.equal(c.calculatedSize, 0) 188 | t.same(sizes, [0]) 189 | 190 | c.set(1, 1, { status: s() }) 191 | checkSize(c) 192 | t.equal(c.size, 0) 193 | t.equal(c.calculatedSize, 0) 194 | t.same(sizes, [0]) 195 | 196 | c.set(3, 3, { size: 3, status: s() }) 197 | checkSize(c) 198 | t.equal(c.size, 1) 199 | t.equal(c.calculatedSize, 3) 200 | t.same(sizes, [3]) 201 | 202 | c.set(4, 4, { status: s() }) 203 | checkSize(c) 204 | t.equal(c.size, 1) 205 | t.equal(c.calculatedSize, 3) 206 | t.same(sizes, [3]) 207 | 208 | t.matchSnapshot(statuses, 'status updates') 209 | t.end() 210 | }) 211 | 212 | t.test('large item falls out of cache because maxEntrySize', t => { 213 | const statuses: LRU.Status[] = [] 214 | const s = (): LRU.Status => { 215 | const status: LRU.Status = {} 216 | statuses.push(status) 217 | return status 218 | } 219 | 220 | const c = new LRU({ 221 | maxSize: 1000, 222 | maxEntrySize: 10, 223 | sizeCalculation: () => 100, 224 | }) 225 | const sizes = expose(c).sizes 226 | 227 | checkSize(c) 228 | t.equal(c.size, 0) 229 | t.equal(c.calculatedSize, 0) 230 | t.same(sizes, []) 231 | 232 | c.set(2, 2, { size: 2, status: s() }) 233 | checkSize(c) 234 | t.equal(c.size, 1) 235 | t.equal(c.calculatedSize, 2) 236 | t.same(sizes, [2]) 237 | 238 | c.delete(2) 239 | checkSize(c) 240 | t.equal(c.size, 0) 241 | t.equal(c.calculatedSize, 0) 242 | t.same(sizes, [0]) 243 | 244 | c.set(1, 1, { status: s() }) 245 | checkSize(c) 246 | t.equal(c.size, 0) 247 | t.equal(c.calculatedSize, 0) 248 | t.same(sizes, [0]) 249 | 250 | c.set(3, 3, { size: 3, status: s() }) 251 | checkSize(c) 252 | t.equal(c.size, 1) 253 | t.equal(c.calculatedSize, 3) 254 | t.same(sizes, [3]) 255 | 256 | c.set(4, 4, { status: s() }) 257 | checkSize(c) 258 | t.equal(c.size, 1) 259 | t.equal(c.calculatedSize, 3) 260 | t.same(sizes, [3]) 261 | 262 | t.matchSnapshot(statuses, 'status updates') 263 | t.end() 264 | }) 265 | 266 | t.test('maxEntrySize, no maxSize', async t => { 267 | const c = new LRU({ 268 | max: 10, 269 | maxEntrySize: 10, 270 | sizeCalculation: s => s.length, 271 | fetchMethod: async n => 'x'.repeat(n), 272 | }) 273 | t.equal(await c.fetch(2), 'xx') 274 | t.equal(c.size, 1) 275 | t.equal(await c.fetch(3), 'xxx') 276 | t.equal(c.size, 2) 277 | t.equal(await c.fetch(11), 'x'.repeat(11)) 278 | t.equal(c.size, 2) 279 | t.equal(c.has(11), false) 280 | }) 281 | -------------------------------------------------------------------------------- /test/ttl.ts: -------------------------------------------------------------------------------- 1 | if (typeof performance === 'undefined') { 2 | global.performance = require('perf_hooks').performance 3 | } 4 | import t, { Test } from 'tap' 5 | import { LRUCache } from '../dist/esm/index.js' 6 | import { expose } from './fixtures/expose.js' 7 | 8 | import { Clock } from 'clock-mock' 9 | const clock = new Clock() 10 | 11 | const runTests = (LRU: typeof LRUCache, t: Test) => { 12 | const statuses: LRUCache.Status[] = [] 13 | const s = (): LRUCache.Status => { 14 | const status: LRUCache.Status = {} 15 | statuses.push(status) 16 | return status 17 | } 18 | 19 | const { setTimeout, clearTimeout } = global 20 | t.teardown(() => 21 | // @ts-ignore 22 | Object.assign(global, { setTimeout, clearTimeout }) 23 | ) 24 | //@ts-ignore 25 | global.setTimeout = clock.setTimeout.bind(clock) 26 | //@ts-ignore 27 | global.clearTimeout = clock.clearTimeout.bind(clock) 28 | 29 | t.test('ttl tests defaults', t => { 30 | statuses.length = 0 31 | // have to advance it 1 so we don't start with 0 32 | // NB: this module will misbehave if you create an entry at a 33 | // clock time of 0, for example if you are filling an LRU cache 34 | // in a node lacking perf_hooks, at midnight UTC on 1970-01-01. 35 | // This is a known bug that I am ok with. 36 | clock.advance(1) 37 | const c = new LRU({ max: 5, ttl: 10, ttlResolution: 0 }) 38 | const e = expose(c, LRU) 39 | c.set(1, 1, { status: s() }) 40 | t.equal(c.get(1, { status: s() }), 1, '1 get not stale', { 41 | now: clock.now(), 42 | }) 43 | clock.advance(5) 44 | t.equal(c.get(1, { status: s() }), 1, '1 get not stale', { 45 | now: clock.now(), 46 | }) 47 | t.equal(c.getRemainingTTL(1), 5, '5ms left to live') 48 | t.equal( 49 | c.getRemainingTTL('not in cache'), 50 | 0, 51 | 'thing doesnt exist' 52 | ) 53 | clock.advance(5) 54 | t.equal(c.get(1, { status: s() }), 1, '1 get not stale', { 55 | now: clock.now(), 56 | }) 57 | t.equal(c.getRemainingTTL(1), 0, 'almost stale') 58 | clock.advance(1) 59 | t.equal(c.getRemainingTTL(1), -1, 'gone stale') 60 | clock.advance(1) 61 | t.equal(c.getRemainingTTL(1), -2, 'even more stale') 62 | t.equal(c.size, 1, 'still there though') 63 | t.equal(c.has(1, { status: s() }), false, '1 has stale', { 64 | now: clock.now(), 65 | index: e.keyMap.get(1), 66 | stale: e.isStale(e.keyMap.get(1)), 67 | }) 68 | t.equal(c.get(1, { status: s() }), undefined) 69 | t.equal(c.size, 0) 70 | 71 | c.set(2, 2, { ttl: 100 }) 72 | clock.advance(50) 73 | t.equal(c.has(2, { status: s() }), true) 74 | t.equal(c.get(2, { status: s() }), 2) 75 | clock.advance(51) 76 | t.equal(c.has(2), false) 77 | t.equal(c.get(2, { status: s() }), undefined) 78 | 79 | c.clear() 80 | for (let i = 0; i < 9; i++) { 81 | c.set(i, i, { status: s() }) 82 | } 83 | // now we have 9 items 84 | // get an expired item from old set 85 | clock.advance(11) 86 | t.equal(c.peek(4), undefined) 87 | t.equal(c.has(4, { status: s() }), false) 88 | t.equal(c.get(4, { status: s() }), undefined) 89 | 90 | // set an item WITHOUT a ttl on it 91 | c.set('immortal', true, { ttl: 0 }) 92 | clock.advance(100) 93 | t.equal(c.getRemainingTTL('immortal'), Infinity) 94 | t.equal(c.get('immortal', { status: s() }), true) 95 | c.get('immortal', { updateAgeOnGet: true }) 96 | clock.advance(100) 97 | t.equal(c.get('immortal', { status: s() }), true) 98 | t.matchSnapshot(statuses, 'status updates') 99 | t.end() 100 | }) 101 | 102 | t.test('ttl tests with ttlResolution=100', t => { 103 | statuses.length = 0 104 | const c = new LRU({ ttl: 10, ttlResolution: 100, max: 10 }) 105 | const e = expose(c, LRU) 106 | c.set(1, 1, { status: s() }) 107 | t.equal(c.get(1, { status: s() }), 1, '1 get not stale', { 108 | now: clock.now(), 109 | }) 110 | clock.advance(5) 111 | t.equal(c.get(1, { status: s() }), 1, '1 get not stale', { 112 | now: clock.now(), 113 | }) 114 | clock.advance(5) 115 | t.equal(c.get(1, { status: s() }), 1, '1 get not stale', { 116 | now: clock.now(), 117 | }) 118 | clock.advance(1) 119 | t.equal(c.has(1, { status: s() }), true, '1 has stale', { 120 | now: clock.now(), 121 | ttls: e.ttls, 122 | starts: e.starts, 123 | index: e.keyMap.get(1), 124 | stale: e.isStale(e.keyMap.get(1)), 125 | }) 126 | t.equal(c.get(1, { status: s() }), 1) 127 | clock.advance(100) 128 | t.equal(c.has(1, { status: s() }), false, '1 has stale', { 129 | now: clock.now(), 130 | ttls: e.ttls, 131 | starts: e.starts, 132 | index: e.keyMap.get(1), 133 | stale: e.isStale(e.keyMap.get(1)), 134 | }) 135 | t.equal(c.get(1, { status: s() }), undefined) 136 | t.equal(c.size, 0) 137 | t.matchSnapshot(statuses, 'status updates') 138 | t.end() 139 | }) 140 | 141 | t.test( 142 | 'ttlResolution only respected if non-negative integer', 143 | t => { 144 | const invalids = [-1, null, undefined, 'banana', {}] 145 | for (const i of invalids) { 146 | //@ts-expect-error 147 | const c = new LRU({ ttl: 5, ttlResolution: i, max: 5 }) 148 | t.not(c.ttlResolution, i) 149 | t.equal(c.ttlResolution, Math.floor(c.ttlResolution)) 150 | t.ok(c.ttlResolution >= 0) 151 | } 152 | t.end() 153 | } 154 | ) 155 | 156 | t.test('ttlAutopurge', t => { 157 | statuses.length = 0 158 | const c = new LRU({ 159 | ttl: 10, 160 | ttlAutopurge: true, 161 | ttlResolution: 0, 162 | }) 163 | c.set(1, 1, { status: s() }) 164 | c.set(2, 2, { status: s() }) 165 | t.equal(c.size, 2) 166 | c.set(2, 3, { ttl: 11, status: s() }) 167 | clock.advance(11) 168 | t.equal(c.size, 1) 169 | clock.advance(1) 170 | t.equal(c.size, 0) 171 | t.matchSnapshot(statuses, 'status updates') 172 | t.end() 173 | }) 174 | 175 | t.test('ttl on set, not on cache', t => { 176 | statuses.length = 0 177 | const c = new LRU({ max: 5, ttlResolution: 0 }) 178 | c.set(1, 1, { ttl: 10, status: s() }) 179 | t.equal(c.get(1, { status: s() }), 1) 180 | clock.advance(5) 181 | t.equal(c.get(1, { status: s() }), 1) 182 | clock.advance(5) 183 | t.equal(c.get(1, { status: s() }), 1) 184 | clock.advance(1) 185 | t.equal(c.has(1, { status: s() }), false) 186 | t.equal(c.get(1, { status: s() }), undefined) 187 | t.equal(c.size, 0) 188 | 189 | c.set(2, 2, { ttl: 100, status: s() }) 190 | clock.advance(50) 191 | t.equal(c.has(2, { status: s() }), true) 192 | t.equal(c.get(2, { status: s() }), 2) 193 | clock.advance(51) 194 | t.equal(c.has(2, { status: s() }), false) 195 | t.equal(c.get(2, { status: s() }), undefined) 196 | 197 | c.clear() 198 | for (let i = 0; i < 9; i++) { 199 | c.set(i, i, { ttl: 10, status: s() }) 200 | } 201 | // now we have 9 items 202 | // get an expired item from old set 203 | clock.advance(11) 204 | t.equal(c.has(4, { status: s() }), false) 205 | t.equal(c.get(4, { status: s() }), undefined) 206 | 207 | t.matchSnapshot(statuses, 'status updates') 208 | t.end() 209 | }) 210 | 211 | t.test('ttl with allowStale', t => { 212 | const c = new LRU({ 213 | max: 5, 214 | ttl: 10, 215 | allowStale: true, 216 | ttlResolution: 0, 217 | }) 218 | c.set(1, 1) 219 | t.equal(c.get(1), 1) 220 | clock.advance(5) 221 | t.equal(c.get(1), 1) 222 | clock.advance(5) 223 | t.equal(c.get(1), 1) 224 | clock.advance(1) 225 | t.equal(c.has(1), false) 226 | 227 | t.equal(c.get(1, { status: s(), noDeleteOnStaleGet: true }), 1) 228 | t.equal(c.get(1), 1) 229 | t.equal(c.get(1), undefined) 230 | t.equal(c.size, 0) 231 | 232 | c.set(2, 2, { ttl: 100 }) 233 | clock.advance(50) 234 | t.equal(c.has(2), true) 235 | t.equal(c.get(2), 2) 236 | clock.advance(51) 237 | t.equal(c.has(2), false) 238 | t.equal(c.get(2), 2) 239 | t.equal(c.get(2), undefined) 240 | 241 | c.clear() 242 | for (let i = 0; i < 9; i++) { 243 | c.set(i, i) 244 | } 245 | // now we have 9 items 246 | // get an expired item from old set 247 | clock.advance(11) 248 | t.equal(c.has(4), false) 249 | t.equal(c.get(4), 4) 250 | t.equal(c.get(4), undefined) 251 | 252 | t.end() 253 | }) 254 | 255 | t.test('ttl with updateAgeOnGet/updateAgeOnHas', t => { 256 | const c = new LRU({ 257 | max: 5, 258 | ttl: 10, 259 | updateAgeOnGet: true, 260 | updateAgeOnHas: true, 261 | ttlResolution: 0, 262 | }) 263 | c.set(1, 1) 264 | t.equal(c.get(1), 1) 265 | clock.advance(5) 266 | t.equal(c.has(1), true) 267 | clock.advance(5) 268 | t.equal(c.get(1), 1) 269 | clock.advance(1) 270 | t.equal(c.getRemainingTTL(1), 9) 271 | t.equal(c.has(1), true) 272 | t.equal(c.getRemainingTTL(1), 10) 273 | t.equal(c.get(1), 1) 274 | t.equal(c.size, 1) 275 | c.clear() 276 | 277 | c.set(2, 2, { ttl: 100 }) 278 | for (let i = 0; i < 10; i++) { 279 | clock.advance(50) 280 | t.equal(c.has(2), true) 281 | t.equal(c.get(2), 2) 282 | } 283 | clock.advance(101) 284 | t.equal(c.has(2), false) 285 | t.equal(c.get(2), undefined) 286 | 287 | c.clear() 288 | for (let i = 0; i < 9; i++) { 289 | c.set(i, i) 290 | } 291 | // now we have 9 items 292 | // get an expired item 293 | t.equal(c.has(3), false) 294 | t.equal(c.get(3), undefined) 295 | clock.advance(11) 296 | t.equal(c.has(4), false) 297 | t.equal(c.get(4), undefined) 298 | 299 | t.end() 300 | }) 301 | 302 | t.test('purge stale items', t => { 303 | const c = new LRU({ max: 10, ttlResolution: 0 }) 304 | for (let i = 0; i < 10; i++) { 305 | c.set(i, i, { ttl: i + 1 }) 306 | } 307 | clock.advance(3) 308 | t.equal(c.size, 10) 309 | t.equal(c.purgeStale(), true) 310 | t.equal(c.size, 8) 311 | t.equal(c.purgeStale(), false) 312 | 313 | clock.advance(100) 314 | t.equal(c.size, 8) 315 | t.equal(c.purgeStale(), true) 316 | t.equal(c.size, 0) 317 | t.equal(c.purgeStale(), false) 318 | t.equal(c.size, 0) 319 | t.end() 320 | }) 321 | 322 | t.test('no update ttl', t => { 323 | const statuses: LRUCache.Status[] = [] 324 | const s = (): LRUCache.Status => { 325 | const status: LRUCache.Status = {} 326 | statuses.push(status) 327 | return status 328 | } 329 | const c = new LRU({ 330 | max: 10, 331 | ttlResolution: 0, 332 | noUpdateTTL: true, 333 | ttl: 10, 334 | }) 335 | for (let i = 0; i < 3; i++) { 336 | c.set(i, i) 337 | } 338 | clock.advance(9) 339 | // set, but do not update ttl. this will fall out. 340 | c.set(0, 0, { status: s() }) 341 | 342 | // set, but update the TTL 343 | c.set(1, 1, { noUpdateTTL: false, status: s() }) 344 | clock.advance(9) 345 | c.purgeStale() 346 | 347 | t.equal( 348 | c.get(2, { status: s() }), 349 | undefined, 350 | 'fell out of cache normally' 351 | ) 352 | t.equal( 353 | c.get(1, { status: s() }), 354 | 1, 355 | 'still in cache, ttl updated' 356 | ) 357 | t.equal( 358 | c.get(0, { status: s() }), 359 | undefined, 360 | 'fell out of cache, despite update' 361 | ) 362 | 363 | clock.advance(9) 364 | c.purgeStale() 365 | t.equal( 366 | c.get(1, { status: s() }), 367 | undefined, 368 | 'fell out of cache after ttl update' 369 | ) 370 | 371 | t.end() 372 | }) 373 | 374 | // https://github.com/isaacs/node-lru-cache/issues/203 375 | t.test('indexes/rindexes can walk over stale entries', t => { 376 | const c = new LRU({ max: 10, ttl: 10 }) 377 | const e = expose(c, LRU) 378 | for (let i = 0; i < 3; i++) { 379 | c.set(i, i) 380 | } 381 | clock.advance(9) 382 | for (let i = 3; i < 10; i++) { 383 | c.set(i, i) 384 | } 385 | c.get(1) 386 | c.get(3) 387 | clock.advance(9) 388 | const indexes = [...e.indexes()] 389 | const indexesStale = [...e.indexes({ allowStale: true })] 390 | const rindexes = [...e.rindexes()] 391 | const rindexesStale = [...e.rindexes({ allowStale: true })] 392 | t.same( 393 | { 394 | indexes, 395 | indexesStale, 396 | rindexes, 397 | rindexesStale, 398 | }, 399 | { 400 | indexes: [3, 9, 8, 7, 6, 5, 4], 401 | indexesStale: [3, 1, 9, 8, 7, 6, 5, 4, 2, 0], 402 | rindexes: [4, 5, 6, 7, 8, 9, 3], 403 | rindexesStale: [0, 2, 4, 5, 6, 7, 8, 9, 1, 3], 404 | } 405 | ) 406 | t.end() 407 | }) 408 | 409 | // https://github.com/isaacs/node-lru-cache/issues/203 410 | t.test('clear() disposes stale entries', t => { 411 | const disposed: any[] = [] 412 | const disposedAfter: any[] = [] 413 | const c = new LRU({ 414 | max: 3, 415 | ttl: 10, 416 | dispose: (v: any, k: any) => disposed.push([v, k]), 417 | disposeAfter: (v: any, k: any) => disposedAfter.push([v, k]), 418 | }) 419 | for (let i = 0; i < 4; i++) { 420 | c.set(i, i) 421 | } 422 | t.same(disposed, [[0, 0]]) 423 | t.same(disposedAfter, [[0, 0]]) 424 | clock.advance(20) 425 | c.clear() 426 | t.same(disposed, [ 427 | [0, 0], 428 | [1, 1], 429 | [2, 2], 430 | [3, 3], 431 | ]) 432 | t.same(disposedAfter, [ 433 | [0, 0], 434 | [1, 1], 435 | [2, 2], 436 | [3, 3], 437 | ]) 438 | t.end() 439 | }) 440 | 441 | t.test('purgeStale() lockup', t => { 442 | const c = new LRU({ 443 | max: 3, 444 | ttl: 10, 445 | updateAgeOnGet: true, 446 | }) 447 | c.set(1, 1) 448 | c.set(2, 2) 449 | c.set(3, 3) 450 | clock.advance(5) 451 | c.get(2) 452 | clock.advance(15) 453 | c.purgeStale() 454 | t.pass('did not get locked up') 455 | t.end() 456 | }) 457 | 458 | t.test('set item pre-stale', t => { 459 | const c = new LRU({ 460 | max: 3, 461 | ttl: 10, 462 | allowStale: true, 463 | }) 464 | c.set(1, 1) 465 | t.equal(c.has(1), true) 466 | t.equal(c.get(1), 1) 467 | c.set(2, 2, { start: clock.now() - 11 }) 468 | t.equal(c.has(2), false) 469 | t.equal(c.get(2), 2) 470 | t.equal(c.get(2), undefined) 471 | c.set(2, 2, { start: clock.now() - 11 }) 472 | const dump = c.dump() 473 | t.matchSnapshot(dump, 'dump with stale values') 474 | const d = new LRU({ max: 3, ttl: 10, allowStale: true }) 475 | d.load(dump) 476 | t.equal(d.has(2), false) 477 | t.equal(d.get(2), 2) 478 | t.equal(d.get(2), undefined) 479 | t.end() 480 | }) 481 | 482 | t.test('no delete on stale get', t => { 483 | const c = new LRU({ 484 | noDeleteOnStaleGet: true, 485 | ttl: 10, 486 | max: 3, 487 | }) 488 | c.set(1, 1) 489 | clock.advance(11) 490 | t.equal(c.has(1), false) 491 | t.equal(c.get(1), undefined) 492 | t.equal(c.get(1, { allowStale: true }), 1) 493 | t.equal( 494 | c.get(1, { allowStale: true, noDeleteOnStaleGet: false }), 495 | 1 496 | ) 497 | t.equal(c.get(1, { allowStale: true }), undefined) 498 | t.end() 499 | }) 500 | 501 | t.end() 502 | } 503 | 504 | t.test('tests with perf_hooks.performance.now()', t => { 505 | const { performance, Date } = global 506 | // @ts-ignore 507 | t.teardown(() => Object.assign(global, { performance, Date })) 508 | // @ts-ignore 509 | global.Date = clock.Date 510 | // @ts-ignore 511 | global.performance = clock 512 | const { LRUCache: LRU } = t.mockRequire('../', {}) 513 | runTests(LRU, t) 514 | }) 515 | 516 | t.test('tests using Date.now()', t => { 517 | const { performance, Date } = global 518 | // @ts-ignore 519 | t.teardown(() => Object.assign(global, { performance, Date })) 520 | // @ts-ignore 521 | global.Date = clock.Date 522 | // @ts-ignore 523 | global.performance = null 524 | const { LRUCache: LRU } = t.mockRequire('../', {}) 525 | runTests(LRU, t) 526 | }) 527 | -------------------------------------------------------------------------------- /test/unbounded-warning.ts: -------------------------------------------------------------------------------- 1 | import t from 'tap' 2 | import { LRUCache } from '../dist/esm/index.js' 3 | 4 | t.test('emits warning', t => { 5 | const { emitWarning } = process 6 | t.teardown(() => { 7 | process.emitWarning = emitWarning 8 | }) 9 | const warnings: [string, string, string][] = [] 10 | Object.defineProperty(process, 'emitWarning', { 11 | value: (msg: string, type: string, code: string) => { 12 | warnings.push([msg, type, code]) 13 | }, 14 | configurable: true, 15 | writable: true, 16 | }) 17 | //@ts-expect-error 18 | new LRUCache({ 19 | ttl: 100, 20 | }) 21 | t.same(warnings, [ 22 | [ 23 | 'TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.', 24 | 'UnboundedCacheWarning', 25 | 'LRU_CACHE_UNBOUNDED', 26 | ], 27 | ]) 28 | t.end() 29 | }) 30 | 31 | t.test('prints to stderr if no process.emitWarning', t => { 32 | const { LRUCache: LRU } = t.mockRequire('../', {}) as { 33 | LRUCache: typeof LRUCache 34 | } 35 | const { error } = console 36 | const { emitWarning } = process 37 | t.teardown(() => { 38 | console.error = error 39 | process.emitWarning = emitWarning 40 | }) 41 | const warnings: [string][] = [] 42 | Object.defineProperty(console, 'error', { 43 | value: (msg: string) => { 44 | warnings.push([msg]) 45 | }, 46 | configurable: true, 47 | writable: true, 48 | }) 49 | Object.defineProperty(process, 'emitWarning', { 50 | value: undefined, 51 | configurable: true, 52 | writable: true, 53 | }) 54 | //@ts-expect-error 55 | new LRU({ 56 | ttl: 100, 57 | }) 58 | //@ts-expect-error 59 | new LRU({ 60 | ttl: 100, 61 | }) 62 | t.same(warnings, [ 63 | [ 64 | '[LRU_CACHE_UNBOUNDED] UnboundedCacheWarning: TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.', 65 | ], 66 | ]) 67 | t.end() 68 | }) 69 | -------------------------------------------------------------------------------- /test/warn-missing-ac.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module' 2 | import { fileURLToPath } from 'url' 3 | 4 | const __filename = fileURLToPath(import.meta.url) 5 | const main = async () => { 6 | const { default: t } = await import('tap') 7 | const { spawn } = await import('child_process') 8 | 9 | // need to run both tests in parallel so we don't miss the close event 10 | t.jobs = 3 11 | 12 | const argv = process.execArgv.filter( 13 | a => !a.startsWith('--no-warnings') 14 | ) 15 | const warn = spawn( 16 | process.execPath, 17 | [...argv, __filename, 'child'], 18 | { 19 | env: { 20 | ...process.env, 21 | NODE_OPTIONS: '', 22 | }, 23 | } 24 | ) 25 | const warnErr: Buffer[] = [] 26 | warn.stderr.on('data', c => warnErr.push(c)) 27 | 28 | const noWarn = spawn( 29 | process.execPath, 30 | [...argv, __filename, 'child'], 31 | { 32 | env: { 33 | ...process.env, 34 | LRU_CACHE_IGNORE_AC_WARNING: '1', 35 | NODE_OPTIONS: '', 36 | }, 37 | } 38 | ) 39 | const noWarnErr: Buffer[] = [] 40 | noWarn.stderr.on('data', c => noWarnErr.push(c)) 41 | 42 | const noFetch = spawn( 43 | process.execPath, 44 | [...argv, __filename, 'child-no-fetch'], 45 | { 46 | env: { 47 | ...process.env, 48 | NODE_OPTIONS: '', 49 | }, 50 | } 51 | ) 52 | const noFetchErr: Buffer[] = [] 53 | noFetch.stderr.on('data', c => noFetchErr.push(c)) 54 | 55 | t.test('no warning', async t => { 56 | await new Promise(r => 57 | noWarn.on('close', (code, signal) => { 58 | t.equal(code, 0) 59 | t.equal(signal, null) 60 | r() 61 | }) 62 | ) 63 | t.notMatch( 64 | Buffer.concat(noWarnErr).toString().trim(), 65 | 'NO_ABORT_CONTROLLER' 66 | ) 67 | }) 68 | 69 | t.test('no warning (because no fetch)', async t => { 70 | await new Promise(r => 71 | noFetch.on('close', (code, signal) => { 72 | t.equal(code, 0) 73 | t.equal(signal, null) 74 | r() 75 | }) 76 | ) 77 | t.notMatch( 78 | Buffer.concat(noWarnErr).toString().trim(), 79 | 'NO_ABORT_CONTROLLER' 80 | ) 81 | }) 82 | 83 | t.test('warning', async t => { 84 | await new Promise(r => 85 | warn.on('close', (code, signal) => { 86 | t.equal(code, 0) 87 | t.equal(signal, null) 88 | r() 89 | }) 90 | ) 91 | t.match( 92 | Buffer.concat(warnErr).toString().trim(), 93 | /NO_ABORT_CONTROLLER/ 94 | ) 95 | }) 96 | } 97 | 98 | switch (process.argv[2]) { 99 | case 'child': 100 | //@ts-expect-error 101 | process.emitWarning = null 102 | //@ts-expect-error 103 | globalThis.AbortController = undefined 104 | //@ts-expect-error 105 | globalThis.AbortSignal = undefined 106 | const req = createRequire(import.meta.url) 107 | const { LRUCache } = req('../dist/commonjs/index.js') 108 | new LRUCache({ max: 1, fetchMethod: async () => 1 }).fetch(1) 109 | break 110 | case 'child-no-fetch': 111 | //@ts-expect-error 112 | globalThis.AbortController = undefined 113 | //@ts-expect-error 114 | globalThis.AbortSignal = undefined 115 | import('../dist/esm/index.js') 116 | break 117 | default: 118 | main() 119 | } 120 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "inlineSources": true, 8 | "jsx": "react", 9 | "module": "nodenext", 10 | "moduleResolution": "nodenext", 11 | "noUncheckedIndexedAccess": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "target": "es2022" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationLinks": { 3 | "GitHub": "https://github.com/isaacs/node-lru-cache", 4 | "isaacs projects": "https://isaacs.github.io/", 5 | "benchmark summary": "https://isaacs.github.io/node-lru-cache/benchmark/", 6 | "benchmark details": "https://isaacs.github.io/node-lru-cache/benchmark/results/" 7 | } 8 | } 9 | --------------------------------------------------------------------------------