diff --git a/ .editorconfig b/ .editorconfig new file mode 100644 index 0000000..27bd2b7 --- /dev/null +++ b/ .editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8ae0c01 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [wellwelwel] diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..9c5f1ef --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,6 @@ +name: 'CodeQL Config' + +paths: + - src/ + - lib/ + - browser/ diff --git a/.github/workflows/bot_stale.yml b/.github/workflows/bot_stale.yml new file mode 100644 index 0000000..9b0b89f --- /dev/null +++ b/.github/workflows/bot_stale.yml @@ -0,0 +1,24 @@ +name: '🧹 Bot β€” Stale' + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + name: Issues and PRs + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is stale because it has been open 28 days with no activity. Remove the stale label or comment or this will be closed in 2 days' + stale-pr-message: 'This PR is stale because it has been open 28 days with no activity. Remove the stale label or comment or this will be closed in 2 days' + days-before-stale: 28 + days-before-close: 2 + stale-issue-label: 'stale' + stale-pr-label: 'stale' + remove-stale-when-updated: true + delete-branch: false + enable-statistics: true diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml new file mode 100644 index 0000000..79cfb10 --- /dev/null +++ b/.github/workflows/ci_build.yml @@ -0,0 +1,37 @@ +name: 'πŸ§‘πŸ»β€πŸ³ CI β€” Build' + +on: + push: + branches: + - 'main' + pull_request: + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: false + name: Check + steps: + - name: βž• Actions - Checkout + uses: actions/checkout@v4 + + - name: βž• Actions - Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: βž• Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-linux-${{ hashFiles('package-lock.json') }} + restore-keys: npm-linux- + + - name: πŸ“¦ Installing Dependencies + run: npm ci + + - name: πŸ€ΉπŸ»β€β™€οΈ TypeScript Build + run: npm run build diff --git a/.github/workflows/ci_codeql.yml b/.github/workflows/ci_codeql.yml new file mode 100644 index 0000000..3d6b52c --- /dev/null +++ b/.github/workflows/ci_codeql.yml @@ -0,0 +1,80 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: 'πŸ”Ž CI β€” CodeQL' + +on: + push: + branches: + - 'main' + pull_request: + workflow_dispatch: + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript-typescript'] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + config-file: ./.github/codeql/codeql-config.yml + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # πŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/ci_lint.yml b/.github/workflows/ci_lint.yml new file mode 100644 index 0000000..12db700 --- /dev/null +++ b/.github/workflows/ci_lint.yml @@ -0,0 +1,37 @@ +name: 'πŸ‘” CI β€” Lint' + +on: + push: + branches: + - 'main' + pull_request: + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: false + name: Check + steps: + - name: βž• Actions - Checkout + uses: actions/checkout@v4 + + - name: βž• Actions - Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: βž• Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-linux-${{ hashFiles('package-lock.json') }} + restore-keys: npm-linux- + + - name: πŸ“¦ Installing Dependencies + run: npm ci + + - name: πŸ€ΉπŸ»β€β™€οΈ Biome + Prettier Check + run: npm run lint diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..7ec4a1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +deno.lock +/lib +/browser +/playground diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..67839f0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +/lib +/CHANGELOG.md +/browser diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..37fb01d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "css", + "endOfLine": "auto", + "embeddedLanguageFormatting": "auto", + "singleAttributePerLine": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100755 index 0000000..5af172e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.trimAutoWhitespace": true, + "editor.indentSize": 2, + "editor.tabSize": 2, + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "[markdown]": { + "files.trimTrailingWhitespace": false + } +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c95714a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,48 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +--- + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +--- + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +--- + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +--- + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..56ab098 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,70 @@ +# Contributing + +If you're thinking of contributing, thank you, and _naturally_, please **be respectful** πŸ™‹πŸ»β€β™‚οΈ + +## Issues + +By opening an **Issue**, please describe the problem. If you can share a basic repro, it will be great. + +--- + +## Pull Requests + +By opening a **Pull Request**, please describe the proposed solution and what it solves. + +--- + +## Developing + +### Environment + +You will need these tools installed on your system: + +- [**Node.js**](https://nodejs.org/en/download/package-manager) +- [**Bun**](https://bun.sh/docs/installation) (optional) +- [**Deno**](https://docs.deno.com/runtime/manual/getting_started/installation) (optional) + +> **Bun**, **Deno** and **Node.js** versions are tested using **Docker** official images. + +--- + +Fork this project, download your forked repository locally and create a new branch from `main`. +Then run `npm ci` to clean install the node modules. + +> Please, do not change the _package-lock.json_. + +### Fixes + +Where possible, provide an error test case that the fix covers. + +### Features + +It's better to discuss an **API** before actually start implementing it. You can open an [**Issue on Github**](https://github.com/wellwelwel/lru.min/issues/new), so we can discuss the **API** design implementation ideas. + +> Please ensure test cases to cover new features. + +--- + +## Testing + +```sh +npm run test:node +bun run test:bun +deno task test:deno +``` + +Also, run `npm run build` to compile and run _E2E_ tests in the virtual browser. + +--- + +### Lint + +```sh +npm run lint +``` + +> Also +> +> ```sh +> npm run lint:fix +> ``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e8c4fff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-current Weslley AraΓΊjo (@wellwelwel) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0ad5d0 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +

lru.min

+
+ +πŸ”₯ Extremely fast LRU Cache for JavaScript (Browser compatible) β€” **6.1KB**. + +
+ +## Why + +#### 🀝 Compatibility + +- **lru.min** ensures is fully compatible with both **Node.js** _(8+)_, **Bun**, **Deno** and, browser environments.
+ +#### πŸ‘¨πŸ»β€πŸ’» Features + +- πŸ•’ Smart and dynamic **TTL**. +- 🧹 Efficient **disposal** handling. +- βœ… Truly respects the maximum **cache size**. +- πŸ”‘ Take full caching control _(highly debuggable)_. + +#### πŸŽ–οΈ [Performance](#performance) + +- **lru.min** has beaten the two most used and popular **LRU** packages. + +--- + +## Install + +```bash +# Node.js +npm i lru.min +``` + +```bash +# Bun +bun add lru.min +``` + +```bash +# Deno +deno add npm:lru.min +``` + +--- + +## Usage + +> - ⚠️ Please wait until `v1.x.x` before using this package. +> - πŸ“˜ Complete documentation an public repository coming soon. + +### Import + +#### ES Modules + +```js +import { createLRU } from 'lru.min'; +``` + +#### CommonJS + +```js +const { createLRU } = require('lru.min'); +``` + +#### Browser + +```html + +``` + +--- + +## Performance + +In **Bun**, lru.min achieves up to **11** more operations per second than [**lru-cache**](https://github.com/isaacs/node-lru-cache) `v11.x.x` and up to **5** more ops per second than [**quick-lru**](https://github.com/sindresorhus/quick-lru) `v7.x.x` for essential usage (_get_, _set_, _evict_, and _delete_ the cache): + +```sh +# Total Number of Cores: 24 (16 performance and 8 efficiency) +# Memory: 64 GB + +β€Ί Node.js +lru-cache x 24.42 ops/sec β€” Β±0.76% (45 runs sampled) +quick-lru x 25.62 ops/sec β€” Β±0.33% (47 runs sampled) +lru.min x 26.28 ops/sec β€” Β±0.19% (48 runs sampled) + +πŸš€ Fastest is lru.min + +β€Ί Bun +lru-cache x 97.75 ops/sec β€” Β±0.32% (85 runs sampled) +quick-lru x 105.72 ops/sec β€” Β±0.33% (79 runs sampled) +lru.min x 110.08 ops/sec β€” Β±0.34% (82 runs sampled) + +πŸš€ Fastest is lru.min + +β€Ί Deno +lru-cache x 52.79 ops/sec β€” Β±0.38% (70 runs sampled) +quick-lru x 54.23 ops/sec β€” Β±0.34% (71 runs sampled) +lru.min x 56.39 ops/sec β€” Β±0.33% (74 runs sampled) + +πŸš€ Fastest is lru.min +``` + +- You can see how the tests are run and compared in the [benchmark](https://github.com/wellwelwel/lru.min/tree/main/benchmark) directory. +- **lru.min** is [continuously tested](https://github.com/wellwelwel/lru.min/blob/main/.github/workflows/ci_benchmark.yml) to ensure the above expectations. + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..131e7ee --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +## Is lru.min Safe? + +**lru.min** is an open-source project, so you can see both the [Source Code on **GitHub** Repository](https://github.com/wellwelwel/lru.min) and the [Distribution Code on **NPM**](https://www.npmjs.com/package/lru.min?activeTab=code). + +--- + +## Supported Versions + +Currently, security updates will be applied to the following versions of **lru.min**: + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | +| 0.x.x | :x: | + +--- + +## Reporting a Vulnerability + +- Please, give detailed reports +- Include steps to reproduce the vulnerability, and if possible, a patch or workaround. +- Include the specific version of **lru.min** you are using. + +**Reporting:** + +- https://github.com/wellwelwel/lru.min/security/advisories + +> Once the issue has been resolved, you will be attributed a part of the report. diff --git a/benchmark/deno.json b/benchmark/deno.json new file mode 100644 index 0000000..d420a34 --- /dev/null +++ b/benchmark/deno.json @@ -0,0 +1,6 @@ +{ + "imports": { + "lru-cache": "npm:lru-cache@^11.0.0", + "quick-lru": "npm:quick-lru@^7.0.0" + } +} diff --git a/benchmark/lib/index.js b/benchmark/lib/index.js new file mode 100644 index 0000000..b071408 --- /dev/null +++ b/benchmark/lib/index.js @@ -0,0 +1,126 @@ +import { execSync } from 'node:child_process'; +import Benchmark from 'benchmark'; + +const exec = (command) => { + // execSync(command, { stdio: 'inherit' }); + execSync(command, { stdio: 'ignore' }); +}; + +console.log(); + +console.log('β€Ί \x1b[1mNode.js\x1b[0m'); +{ + const suite = new Benchmark.Suite(); + const results = new Map(); + + suite + .add('lru-cache ', () => { + exec('node ./lib/lru-cache.js'); + }) + .add('quick-lru ', () => { + exec('node ./lib/quick-lru.js'); + }) + .add('lru.min ', () => { + exec('node ./lib/lru.min.js'); + }) + .on('cycle', (event) => { + const name = event.target.name.trim(); + const result = { + opsPerSec: event.target.hz, + percentage: event.target.stats.rme, + }; + + results.set(name, result); + console.log( + `${event.target.name} x ${result.opsPerSec.toFixed(2)} ops/sec β€” Β±${result.percentage.toFixed(2)}% (${event.target.stats.sample.length} runs sampled)` + ); + }) + .on('complete', function () { + const fatest = String(this.filter('fastest').map('name')).trim(); + + console.log(`\nπŸš€ Fastest is \x1b[1m${fatest}\x1b[0m\n`); + + if (!/^lru.min/.test(fatest)) { + process.exit(1); + } + }) + .run({ async: false }); +} + +console.log('β€Ί \x1b[1mBun\x1b[0m'); +{ + const suite = new Benchmark.Suite(); + const results = new Map(); + + suite + .add('lru-cache ', () => { + exec('bun ./lib/lru-cache.js'); + }) + .add('quick-lru ', () => { + exec('bun ./lib/quick-lru.js'); + }) + .add('lru.min ', () => { + exec('bun ./lib/lru.min.js'); + }) + .on('cycle', (event) => { + const name = event.target.name.trim(); + const result = { + opsPerSec: event.target.hz, + percentage: event.target.stats.rme, + }; + + results.set(name, result); + console.log( + `${event.target.name} x ${result.opsPerSec.toFixed(2)} ops/sec β€” Β±${result.percentage.toFixed(2)}% (${event.target.stats.sample.length} runs sampled)` + ); + }) + .on('complete', function () { + const fatest = String(this.filter('fastest').map('name')).trim(); + + console.log(`\nπŸš€ Fastest is \x1b[1m${fatest}\x1b[0m\n`); + + if (!/^lru.min/.test(fatest)) { + process.exit(1); + } + }) + .run({ async: false }); +} + +console.log('β€Ί \x1b[1mDeno\x1b[0m'); +{ + const suite = new Benchmark.Suite(); + const results = new Map(); + + suite + .add('lru-cache ', () => { + exec('deno run ./lib/lru-cache.js'); + }) + .add('quick-lru ', () => { + exec('deno run ./lib/quick-lru.js'); + }) + .add('lru.min ', () => { + exec('deno run ./lib/lru.min.js'); + }) + .on('cycle', (event) => { + const name = event.target.name.trim(); + const result = { + opsPerSec: event.target.hz, + percentage: event.target.stats.rme, + }; + + results.set(name, result); + console.log( + `${event.target.name} x ${result.opsPerSec.toFixed(2)} ops/sec β€” Β±${result.percentage.toFixed(2)}% (${event.target.stats.sample.length} runs sampled)` + ); + }) + .on('complete', function () { + const fatest = String(this.filter('fastest').map('name')).trim(); + + console.log(`\nπŸš€ Fastest is \x1b[1m${fatest}\x1b[0m\n`); + + if (!/^lru.min/.test(fatest)) { + process.exit(1); + } + }) + .run({ async: false }); +} diff --git a/benchmark/lib/lru-cache.js b/benchmark/lib/lru-cache.js new file mode 100644 index 0000000..c8c8b04 --- /dev/null +++ b/benchmark/lib/lru-cache.js @@ -0,0 +1,20 @@ +import { LRUCache } from 'lru-cache'; + +const cache = new LRUCache({ + max: 3, + // dispose: (key) => { + // console.log(key); + // }, +}); + +cache.set('a', 1); +cache.set('b', 2); +cache.set('c', 3); +cache.get('a'); +cache.set('d', 4); // remove 'b' +cache.get('b'); +cache.get('c'); +cache.get('d'); +cache.delete('c'); + +// console.log([...cache.keys()]); diff --git a/benchmark/lib/lru.min.js b/benchmark/lib/lru.min.js new file mode 100644 index 0000000..4b92b15 --- /dev/null +++ b/benchmark/lib/lru.min.js @@ -0,0 +1,20 @@ +import { createLRU } from '../../lib/index.mjs'; + +const cache = createLRU({ + max: 3, + // dispose: (key) => { + // console.log(key); + // }, +}); + +cache.set('a', 1); +cache.set('b', 2); +cache.set('c', 3); +cache.get('a'); +cache.set('d', 4); // remove 'b' +cache.get('b'); +cache.get('c'); +cache.get('d'); +cache.del('c'); + +// console.log(cache.keys()); diff --git a/benchmark/lib/quick-lru.js b/benchmark/lib/quick-lru.js new file mode 100644 index 0000000..b689055 --- /dev/null +++ b/benchmark/lib/quick-lru.js @@ -0,0 +1,20 @@ +import QuickLRU from 'quick-lru'; + +const cache = new QuickLRU({ + maxSize: 3, + // onEviction: (key) => { + // console.log(key); + // }, +}); + +cache.set('a', 1); +cache.set('b', 2); +cache.set('c', 3); +cache.get('a'); +cache.set('d', 4); // remove 'b' +cache.get('b'); +cache.get('c'); +cache.get('d'); +cache.delete('c'); + +// console.log([...cache.keys()]); diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json new file mode 100644 index 0000000..e918544 --- /dev/null +++ b/benchmark/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "benchmark", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "benchmark": "^2.1.4", + "lru-cache": "^11.0.0", + "quick-lru": "^7.0.0" + } + }, + "node_modules/benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.4", + "platform": "^1.3.3" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.0.0.tgz", + "integrity": "sha512-MX8gB7cVYTrYcFfAnfLlhRd0+Toyl8yX8uBx1MrX7K0jegiz9TumwOK27ldXrgDlHRdVi+MqU9Ssw6dr4BNreg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 0000000..fe3a1c2 --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "dependencies": { + "benchmark": "^2.1.4", + "lru-cache": "^11.0.0", + "quick-lru": "^7.0.0" + } +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..0aa070f --- /dev/null +++ b/biome.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "files": { + "include": ["**/**"], + "ignore": ["node_modules", "lib", "browser"] + }, + "linter": { + "enabled": true, + "rules": { + "all": true, + "complexity": { + "all": true, + "noExcessiveCognitiveComplexity": "off" + }, + "a11y": { + "all": true + }, + "correctness": { + "all": true, + "noNodejsModules": "off" + }, + "nursery": { + "all": true, + "useImportRestrictions": "off", + "useImportExtensions": "error", + "noConsole": "off" + }, + "performance": { + "all": true, + "noAccumulatingSpread": "error", + "noBarrelFile": "error", + "noDelete": "error", + "noReExportAll": "error" + }, + "security": { + "all": true + }, + "suspicious": { + "all": true, + "noConsoleLog": "off", + "noEmptyBlockStatements": "off" + }, + "style": { + "all": true, + "useNamingConvention": "off", + "noNamespaceImport": "off", + "noDefaultExport": "error", + "useBlockStatements": "off" + } + } + }, + "overrides": [ + { + "include": ["test", "tools"], + "linter": { + "rules": { + "correctness": { + "noNodejsModules": "off" + }, + "nursery": { + "useTopLevelRegex": "off" + } + } + } + } + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dfa0f20 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,832 @@ +{ + "name": "lru.min", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lru.min", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^1.8.3", + "@types/node": "^22.5.0", + "esbuild": "^0.23.1", + "happy-dom": "^15.0.0", + "packages-update": "^2.0.0", + "poku": "^2.5.0", + "prettier": "^3.3.3", + "tsx": "^4.17.0", + "typescript": "^5.5.4" + }, + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.8.3.tgz", + "integrity": "sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.8.3", + "@biomejs/cli-darwin-x64": "1.8.3", + "@biomejs/cli-linux-arm64": "1.8.3", + "@biomejs/cli-linux-arm64-musl": "1.8.3", + "@biomejs/cli-linux-x64": "1.8.3", + "@biomejs/cli-linux-x64-musl": "1.8.3", + "@biomejs/cli-win32-arm64": "1.8.3", + "@biomejs/cli-win32-x64": "1.8.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz", + "integrity": "sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz", + "integrity": "sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz", + "integrity": "sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz", + "integrity": "sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz", + "integrity": "sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz", + "integrity": "sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz", + "integrity": "sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz", + "integrity": "sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/happy-dom": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.0.0.tgz", + "integrity": "sha512-DsvANUcxxY20iCo3Yllm7dqwzPVPduGfVFxa7mONwMBLczFeQgkN0LpDir1kIY322JMh+hrcPV3aGLyHCESDlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0", + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/packages-update": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/packages-update/-/packages-update-2.0.0.tgz", + "integrity": "sha512-eT5DegVpuM5SRmhBhlLZXTCqv/sAOnAwFiMeJIvVzniqFe3BUk4SzGLlPBnO185QAJbWOjp0fAHAbdOE9gpPSg==", + "dev": true, + "license": "MIT", + "bin": { + "packages-update": "lib/bin/index.js", + "pu": "lib/bin/index.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/poku": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/poku/-/poku-2.5.0.tgz", + "integrity": "sha512-aeFn5H4oQXAXkph1NuzGxkq57WzqBlJAyXxLGTo156DMgiLARdtCOkzLSGtq/xLl9yJiZpZ25xJqFWmyWcTk/w==", + "dev": true, + "license": "MIT", + "bin": { + "poku": "lib/bin/index.js" + }, + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.17.0", + "typescript": ">=4.7.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz", + "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..410d593 --- /dev/null +++ b/package.json @@ -0,0 +1,86 @@ +{ + "name": "lru.min", + "version": "0.1.0", + "description": "πŸ”₯ Extremely fast LRU cache for JavaScript (Browser compatible) β€” 6.1KB.", + "main": "./lib/index.js", + "module": "./lib/index.mjs", + "types": "./lib/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/wellwelwel/lru.min.git" + }, + "bugs": { + "url": "https://github.com/wellwelwel/lru.min/issues" + }, + "author": "https://github.com/wellwelwel", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + }, + "files": [ + "browser", + "lib" + ], + "engines": { + "node": ">=8.0.0", + "bun": ">=1.0.0", + "deno": ">=1.30.0" + }, + "scripts": { + "benchmark": "cd benchmark && npm ci && node lib/index.js", + "prebuild": "rm -rf ./browser ./lib", + "build:browser": "tsx tools/browserfy.ts", + "build:cjs": "esbuild src/index.ts --outfile=lib/index.js --platform=node --target=node8 --format=cjs", + "build:esm": "esbuild src/index.ts --outfile=lib/index.mjs --platform=node --target=node12 --format=esm", + "build": "tsc && npm run build:cjs && npm run build:esm && npm run build:browser", + "test:node": "poku --node -p", + "test:bun": "poku --bun -p", + "test:deno": "poku --deno -p", + "lint": "npx @biomejs/biome lint && prettier --check .", + "lint:fix": "npx @biomejs/biome lint --write && prettier --write .github/workflows/*.yml .", + "update": "pu minor && npm i && npm audit fix", + "postupdate": "npm run lint:fix", + "size": "ls -lh lib/index.mjs | awk '{print $5}'" + }, + "devDependencies": { + "@biomejs/biome": "^1.8.3", + "@types/node": "^22.5.0", + "esbuild": "^0.23.1", + "happy-dom": "^15.0.0", + "packages-update": "^2.0.0", + "poku": "^2.5.0", + "prettier": "^3.3.3", + "tsx": "^4.17.0", + "typescript": "^5.5.4" + }, + "exports": { + ".": { + "import": { + "types": "./lib/index.d.ts", + "default": "./lib/index.mjs" + }, + "require": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + } + } + }, + "keywords": [ + "lru", + "mru", + "cache", + "caching", + "ttl", + "hash", + "node", + "nodejs", + "bun", + "deno", + "typescript", + "browser", + "fast", + "lru-cache", + "quick-lru" + ] +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..82841d7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,309 @@ +export interface LRUCacheOptions { + max: number; + maxAge?: number; + dispose?: (value: Value, key: Key) => undefined; +} + +export interface SetOptions { + maxAge?: number; +} + +interface CacheNode { + key: Key; + value: Value; + prev: CacheNode | null; + next: CacheNode | null; + timestamp?: number; + maxAge?: number; +} + +export const createLRU = ( + options: LRUCacheOptions +) => { + let max = options.max; + let head: CacheNode | null = null; + let tail: CacheNode | null = null; + let size = 0; + let maxAgeCount = 0; + + const map = new Map>(); + const dispose = options?.dispose; + const maxAge = options?.maxAge; + + const addNode = (node: CacheNode): undefined => { + node.next = head; + + if (head) head.prev = node; + head = node; + + if (!tail) tail = node; + size++; + }; + + const removeNode = (node: CacheNode): undefined => { + if (node.prev) node.prev.next = node.next; + if (node.next) node.next.prev = node.prev; + if (node === head) head = node.next; + if (node === tail) tail = node.prev; + + size--; + }; + + const moveToHead = (node: CacheNode): undefined => { + removeNode(node); + addNode(node); + }; + + function refresh(key: Key): boolean; + function refresh(): undefined; + function refresh(key?: Key): boolean | undefined { + if (!(maxAge || maxAgeCount)) return true; + + const now = Date.now(); + + if (key) { + const node = map.get(key); + if (!node?.timestamp) return true; + + const effectiveMaxAge = node.maxAge ?? maxAge; + + if (effectiveMaxAge && now - node.timestamp > effectiveMaxAge) { + del(node.key); + return false; + } + + return true; + } + + for (let current = tail; current !== null; current = current.prev) { + if (!current?.timestamp) continue; + + const effectiveMaxAge = current.maxAge ?? maxAge; + + if (effectiveMaxAge && now - current.timestamp > effectiveMaxAge) + del(current.key); + } + } + + const evict = (size = 1): undefined => { + for (let i = 0; i < size; i++) { + if (!tail) return; + + const tailKey = tail.key; + const tailValue = tail.value; + + if (tail.maxAge && maxAgeCount > 0) { + maxAgeCount--; + } + + removeNode(tail); + map.delete(tailKey); + + if (dispose) dispose(tailValue, tailKey); + } + }; + + const set = (key: Key, value: Value, options?: SetOptions): undefined => { + let node = map.get(key); + + const now = maxAge || options?.maxAge ? Date.now() : undefined; + + if (options?.maxAge && !map.has(key)) maxAgeCount++; + + if (node) { + node.value = value; + node.timestamp = now; + node.maxAge = options?.maxAge; + + if (head !== node) moveToHead(node); + + return; + } + + node = { + key, + value, + prev: null, + next: null, + timestamp: now, + }; + + map.set(key, node); + addNode(node); + + if (size > max) evict(); + }; + + const get = (key: Key): Value | undefined => { + const node = map.get(key); + if (!node) return; + if (!refresh(key)) return; + + if (head !== node) moveToHead(node); + + return node.value; + }; + + const peek = (key: Key): Value | undefined => { + const node = map.get(key); + if (!node) return; + if (!refresh(key)) return; + + if (node?.timestamp) { + const now = Date.now(); + const effectiveMaxAge = node.maxAge ?? maxAge; + + if (effectiveMaxAge && now - node.timestamp > effectiveMaxAge) { + del(key); + return; + } + } + + return node.value; + }; + + const has = (key: Key): boolean => { + if (!refresh(key)) return false; + + return map.has(key); + }; + + const keys = (): Key[] => { + const result: Key[] = []; + + for (let current = head; current !== null; current = current.next) { + if (!refresh(current.key)) continue; + + result.push(current.key); + } + + return result; + }; + + const values = (): Value[] => { + const result: Value[] = []; + + for (let current = head; current !== null; current = current.next) { + if (!refresh(current.key)) continue; + + result.push(current.value); + } + + return result; + }; + + const entries = (): [Key, Value][] => { + const result: [Key, Value][] = []; + + for (let current = head; current !== null; current = current.next) { + if (!refresh(current.key)) continue; + + result.push([current.key, current.value]); + } + + return result; + }; + + const forEach = ( + callback: (value: Value, key: Key) => undefined + ): undefined => { + for (let current = head; current !== null; current = current.next) { + if (!refresh(current.key)) continue; + + callback(current.value, current.key); + } + }; + + const del = (key: Key): boolean => { + const node = map.get(key); + if (!node) return false; + + if (node.maxAge && maxAgeCount > 0) { + maxAgeCount--; + } + + removeNode(node); + map.delete(key); + + if (dispose) dispose(node.value, key); + + return true; + }; + + const clear = (): undefined => { + if (dispose) for (const node of map.values()) dispose(node.value, node.key); + + map.clear(); + + head = tail = null; + size = 0; + maxAgeCount = 0; + }; + + const resize = (newMax: number): undefined => { + max = newMax; + + for (let i = size; i > max; i--) evict(); + }; + + const available = () => { + refresh(); + + return max - map.size; + }; + + const stored = () => { + refresh(); + + return map.size; + }; + + const maxSize = () => max; + + return { + /** Adds a key-value pair to the cache. Updates the value if the key already exists. */ + set, + + /** Retrieves the value for a given key and moves the key to the most recent position. */ + get, + + /** Retrieves the value for a given key without changing its position in the cache. */ + peek, + + /** Checks if a key exists in the cache. */ + has, + + /** Returns an array of all keys in the cache, from most recent to least recent. */ + keys, + + /** Returns an array of all values in the cache, from most recent to least recent. */ + values, + + /** Returns an array of [key, value] pairs, from most recent to least recent. */ + entries, + + /** Iterates over each key-value pair in the cache, from most recent to least recent. */ + forEach, + + /** Deletes a key-value pair from the cache. */ + del, + + /** Evicts the oldest item or the specified number of the oldest items from the cache. */ + evict, + + /** Clears all key-value pairs from the cache. */ + clear, + + /** Resizes the cache to a new maximum size, evicting items if necessary. */ + resize, + + /** Returns the number of available slots in the cache before reaching the maximum size. */ + available, + + /** Returns the number of items currently stored in the cache. */ + stored, + + /** Returns the maximum number of items that can be stored in the cache. */ + maxSize, + }; +}; diff --git a/tools/browserfy.ts b/tools/browserfy.ts new file mode 100644 index 0000000..9db6e5d --- /dev/null +++ b/tools/browserfy.ts @@ -0,0 +1,20 @@ +import { readFile } from 'node:fs/promises'; +import esbuild from 'esbuild'; + +(async () => { + const contents = (await readFile('src/index.ts', 'utf8')) + .replace(/export/gim, '') + .replace(/(let|const) /gim, 'var '); + + await esbuild.build({ + stdin: { + contents, + loader: 'ts', + }, + platform: 'browser', + target: 'es6', + bundle: false, + outfile: 'browser/lru.min.js', + minify: true, + }); +})(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d95c4e0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "include": ["src"], + "compilerOptions": { + "target": "ES6", + "module": "CommonJS", + "outDir": "lib", + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "allowJs": false, + "strict": true, + "alwaysStrict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noImplicitAny": true, + "removeComments": false, + "sourceMap": false, + "esModuleInterop": false, + "noEmitOnError": true, + "declaration": true, + "declarationDir": "lib", + "allowSyntheticDefaultImports": false, + "skipLibCheck": true, + "emitDeclarationOnly": true + } +}