├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codecov.yml │ ├── deploy-website.yml │ └── tests.yml ├── .gitignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── package.json ├── packages ├── benchmark │ ├── .DS_Store │ ├── LICENSE │ ├── README.md │ ├── memory-lru.ts │ ├── memory.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── object-generator.ts │ └── tsconfig.json ├── cache-manager │ ├── LICENSE │ ├── README.md │ ├── READMEv4.md │ ├── READMEv5.md │ ├── package.json │ ├── src │ │ ├── coalesce-async.ts │ │ ├── index.ts │ │ ├── is-object.ts │ │ ├── keyv-adapter.ts │ │ ├── lt.ts │ │ └── run-if-fn.ts │ ├── test │ │ ├── cache-id.test.ts │ │ ├── clear.test.ts │ │ ├── del.test.ts │ │ ├── disconnect.test.ts │ │ ├── events.test.ts │ │ ├── example.test.ts │ │ ├── get.test.ts │ │ ├── init.test.ts │ │ ├── keyv-adapter.test.ts │ │ ├── mdel.test.ts │ │ ├── mget.test.ts │ │ ├── mset.test.ts │ │ ├── multiple-stores.test.ts │ │ ├── set.test.ts │ │ ├── sleep.ts │ │ ├── stores.test.ts │ │ ├── ttl.test.ts │ │ └── wrap.test.ts │ ├── tsconfig.json │ └── vite.config.ts ├── cacheable-request │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── types.ts │ ├── test │ │ ├── cache.test.ts │ │ ├── cacheable-request-class.test.ts │ │ ├── cacheable-request-instance.test.ts │ │ ├── create-test-server │ │ │ └── index.mjs │ │ └── dependencies.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.mjs ├── cacheable │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── cacheable-item-types.ts │ │ ├── coalesce-async.ts │ │ ├── hash.ts │ │ ├── index.ts │ │ ├── keyv-memory.ts │ │ ├── memory-lru.ts │ │ ├── memory.ts │ │ ├── shorthand-time.ts │ │ ├── stats.ts │ │ ├── ttl.ts │ │ └── wrap.ts │ ├── test │ │ ├── hash.test.ts │ │ ├── index.test.ts │ │ ├── keyv-memory.test.ts │ │ ├── memory.test.ts │ │ ├── secondary-primary.test.ts │ │ ├── shared-secondary.test.ts │ │ ├── shorthand-time.test.ts │ │ ├── sleep.ts │ │ ├── stats.test.ts │ │ ├── ttl.test.ts │ │ └── wrap.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vite.config.ts ├── file-entry-cache │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ ├── eslint.test.ts │ │ └── index.test.ts │ ├── tsconfig.json │ └── vite.config.ts ├── flat-cache │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ ├── fixtures │ │ │ └── .cache │ │ │ │ └── cache1 │ │ └── index.test.ts │ ├── tsconfig.json │ └── vite.config.ts ├── node-cache │ ├── .DS_Store │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── store.ts │ ├── test │ │ ├── export.test.ts │ │ ├── index.test.ts │ │ └── store.test.ts │ ├── tsconfig.json │ └── vite.config.ts └── website │ ├── package.json │ ├── site │ ├── docula.config.mjs │ ├── favicon.ico │ ├── logo.svg │ ├── symbol.svg │ └── variables.css │ └── src │ └── docs.cts ├── pnpm-workspace.yaml └── vitest.workspace.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **How To Reproduce (best to provide workable code or tests!)** 14 | Please provide code or unit test example that will reproduce the error. If you can't provide code, please provide a detailed description of how to reproduce the error. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Please check if the PR fulfills these requirements** 2 | - [ ] Followed the [Contributing](https://github.com/jaredwray/cacheable/blob/main/CONTRIBUTING.md) guidelines. 3 | - [ ] Tests for the changes have been added (for bug fixes/features) with 100% code coverage. 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | 6 | **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 7 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: ['22'] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install pnpm 29 | run: npm install -g pnpm 30 | 31 | - name: Start Test Services 32 | run: pnpm test:services:start 33 | 34 | - name: Install dependencies 35 | run: pnpm install 36 | 37 | - name: Approve Builds 38 | run: pnpm approve-builds 39 | 40 | - name: Build 41 | run: pnpm build 42 | 43 | - name: Test 44 | run: pnpm test:ci 45 | 46 | - name: Codecov 47 | uses: codecov/codecov-action@v3 48 | with: 49 | files: ./coverage/lcov.info 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: deploy-website 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [released] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | setup-build-deploy: 13 | name: Deploy Website 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | # Test 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install PNPM 31 | run: npm install -g pnpm 32 | 33 | - name: Install Modules 34 | run: pnpm install 35 | 36 | - name: Approve Builds 37 | run: pnpm approve-builds 38 | 39 | - name: Build Website 40 | run: pnpm website:build 41 | 42 | - name: Publish to Cloudflare Pages 43 | uses: cloudflare/wrangler-action@v3 44 | with: 45 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 46 | command: pages deploy packages/website/dist --project-name=cacheableorg --branch=main 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: ['20', '22'] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install pnpm 29 | run: npm install -g pnpm 30 | 31 | - name: Start Test Services 32 | run: pnpm test:services:start 33 | 34 | - name: Install dependencies 35 | run: pnpm install 36 | 37 | - name: Build 38 | run: pnpm build 39 | 40 | - name: Approve Builds 41 | run: pnpm approve-builds 42 | 43 | - name: Test 44 | run: pnpm test:ci 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .idea 5 | *.iml 6 | out 7 | .vscode 8 | .eslintcache 9 | dist 10 | yarn.lock 11 | pnpm-lock.yaml 12 | packages/website/site/docs/*.md 13 | packages/cacheable-request/test/testdb.sqlite 14 | packages/flat-cache/.cache 15 | packages/file-entry-cache/.cache 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me@jaredwray.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 3 | 4 | Please note we have a [Code of Conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 5 | 6 | We release new versions of this project (maintenance/features) on a monthly cadence so please be aware that some items will not get released right away. 7 | 8 | # Testing Environment 9 | 10 | To do testing you need to have redis installed on your machine. Have docker installed and run the following command to start a redis container: 11 | 12 | ```bash 13 | pnpm test:services:start 14 | ``` 15 | 16 | # Pull Request Process 17 | You can contribute changes to this repo by opening a pull request: 18 | 19 | 1) After forking this repository to your Git account, make the proposed changes on your forked branch. 20 | 2) Run tests and linting locally by running `pnpm i && pnpm build && pnpm test`. 21 | 3) Commit your changes and push them to your forked repository. 22 | 4) Navigate to the main `cacheable` repository and select the *Pull Requests* tab. 23 | 5) Click the *New pull request* button, then select the option "Compare across forks" 24 | 6) Leave the base branch set to main. Set the compare branch to your forked branch, and open the pull request. 25 | 7) Once your pull request is created, ensure that all checks have passed and that your branch has no conflicts with the base branch. If there are any issues, resolve these changes in your local repository, and then commit and push them to git. 26 | 8) Similarly, respond to any reviewer comments or requests for changes by making edits to your local repository and pushing them to Git. 27 | 9) Once the pull request has been reviewed, those with write access to the branch will be able to merge your changes into the `main` repository branch and pushed via our release schedule. 28 | 29 | If you need more information on the steps to create a pull request, you can find a detailed walkthrough in the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) 30 | 31 | # Code of Conduct 32 | Please refer to our [Code of Conduct](CODE_OF_CONDUCT.md) readme for how to contribute to this open source project and work within the community. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Cacheable](https://github.com/jaredwray/cacheable) 2 | 3 | > Caching for Nodejs based on Keyv 4 | 5 | [![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml) 6 | [![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable) 7 | [![license](https://img.shields.io/github/license/jaredwray/cacheable)](https://github.com/jaredwray/cacheable/blob/main/LICENSE) 8 | 9 | With over `1bn downloads` a year the goal with the `Cacheable Project` is to provide a robust, scalable, and maintained set of caching packages that can be used in various projects. The packages in this repository are: 10 | 11 | | Package | Downloads | Description | 12 | |---------|-----------|-------------| 13 | | [cacheable](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable) | [![npm](https://img.shields.io/npm/dm/cacheable.svg)](https://www.npmjs.com/package/cacheable) | Next generation caching framework built from the ground up with layer 1 / layer 2 caching. | 14 | | [cache-manager](https://github.com/jaredwray/cacheable/tree/main/packages/cache-manager) | [![npm](https://img.shields.io/npm/dm/cache-manager.svg)](https://www.npmjs.com/package/cache-manager) | Cache Manager that is used in services such as NestJS and others with robust features such as `wrap` and more. | 15 | | [cacheable-request](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable-request) | [![npm](https://img.shields.io/npm/dm/cacheable-request.svg)](https://www.npmjs.com/package/cacheable-request) | Wrap native HTTP requests with RFC compliant cache support | 16 | | [flat-cache](https://github.com/jaredwray/cacheable/tree/main/packages/flat-cache) | [![npm](https://img.shields.io/npm/dm/flat-cache.svg)](https://www.npmjs.com/package/flat-cache) | Fast In-Memory Caching with file store persistence | 17 | | [file-entry-cache](https://github.com/jaredwray/cacheable/tree/main/packages/file-entry-cache) | [![npm](https://img.shields.io/npm/dm/file-entry-cache.svg)](https://www.npmjs.com/package/file-entry-cache) | A lightweight cache for file metadata, ideal for processes that work on a specific set of files and only need to reprocess files that have changed since the last run | 18 | | [@cacheable/node-cache](https://github.com/jaredwray/cacheable/tree/main/packages/node-cache) | [![npm](https://img.shields.io/npm/dm/@cacheable/node-cache.svg)](https://www.npmjs.com/package/@cacheable/node-cache) | Maintained built in replacement of `node-cache` | 19 | 20 | The website documentation for https://cacheable.org is included in this repository [here](https://github.com/jaredwray/cacheable/tree/main/packages/website). 21 | 22 | # How to Use the Cacheable Mono Repo 23 | 24 | * [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) - Our code of conduct 25 | * [CONTRIBUTING](CONTRIBUTING.md) - How to contribute to this project 26 | * [SECURITY](SECURITY.md) - Security guidelines and supported versions 27 | 28 | ## Open a Pull Request 29 | 30 | Please follow the [CONTRIBUTING](CONTRIBUTING.md) guidelines provided and remember you will need to do setup on this project such as having redis running (via docker), building the project `pnpm build`, and testing `pnpm test` which will also perform linting. 31 | 32 | ## Post an Issue 33 | 34 | To post an issue, navigate to the "Issues" tab in the main repository, and then select "New Issue." Enter a clear title describing the issue, as well as a description containing additional relevant information. Also select the label that best describes your issue type. For a bug report, for example, create an issue with the label "bug." In the description field, Be sure to include replication steps, as well as any relevant error messages. 35 | 36 | If you're reporting a security violation, be sure to check out the project's [security policy](https://github.com/jaredwray/cacheable/blob/main/SECURITY.md). 37 | 38 | Please also refer to our [Code of Conduct](https://github.com/jaredwray/cacheable/blob/main/CODE_OF_CONDUCT.md) for more information on how to report issues. 39 | 40 | ## Ask a Question 41 | 42 | To ask a question, create an issue with the label "question." In the issue description, include the related code and any context that can help us answer your question. 43 | 44 | ## License 45 | 46 | [MIT © Jared Wray](LICENSE) 47 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | These packages are reviewed for security vulnerabilities and are maintained by the community. If you have a security vulnerability to report please follow the guidelines below. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you have a vulnerability to report please create an issue with `[security]` at the start of the title and a full description including code to reproduce the issue so that we can solve it quickly. If you have a fix please open the issue and then create a pull request with the issue that was created referenced in the description. 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis 4 | ports: 5 | - '6379:6379' 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheable-mono-repo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Cacheable Mono Repo", 6 | "scripts": { 7 | "test": "pnpm -r --workspace-concurrency 1 test", 8 | "test:ci": "c8 --reporter=lcov pnpm -r --workspace-concurrency 1 test:ci", 9 | "test:services:start": "docker compose up -d", 10 | "test:services:stop": "docker compose down", 11 | "website:build": "pnpm recursive --filter @cacheable/website run website:build", 12 | "website:serve": "pnpm recursive --filter @cacheable/website run website:serve", 13 | "build": "pnpm recursive run build", 14 | "clean": "pnpm recursive run clean" 15 | }, 16 | "keywords": [], 17 | "author": "Jared Wray ", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "c8": "^10.1.3", 21 | "vitest": "^3.1.1", 22 | "wrangler": "^4.7.0" 23 | }, 24 | "pnpm": { 25 | "onlyBuiltDependencies": [ 26 | "esbuild", 27 | "protobufjs", 28 | "sharp", 29 | "sqlite3", 30 | "unrs-resolver", 31 | "workerd" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/benchmark/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredwray/cacheable/bc5387b7f0d99ac4c19a2dc69619db9163355fbe/packages/benchmark/.DS_Store -------------------------------------------------------------------------------- /packages/benchmark/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/benchmark/README.md: -------------------------------------------------------------------------------- 1 | [Cacheable](https://github.com/jaredwray/cacheable) 2 | 3 | # Cacheable Benchmark 4 | 5 | 6 | # How to Contribute 7 | 8 | You can contribute by forking the repo and submitting a pull request. Please make sure to add tests and update the documentation. To learn more about how to contribute go to our main README [https://github.com/jaredwray/cacheable](https://github.com/jaredwray/cacheable). This will talk about how to `Open a Pull Request`, `Ask a Question`, or `Post an Issue`. 9 | 10 | # License and Copyright 11 | [MIT © Jared Wray](./LICENSE) -------------------------------------------------------------------------------- /packages/benchmark/memory-lru.ts: -------------------------------------------------------------------------------- 1 | import { createBenchmark, getModuleName, printToConsole, generateAlphaNumeric } from "index.js"; 2 | import { CacheableMemory } from "cacheable"; 3 | import QuickLRU from 'quick-lru'; 4 | import { createLRU } from 'lru.min'; 5 | 6 | const bench = createBenchmark("Memory LRU Benchmark", 100000); 7 | 8 | // Cacheable Memory 9 | const cacheable = new CacheableMemory({ storeHashSize: 1, lruSize: 80000 }); 10 | let cacheableName = getModuleName("Cacheable Memory", "1.10.0"); 11 | 12 | // QuickLRU 13 | const quickLRU = new QuickLRU({maxSize: 80000}); 14 | let quickLRUName = getModuleName("quick-lru"); 15 | 16 | // lru.min 17 | const lruMin = createLRU({ max: 80000 }); 18 | let lruMinName = getModuleName("lru.min"); 19 | 20 | // Map 21 | const map = new Map(); 22 | let mapName = getModuleName("Map", "22"); 23 | 24 | bench.add(`${cacheableName} - set / get`, async () => { 25 | const alphaNumericData = generateAlphaNumeric(); 26 | cacheable.set(alphaNumericData.key, alphaNumericData.value); 27 | cacheable.get(alphaNumericData.key); 28 | }); 29 | 30 | bench.add(`${quickLRUName} - set / get`, async () => { 31 | const alphaNumericData = generateAlphaNumeric(); 32 | quickLRU.set(alphaNumericData.key, alphaNumericData.value); 33 | quickLRU.get(alphaNumericData.key); 34 | }); 35 | 36 | bench.add(`${lruMinName} - set / get`, async () => { 37 | const alphaNumericData = generateAlphaNumeric(); 38 | lruMin.set(alphaNumericData.key, alphaNumericData.value); 39 | lruMin.get(alphaNumericData.key); 40 | }); 41 | 42 | bench.add(`${mapName} - set / get`, async () => { 43 | const alphaNumericData = generateAlphaNumeric(); 44 | map.set(alphaNumericData.key, alphaNumericData.value); 45 | map.get(alphaNumericData.key); 46 | }); 47 | 48 | await bench.run(); 49 | 50 | console.log(`*${bench.name} Results:*`); 51 | printToConsole(bench); 52 | -------------------------------------------------------------------------------- /packages/benchmark/memory.ts: -------------------------------------------------------------------------------- 1 | import { createBenchmark, getModuleName, printToConsole, generateAlphaNumeric } from "index.js"; 2 | import { CacheableMemory } from "cacheable"; 3 | import NodeCache from 'node-cache'; 4 | import { BentoCache, bentostore } from 'bentocache'; 5 | import { memoryDriver } from 'bentocache/drivers/memory'; 6 | 7 | const bench = createBenchmark("Memory Benchmark", 100000); 8 | 9 | // Cacheable Memory 10 | const cacheable = new CacheableMemory(); 11 | let cacheableName = getModuleName("Cacheable Memory", "1.10.0"); 12 | 13 | // Node Cache 14 | const nodeCache = new NodeCache(); 15 | let nodeCacheName = getModuleName("Node Cache"); 16 | 17 | // BentoCache with Memory Driver 18 | const bento = new BentoCache({ 19 | default: 'myCache', 20 | stores: { 21 | // A first cache store named "myCache" using 22 | // only L1 in-memory cache 23 | myCache: bentostore() 24 | .useL1Layer(memoryDriver({ maxSize: '10mb' })) 25 | } 26 | }); 27 | let bentoName = getModuleName("BentoCache"); 28 | 29 | // Map 30 | const map = new Map(); 31 | let mapName = getModuleName("Map", "22"); 32 | 33 | bench.add(`${cacheableName} - set / get`, async () => { 34 | const alphaNumericData = generateAlphaNumeric(); 35 | cacheable.set(alphaNumericData.key, alphaNumericData.value); 36 | cacheable.get(alphaNumericData.key); 37 | }); 38 | 39 | bench.add(`${nodeCacheName} - set / get`, async () => { 40 | const alphaNumericData = generateAlphaNumeric(); 41 | nodeCache.set(alphaNumericData.key, alphaNumericData.value); 42 | nodeCache.get(alphaNumericData.key); 43 | }); 44 | 45 | bench.add(`${bentoName} - set / get`, async () => { 46 | const alphaNumericData = generateAlphaNumeric(); 47 | await bento.set({ key: alphaNumericData.key, value: alphaNumericData.value}); 48 | await bento.get({ key: alphaNumericData.key}); 49 | }); 50 | 51 | bench.add(`${mapName} - set / get`, async () => { 52 | const alphaNumericData = generateAlphaNumeric(); 53 | map.set(alphaNumericData.key, alphaNumericData.value); 54 | map.get(alphaNumericData.key); 55 | }); 56 | 57 | await bench.run(); 58 | 59 | console.log(`*${bench.name} Results:*`); 60 | printToConsole(bench); 61 | -------------------------------------------------------------------------------- /packages/benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cacheable/benchmark", 3 | "version": "1.0.0", 4 | "description": "Cacheable Benchmark Library", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jaredwray/cacheable.git", 18 | "directory": "packages/node-cache" 19 | }, 20 | "author": "Jared Wray ", 21 | "license": "MIT", 22 | "private": false, 23 | "keywords": [ 24 | "cache", 25 | "caching", 26 | "benchmark", 27 | "node", 28 | "nodejs", 29 | "cacheable", 30 | "cacheable-benchmark" 31 | ], 32 | "scripts": { 33 | "build": "echo 'no build needed'", 34 | "prepublish": "pnpm build", 35 | "test": "echo 'no tests needed'", 36 | "test:ci": "echo 'no tests needed'", 37 | "benchmark:memory": "tsx ./memory.ts && tsx ./memory-lru.ts", 38 | "clean": "rimraf ./dist ./coverage ./node_modules" 39 | }, 40 | "dependencies": { 41 | "@faker-js/faker": "^9.8.0", 42 | "@monstermann/tinybench-pretty-printer": "^0.1.0", 43 | "bentocache": "^1.4.0", 44 | "cache-manager": "workspace:^", 45 | "cacheable": "workspace:^", 46 | "lru.min": "^1.1.2", 47 | "quick-lru": "^7.0.1", 48 | "tinybench": "^4.0.1" 49 | }, 50 | "files": [ 51 | "dist", 52 | "license" 53 | ], 54 | "devDependencies": { 55 | "node-cache": "^5.1.2", 56 | "tsx": "^4.19.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/benchmark/src/index.ts: -------------------------------------------------------------------------------- 1 | import {Bench} from 'tinybench'; 2 | import {tinybenchPrinter} from '@monstermann/tinybench-pretty-printer'; 3 | import { faker } from '@faker-js/faker'; 4 | import pkg from '../package.json' assert { type: 'json' }; 5 | import { ObjectGenerator } from 'object-generator.js'; 6 | 7 | export function createBenchmark(name: string, iterations: number) { 8 | const bench = new Bench({ 9 | name, 10 | iterations 11 | }); 12 | 13 | return bench; 14 | } 15 | 16 | export function getModuleName(module: string, version?: string) { 17 | let result = module; 18 | 19 | if (version) { 20 | return `${module} (v${version})`; 21 | } 22 | 23 | if(pkg.name.toLowerCase() === module.toLowerCase()) { 24 | result = `${pkg.name} (v${pkg.version})`; 25 | } else { 26 | for (const [key, value] of Object.entries(pkg.dependencies)) { 27 | if (key.toLowerCase() === module.toLowerCase()) { 28 | let version = value; 29 | // Remove the caret (^) and tilde (~) from the version string 30 | version = version.replace(/^\D*/, ''); 31 | // Remove any trailing characters that are not numbers or dots 32 | version = version.replace(/[^\d.]*$/, ''); 33 | // Remove any leading characters that are not numbers or dots 34 | version = version.replace(/^[^\d.]*/, ''); 35 | // Remove any leading zeros from the version string 36 | version = version.replaceAll(/^0+/g, ''); 37 | 38 | result = `${key} (v${version})`; 39 | break; 40 | } 41 | } 42 | } 43 | 44 | return result; 45 | } 46 | 47 | export function printToConsole(bench: Bench) { 48 | const cli = tinybenchPrinter.toMarkdown(bench); 49 | console.log(cli); 50 | console.log(''); 51 | } 52 | 53 | export function generateDataToArray(count: number) { 54 | const data = new Array<{ key: string; value: string | number | object }>(); 55 | const mapData = generateDataToMap(count); 56 | for (const [key, value] of mapData) { 57 | data.push({ key, value }); 58 | } 59 | return data; 60 | } 61 | 62 | /** 63 | * Generate random alphanumeric, numeric, or object data in a Map. 64 | * @param count The number of data points to generate 65 | */ 66 | export function generateDataToMap(count: number) { 67 | const data = new Map(); 68 | for (let i = 0; i < count; i++) { 69 | const randomNumber = faker.number.int({ min: 1, max: 3 }); 70 | switch (randomNumber) { 71 | case 1: 72 | const alphaNumericData = generateAlphaNumeric(); 73 | data.set(alphaNumericData.key, alphaNumericData.value); 74 | break; 75 | case 2: 76 | const numericData = generateNumeric(); 77 | data.set(numericData.key, numericData.value); 78 | break; 79 | case 3: 80 | const objectData = generateObject(); 81 | data.set(objectData.key, objectData.value); 82 | break; 83 | } 84 | } 85 | 86 | return data; 87 | } 88 | 89 | export function generateAlphaNumericData(count: number) { 90 | const data = new Map(); 91 | for (let i = 0; i < count; i++) { 92 | const alphaNumericData = generateAlphaNumeric(); 93 | data.set(alphaNumericData.key, alphaNumericData.value); 94 | } 95 | return data; 96 | } 97 | 98 | export function generateAlphaNumeric() { 99 | const data = { 100 | key: faker.string.alphanumeric(10), 101 | value: faker.string.alphanumeric(100) 102 | } 103 | return data; 104 | } 105 | 106 | export function generateNumericData(count: number) { 107 | const data = new Map(); 108 | for (let i = 0; i < count; i++) { 109 | const numericData = generateNumeric(); 110 | data.set(numericData.key, numericData.value); 111 | } 112 | return data; 113 | } 114 | 115 | export function generateNumeric() { 116 | const data = { 117 | key: faker.string.numeric(10), 118 | value: faker.number.int({ min: 1, max: 100000000 }) 119 | } 120 | return data; 121 | } 122 | 123 | export function generateObjectData(count: number) { 124 | const data = new Map(); 125 | const objectGenerator = new ObjectGenerator(); 126 | for (let i = 0; i < count; i++) { 127 | const objectData = generateObject(); 128 | data.set(objectData.key, objectData.value); 129 | } 130 | return data; 131 | } 132 | 133 | export function generateObject() { 134 | const objectGenerator = new ObjectGenerator(); 135 | const randomNumber = faker.number.int({ min: 1, max: 10 }); 136 | let data = { 137 | key: '', 138 | value: {} 139 | }; 140 | 141 | switch (randomNumber) { 142 | case 1: 143 | const user = objectGenerator.generateFakeUser(); 144 | data.key = user.id; 145 | data.value = user; 146 | break; 147 | case 2: 148 | const product = objectGenerator.generateFakeProduct(); 149 | data.key = product.id; 150 | data.value = product; 151 | break; 152 | case 3: 153 | const order = objectGenerator.generateFakeOrder(); 154 | data.key = order.id; 155 | data.value = order; 156 | break; 157 | case 4: 158 | const company = objectGenerator.generateFakeCompany(); 159 | data.key = company.id; 160 | data.value = company; 161 | break; 162 | case 5: 163 | const event = objectGenerator.generateFakeEvent(); 164 | data.key = event.id; 165 | data.value = event; 166 | break; 167 | case 6: 168 | const review = objectGenerator.generateFakeReview(); 169 | data.key = review.id; 170 | data.value = review; 171 | break; 172 | case 7: 173 | const car = objectGenerator.generateFakeCar(); 174 | data.key = car.id; 175 | data.value = car; 176 | break; 177 | case 8: 178 | const payment = objectGenerator.generateFakePayment(); 179 | data.key = payment.id; 180 | data.value = payment; 181 | break; 182 | case 9: 183 | const blogPost = objectGenerator.generateFakeBlogPost(); 184 | data.key = blogPost.id; 185 | data.value = blogPost; 186 | break; 187 | case 10: 188 | const comment = objectGenerator.generateFakeComment(); 189 | data.key = comment.id; 190 | data.value = comment; 191 | break; 192 | } 193 | 194 | return data; 195 | } -------------------------------------------------------------------------------- /packages/benchmark/src/object-generator.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | interface User { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | email: string; 8 | } 9 | interface Product { 10 | id: string; 11 | name: string; 12 | price: number; 13 | description: string; 14 | } 15 | interface Order { 16 | id: string; 17 | userId: string; 18 | productIds: string[]; 19 | total: number; 20 | createdAt: Date; 21 | } 22 | interface Company { 23 | id: string; 24 | name: string; 25 | catchPhrase: string; 26 | bs: string; 27 | } 28 | interface Event { 29 | id: string; 30 | title: string; 31 | date: Date; 32 | location: string; 33 | } 34 | interface Review { 35 | id: string; 36 | productId: string; 37 | rating: number; 38 | comment: string; 39 | } 40 | 41 | interface Car { 42 | id: string; 43 | make: string; 44 | model: string; 45 | year: number; 46 | } 47 | 48 | interface Payment { 49 | id: string; 50 | orderId: string; 51 | amount: number; 52 | method: string; 53 | date: Date; 54 | } 55 | 56 | interface BlogPost { 57 | id: string; 58 | title: string; 59 | content: string; 60 | author: string; 61 | publishedAt: Date; 62 | } 63 | 64 | interface Comment { 65 | id: string; 66 | blogPostId: string; 67 | author: string; 68 | content: string; 69 | postedAt: Date; 70 | } 71 | 72 | export class ObjectGenerator { 73 | constructor() { 74 | // Initialize the object generator 75 | } 76 | 77 | public generateFakeUser(): User { 78 | return { 79 | id: faker.string.uuid(), 80 | firstName: faker.person.firstName(), 81 | lastName: faker.person.lastName(), 82 | email: faker.internet.email(), 83 | }; 84 | } 85 | 86 | public generateFakeProduct(): Product { 87 | return { 88 | id: faker.string.uuid(), 89 | name: faker.commerce.productName(), 90 | price: parseFloat(faker.commerce.price()), 91 | description: faker.commerce.productDescription(), 92 | }; 93 | } 94 | 95 | public generateFakeOrder(): Order { 96 | const productCount = faker.helpers.rangeToNumber({ min: 1, max: 5 }); 97 | return { 98 | id: faker.string.uuid(), 99 | userId: faker.string.uuid(), 100 | productIds: Array.from({ length: productCount }, () => faker.string.uuid()), 101 | total: parseFloat(faker.commerce.price()), 102 | createdAt: faker.date.past(), 103 | }; 104 | } 105 | 106 | public generateFakeCompany(): Company { 107 | return { 108 | id: faker.string.uuid(), 109 | name: faker.company.name(), 110 | catchPhrase: faker.company.catchPhrase(), 111 | bs: faker.company.buzzPhrase(), 112 | }; 113 | } 114 | 115 | public generateFakeEvent(): Event { 116 | return { 117 | id: faker.string.uuid(), 118 | title: faker.lorem.words(3), 119 | date: faker.date.future(), 120 | location: `${faker.location.city()}, ${faker.location.country()}`, 121 | }; 122 | } 123 | 124 | public generateFakeReview(): Review { 125 | return { 126 | id: faker.string.uuid(), 127 | productId: faker.string.uuid(), 128 | rating: faker.helpers.rangeToNumber({ min: 1, max: 5 }), 129 | comment: faker.lorem.sentence(), 130 | }; 131 | } 132 | 133 | public generateFakeCar(): Car { 134 | return { 135 | id: faker.string.uuid(), 136 | make: faker.vehicle.manufacturer(), 137 | model: faker.vehicle.model(), 138 | year: parseInt(faker.date.past({ years: 30 }).getFullYear().toString()), 139 | }; 140 | } 141 | 142 | public generateFakePayment(): Payment { 143 | return { 144 | id: faker.string.uuid(), 145 | orderId: faker.string.uuid(), 146 | amount: parseFloat(faker.commerce.price()), 147 | method: faker.finance.transactionType(), 148 | date: faker.date.recent(), 149 | }; 150 | } 151 | 152 | public generateFakeBlogPost(): BlogPost { 153 | return { 154 | id: faker.string.uuid(), 155 | title: faker.lorem.sentence(), 156 | content: faker.lorem.paragraphs(2), 157 | author: faker.person.fullName(), 158 | publishedAt: faker.date.recent(), 159 | }; 160 | } 161 | 162 | public generateFakeComment(): Comment { 163 | return { 164 | id: faker.string.uuid(), 165 | blogPostId: faker.string.uuid(), 166 | author: faker.person.fullName(), 167 | content: faker.lorem.sentence(), 168 | postedAt: faker.date.recent(), 169 | }; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /packages/benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 7 | 8 | /* Emit */ 9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 12 | 13 | /* Interop Constraints */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | 17 | /* Type Checking */ 18 | "strict": true, /* Enable all strict type-checking options. */ 19 | 20 | /* Completeness */ 21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 22 | "lib": [ 23 | "ESNext", "DOM" 24 | ], 25 | "resolveJsonModule": true 26 | } 27 | } -------------------------------------------------------------------------------- /packages/cache-manager/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/cache-manager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-manager", 3 | "version": "7.0.0", 4 | "description": "Cache Manager for Node.js", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "files": [ 16 | "dist", 17 | "LICENSE" 18 | ], 19 | "scripts": { 20 | "clean": "rimraf ./dist ./coverage ./node_modules", 21 | "test": "xo --fix && vitest run --coverage", 22 | "test:ci": "xo && vitest run", 23 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean", 24 | "prepublish": "pnpm run build" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/jaredwray/cacheable.git", 29 | "directory": "packages/cache-manager" 30 | }, 31 | "keywords": [ 32 | "cache", 33 | "caching", 34 | "cache manager", 35 | "node", 36 | "node.js", 37 | "in-memory cache", 38 | "redis", 39 | "memcached", 40 | "multi-store cache", 41 | "ttl", 42 | "caching layer", 43 | "cache abstraction", 44 | "cache middleware", 45 | "cache strategies", 46 | "cache wrapper" 47 | ], 48 | "authors": [ 49 | { 50 | "name": "Jared Wray", 51 | "email": "me@jaredwray.com" 52 | }, 53 | { 54 | "name": "Tim Phan", 55 | "email": "timphan.dev@gmail.com" 56 | } 57 | ], 58 | "license": "MIT", 59 | "dependencies": { 60 | "keyv": "^5.3.3" 61 | }, 62 | "devDependencies": { 63 | "@faker-js/faker": "^9.8.0", 64 | "@keyv/redis": "^4.4.0", 65 | "@types/node": "^22.15.30", 66 | "@vitest/coverage-v8": "^3.2.2", 67 | "@vitest/spy": "^3.2.2", 68 | "cache-manager-redis-yet": "^5.1.5", 69 | "cacheable": "workspace:^", 70 | "rimraf": "^6.0.1", 71 | "tsup": "^8.5.0", 72 | "typescript": "^5.8.3", 73 | "vitest": "^3.2.2", 74 | "xo": "^1.0.5" 75 | }, 76 | "xo": { 77 | "rules": { 78 | "@typescript-eslint/no-unsafe-call": "off", 79 | "@typescript-eslint/no-unsafe-assignment": "off" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/cache-manager/src/coalesce-async.ts: -------------------------------------------------------------------------------- 1 | type PromiseCallback = { 2 | resolve: (value: T | PromiseLike) => void; 3 | reject: (reason: E) => void; 4 | }; 5 | 6 | const callbacks = new Map(); 7 | 8 | function hasKey(key: string): boolean { 9 | return callbacks.has(key); 10 | } 11 | 12 | function addKey(key: string): void { 13 | callbacks.set(key, []); 14 | } 15 | 16 | function removeKey(key: string): void { 17 | callbacks.delete(key); 18 | } 19 | 20 | function addCallbackToKey(key: string, callback: PromiseCallback): void { 21 | const stash = getCallbacksByKey(key); 22 | stash.push(callback); 23 | callbacks.set(key, stash); 24 | } 25 | 26 | function getCallbacksByKey(key: string): Array> { 27 | return callbacks.get(key) ?? []; 28 | } 29 | 30 | async function enqueue(key: string): Promise { 31 | return new Promise((resolve, reject) => { 32 | const callback: PromiseCallback = {resolve, reject}; 33 | addCallbackToKey(key, callback); 34 | }); 35 | } 36 | 37 | function dequeue(key: string): Array> { 38 | const stash = getCallbacksByKey(key); 39 | removeKey(key); 40 | return stash; 41 | } 42 | 43 | function coalesce(options: {key: string; error?: Error; result?: T}): void { 44 | const {key, error, result} = options; 45 | 46 | for (const callback of dequeue(key)) { 47 | if (error) { 48 | /* c8 ignore next 3 */ 49 | callback.reject(error); 50 | } else { 51 | callback.resolve(result); 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Enqueue a promise for the group identified by `key`. 58 | * 59 | * All requests received for the same key while a request for that key 60 | * is already being executed will wait. Once the running request settles 61 | * then all the waiting requests in the group will settle, too. 62 | * This minimizes how many times the function itself runs at the same time. 63 | * This function resolves or rejects according to the given function argument. 64 | * 65 | * @url https://github.com/douglascayers/promise-coalesce 66 | */ 67 | export async function coalesceAsync( 68 | /** 69 | * Any identifier to group requests together. 70 | */ 71 | key: string, 72 | /** 73 | * The function to run. 74 | */ 75 | fnc: () => T | PromiseLike, 76 | ): Promise { 77 | if (!hasKey(key)) { 78 | addKey(key); 79 | try { 80 | const result = await Promise.resolve(fnc()); 81 | coalesce({key, result}); 82 | return result; 83 | } catch (error: any) { 84 | coalesce({key, error}); 85 | 86 | throw error; 87 | } 88 | } 89 | 90 | return enqueue(key); 91 | } 92 | -------------------------------------------------------------------------------- /packages/cache-manager/src/is-object.ts: -------------------------------------------------------------------------------- 1 | export function isObject>(value: unknown): value is T { 2 | return value !== null && typeof value === 'object' && !Array.isArray(value); 3 | } 4 | -------------------------------------------------------------------------------- /packages/cache-manager/src/keyv-adapter.ts: -------------------------------------------------------------------------------- 1 | import {type KeyvStoreAdapter, type StoredData} from 'keyv'; 2 | 3 | export type CacheManagerStore = { 4 | name: string; 5 | isCacheable?: (value: unknown) => boolean; 6 | get(key: string): Promise; 7 | mget(...keys: string[]): Promise; 8 | set(key: string, value: any, ttl?: number): Promise; 9 | mset(data: Record, ttl?: number): Promise; 10 | del(key: string): Promise; 11 | mdel(...keys: string[]): Promise; 12 | ttl(key: string, ttl?: number): Promise; 13 | keys(): Promise; 14 | reset?(): Promise; 15 | on?(event: string, listener: (...arguments_: any[]) => void): void; 16 | disconnect?(): Promise; 17 | }; 18 | 19 | export class KeyvAdapter implements KeyvStoreAdapter { 20 | opts: any; 21 | namespace?: string | undefined; 22 | private readonly _cache: CacheManagerStore; 23 | constructor(store: CacheManagerStore) { 24 | this._cache = store; 25 | } 26 | 27 | async get(key: string): Promise | undefined> { 28 | const value = await this._cache.get(key); 29 | if (value !== undefined && value !== null) { 30 | return value as T; 31 | } 32 | 33 | return undefined; 34 | } 35 | 36 | async set(key: string, value: any, ttl?: number) { 37 | await this._cache.set(key, value, ttl); 38 | return true; 39 | } 40 | 41 | async delete(key: string): Promise { 42 | await this._cache.del(key); 43 | return true; 44 | } 45 | 46 | async clear(): Promise { 47 | return this._cache.reset?.(); 48 | } 49 | 50 | async has?(key: string): Promise { 51 | const result = await this._cache.get(key); 52 | if (result) { 53 | return true; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | async getMany?(keys: string[]): Promise>> { 60 | // eslint-disable-next-line promise/prefer-await-to-then 61 | return this._cache.mget(...keys).then(values => values.map(value => (value as T))); 62 | } 63 | 64 | async deleteMany?(key: string[]): Promise { 65 | await this._cache.mdel(...key); 66 | return true; 67 | } 68 | 69 | /* c8 ignore next 5 */ 70 | on(event: string, listener: (...arguments_: any[]) => void) { 71 | this._cache.on?.(event, listener); 72 | 73 | return this; 74 | } 75 | 76 | async disconnect?(): Promise { 77 | await this._cache.disconnect?.(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/cache-manager/src/lt.ts: -------------------------------------------------------------------------------- 1 | export function lt(number1?: number, number2?: number) { 2 | return typeof number1 === 'number' && typeof number2 === 'number' ? number1 < number2 : false; 3 | } 4 | -------------------------------------------------------------------------------- /packages/cache-manager/src/run-if-fn.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/chakra-ui/chakra-ui/blob/main/packages/utils/src/run-if-fn.ts 2 | // eslint-disable-next-line @typescript-eslint/naming-convention 3 | type Function_ = (...arguments_: P[]) => T; 4 | 5 | export function runIfFn(valueOrFunction: T | Function_, ...arguments_: P[]): T { 6 | return typeof valueOrFunction === 'function' ? (valueOrFunction as Function_)(...arguments_) : valueOrFunction; 7 | } 8 | -------------------------------------------------------------------------------- /packages/cache-manager/test/cache-id.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, 4 | } from 'vitest'; 5 | import {createCache} from '../src/index.js'; 6 | 7 | describe('cacheId', () => { 8 | let keyv: Keyv; 9 | 10 | beforeEach(async () => { 11 | keyv = new Keyv(); 12 | }); 13 | 14 | it('user set', () => { 15 | const cache = createCache({stores: [keyv], cacheId: 'my-cache-id'}); 16 | expect(cache.cacheId()).toEqual('my-cache-id'); 17 | }); 18 | it('auto generated', () => { 19 | const cache = createCache({stores: [keyv]}); 20 | expect(cache.cacheId()).toBeTypeOf('string'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/cache-manager/test/clear.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import {Keyv} from 'keyv'; 3 | import { 4 | beforeEach, describe, expect, it, 5 | } from 'vitest'; 6 | import {faker} from '@faker-js/faker'; 7 | import {createCache} from '../src/index.js'; 8 | import {sleep} from './sleep.js'; 9 | 10 | describe('clear', () => { 11 | let keyv: Keyv; 12 | let cache: ReturnType; 13 | let ttl = 500; 14 | const data = {key: '', value: ''}; 15 | 16 | beforeEach(async () => { 17 | data.key = faker.string.alpha(20); 18 | data.value = faker.string.sample(); 19 | ttl = faker.number.int({min: 500, max: 1000}); 20 | keyv = new Keyv(); 21 | cache = createCache({stores: [keyv]}); 22 | }); 23 | 24 | it('basic', async () => { 25 | const array = [1, 2, 3]; 26 | for (const index of array) { 27 | await cache.set(data.key + index, data.value + index, ttl); 28 | await expect(cache.get(data.key + index)).resolves.toEqual(data.value + index); 29 | } 30 | 31 | await expect(cache.clear()).resolves.toEqual(true); 32 | for (const index of array) { 33 | await expect(cache.get(data.key + index)).resolves.toBeUndefined(); 34 | } 35 | }); 36 | 37 | it('clear should be non-blocking', async () => { 38 | const secondKeyv = new Keyv(); 39 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true}); 40 | await cache.set(data.key, data.value); 41 | expect(await secondKeyv.get(data.key)).toBe(data.value); 42 | await cache.clear(); 43 | await sleep(200); 44 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 45 | await expect(secondKeyv.get(data.key)).resolves.toBeUndefined(); 46 | }); 47 | 48 | it('error', async () => { 49 | await cache.set(data.key, data.value); 50 | const error = new Error('clear error'); 51 | keyv.clear = () => { 52 | throw error; 53 | }; 54 | 55 | await expect(cache.clear()).rejects.toThrowError(error); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/cache-manager/test/del.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {createCache} from '../src/index.js'; 7 | import {sleep} from './sleep.js'; 8 | 9 | describe('del', () => { 10 | let keyv: Keyv; 11 | let cache: ReturnType; 12 | let ttl = 500; 13 | const data = {key: '', value: ''}; 14 | 15 | beforeEach(async () => { 16 | data.key = faker.string.alpha(20); 17 | data.value = faker.string.sample(); 18 | ttl = faker.number.int({min: 500, max: 1000}); 19 | keyv = new Keyv(); 20 | cache = createCache({stores: [keyv]}); 21 | }); 22 | 23 | it('basic', async () => { 24 | await cache.set(data.key, data.value, ttl); 25 | await expect(cache.get(data.key)).resolves.toEqual(data.value); 26 | await expect(cache.del(data.key)).resolves.toEqual(true); 27 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 28 | }); 29 | 30 | it('error', async () => { 31 | await cache.set(data.key, data.value); 32 | const error = new Error('delete error'); 33 | keyv.delete = () => { 34 | throw error; 35 | }; 36 | 37 | await expect(cache.del(data.key)).rejects.toThrowError(error); 38 | }); 39 | it('del should be non-blocking', async () => { 40 | const secondKeyv = new Keyv(); 41 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true}); 42 | await cache.set(data.key, data.value); 43 | await cache.del(data.key); 44 | await sleep(200); 45 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/cache-manager/test/disconnect.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | describe, expect, test, vi, 3 | } from 'vitest'; 4 | import {Keyv} from 'keyv'; 5 | import KeyvRedis from '@keyv/redis'; 6 | import {CacheableMemory} from 'cacheable'; 7 | import {createCache} from '../src/index.js'; 8 | 9 | describe('disconnect', () => { 10 | test('disconnect from multiple stores', async () => { 11 | const cacheableKeyvStore = new Keyv({ 12 | store: new CacheableMemory({ttl: 60_000, lruSize: 5000}), 13 | }); 14 | const redisKeyvStore = new Keyv({ 15 | store: new KeyvRedis('redis://localhost:6379'), 16 | }); 17 | // Multiple stores 18 | const cache = createCache({ 19 | stores: [ 20 | cacheableKeyvStore, 21 | redisKeyvStore, 22 | ], 23 | }); 24 | 25 | const cacheableDisconnectSpy = vi.spyOn(cacheableKeyvStore, 'disconnect'); 26 | const redisDisconnectSpy = vi.spyOn(redisKeyvStore, 'disconnect'); 27 | await cache.disconnect(); 28 | 29 | expect(cacheableDisconnectSpy).toBeCalled(); 30 | expect(redisDisconnectSpy).toBeCalled(); 31 | }); 32 | test('error', async () => { 33 | const keyv = new Keyv(); 34 | const cache = createCache({ 35 | stores: [ 36 | keyv, 37 | ], 38 | }); 39 | const error = new Error('disconnect error'); 40 | keyv.disconnect = () => { 41 | throw error; 42 | }; 43 | 44 | await expect(cache.disconnect()).rejects.toThrowError(error); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/cache-manager/test/events.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import {Keyv} from 'keyv'; 3 | import { 4 | beforeEach, describe, expect, it, vi, 5 | } from 'vitest'; 6 | import {faker} from '@faker-js/faker'; 7 | import {createCache} from '../src/index.js'; 8 | import {sleep} from './sleep.js'; 9 | 10 | describe('events', () => { 11 | let keyv: Keyv; 12 | let cache: ReturnType; 13 | let ttl = 500; 14 | const data = {key: '', value: ''}; 15 | 16 | beforeEach(async () => { 17 | data.key = faker.string.alpha(20); 18 | data.value = faker.string.sample(); 19 | ttl = faker.number.int({min: 500, max: 1000}); 20 | keyv = new Keyv(); 21 | cache = createCache({stores: [keyv]}); 22 | }); 23 | 24 | it('event: set', async () => { 25 | const listener = vi.fn(() => {}); 26 | cache.on('set', listener); 27 | 28 | await cache.set(data.key, data.value); 29 | expect(listener).toBeCalledWith(data); 30 | 31 | const error = new Error('set failed'); 32 | keyv.set = () => { 33 | throw error; 34 | }; 35 | 36 | await expect(cache.set(data.key, data.value)).rejects.toThrowError(error); 37 | expect(listener).toBeCalledWith({key: data.key, value: data.value, error}); 38 | }); 39 | 40 | it('event: del', async () => { 41 | const listener = vi.fn(() => {}); 42 | cache.on('del', listener); 43 | 44 | await cache.set(data.key, data.value); 45 | await cache.del(data.key); 46 | expect(listener).toBeCalledWith({key: data.key}); 47 | 48 | const error = new Error('del failed'); 49 | keyv.delete = () => { 50 | throw error; 51 | }; 52 | 53 | await expect(cache.del(data.key)).rejects.toThrowError(error); 54 | expect(listener).toBeCalledWith({key: data.key, error}); 55 | }); 56 | 57 | it('event: clear', async () => { 58 | const listener = vi.fn(() => {}); 59 | cache.on('clear', listener); 60 | 61 | await cache.set(data.key, data.value); 62 | await cache.clear(); 63 | expect(listener).toBeCalled(); 64 | 65 | const error = new Error('clear failed'); 66 | keyv.clear = () => { 67 | throw error; 68 | }; 69 | 70 | await expect(cache.clear()).rejects.toThrowError(error); 71 | expect(listener).toBeCalledWith(error); 72 | }); 73 | 74 | it('event: refresh', async () => { 75 | const getValue = () => data.value; 76 | const listener = vi.fn(() => {}); 77 | cache.on('refresh', listener); 78 | 79 | const refreshThreshold = ttl / 2; 80 | await cache.wrap(data.key, getValue, ttl, refreshThreshold); 81 | await sleep(ttl - refreshThreshold + 100); 82 | await cache.wrap(data.key, getValue, ttl, refreshThreshold); 83 | await vi.waitUntil(() => listener.mock.calls.length > 0); 84 | expect(listener).toBeCalledWith({key: data.key, value: data.value}); 85 | }); 86 | 87 | it('event: refresh get error', async () => { 88 | const listener = vi.fn(() => {}); 89 | cache.on('refresh', listener); 90 | 91 | const refreshThreshold = ttl / 2; 92 | await cache.wrap(data.key, () => data.value, ttl, refreshThreshold); 93 | 94 | const error = new Error('get failed'); 95 | await sleep(ttl - refreshThreshold + 100); 96 | await cache.wrap( 97 | data.key, 98 | () => { 99 | throw error; 100 | }, 101 | ttl, 102 | refreshThreshold, 103 | ); 104 | await vi.waitUntil(() => listener.mock.calls.length > 0); 105 | expect(listener).toBeCalledWith({key: data.key, value: data.value, error}); 106 | }); 107 | 108 | it('event: refresh set error', async () => { 109 | const getValue = () => data.value; 110 | const listener = vi.fn(() => {}); 111 | cache.on('refresh', listener); 112 | 113 | const refreshThreshold = ttl / 2; 114 | await cache.wrap(data.key, getValue, ttl, refreshThreshold); 115 | 116 | const error = new Error('set failed'); 117 | keyv.set = () => { 118 | throw error; 119 | }; 120 | 121 | await sleep(ttl - refreshThreshold + 100); 122 | await cache.wrap(data.key, getValue, ttl, refreshThreshold); 123 | await vi.waitUntil(() => listener.mock.calls.length > 0); 124 | expect(listener).toBeCalledWith({key: data.key, value: data.value, error}); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /packages/cache-manager/test/example.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, test} from 'vitest'; 2 | import {Keyv} from 'keyv'; 3 | import KeyvRedis from '@keyv/redis'; 4 | import {CacheableMemory, KeyvCacheableMemory} from 'cacheable'; 5 | import {createCache} from '../src/index.js'; 6 | 7 | describe('examples of cache-manager', async () => { 8 | test('set and get with multiple stores', async () => { 9 | // Multiple stores 10 | const cache = createCache({ 11 | stores: [ 12 | // High performance in-memory cache with LRU and TTL 13 | new Keyv({ 14 | store: new CacheableMemory({ttl: 60_000, lruSize: 5000}), 15 | }), 16 | 17 | // Redis Store 18 | new Keyv({ 19 | store: new KeyvRedis('redis://localhost:6379'), 20 | }), 21 | ], 22 | }); 23 | await cache.set('foo', 'bar'); 24 | const value = await cache.get('foo'); 25 | expect(value).toBe('bar'); 26 | }); 27 | test('set and get with KeyvCacheableMemory', async () => { 28 | const cache = createCache({ 29 | stores: [ 30 | // High performance in-memory cache with LRU and TTL 31 | new Keyv({ 32 | store: new KeyvCacheableMemory({ttl: 60_000, lruSize: 5000}), 33 | }), 34 | 35 | // Redis Store 36 | new Keyv({ 37 | store: new KeyvRedis('redis://localhost:6379'), 38 | }), 39 | ], 40 | }); 41 | await cache.set('foo', 'bar'); 42 | const value = await cache.get('foo'); 43 | expect(value).toBe('bar'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/cache-manager/test/get.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {createCache} from '../src/index.js'; 7 | import {sleep} from './sleep.js'; 8 | 9 | describe('get', () => { 10 | let keyv: Keyv; 11 | let cache: ReturnType; 12 | let ttl = 500; 13 | const data = {key: '', value: ''}; 14 | 15 | beforeEach(async () => { 16 | data.key = faker.string.alpha(20); 17 | data.value = faker.string.sample(); 18 | ttl = faker.number.int({min: 500, max: 1000}); 19 | keyv = new Keyv(); 20 | cache = createCache({stores: [keyv]}); 21 | }); 22 | 23 | it('basic', async () => { 24 | await cache.set(data.key, data.value); 25 | await expect(cache.get(data.key)).resolves.toEqual(data.value); 26 | }); 27 | 28 | it('expired', async () => { 29 | await cache.set(data.key, data.value, ttl); 30 | await sleep(ttl + 100); 31 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 32 | }); 33 | 34 | it('error', async () => { 35 | await cache.set(data.key, data.value); 36 | keyv.get = () => { 37 | throw new Error('get error'); 38 | }; 39 | 40 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 41 | }); 42 | it('error on non-blocking enabled', async () => { 43 | const secondKeyv = new Keyv(); 44 | keyv.get = () => { 45 | throw new Error('get error'); 46 | }; 47 | 48 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true}); 49 | await cache.set(data.key, data.value); 50 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/cache-manager/test/init.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {createCache} from '../src/index.js'; 7 | import {sleep} from './sleep.js'; 8 | 9 | describe('init', () => { 10 | let ttl = 1000; 11 | const data = {key: '', value: ''}; 12 | 13 | beforeEach(async () => { 14 | data.key = faker.string.alpha(20); 15 | data.value = faker.string.sample(); 16 | ttl = faker.number.int({min: 500, max: 1000}); 17 | }); 18 | 19 | it('basic', async () => { 20 | const cache = createCache(); 21 | expect(cache).toBeDefined(); 22 | }); 23 | 24 | it('default ttl', async () => { 25 | const cache = createCache({ttl}); 26 | await cache.set(data.key, data.value); 27 | await sleep(ttl + 100); 28 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 29 | }); 30 | 31 | it('single store', async () => { 32 | const cache = createCache({ 33 | stores: [new Keyv()], 34 | }); 35 | expect(cache).toBeDefined(); 36 | }); 37 | 38 | it('multiple stores', async () => { 39 | const store1 = new Keyv(); 40 | const store2 = new Keyv(); 41 | const cache = createCache({ 42 | stores: [store1, store2], 43 | }); 44 | expect(cache).toBeDefined(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/cache-manager/test/keyv-adapter.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | describe, expect, it, vi, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {redisStore as redisYetStore} from 'cache-manager-redis-yet'; 7 | import {createCache, KeyvAdapter, type CacheManagerStore} from '../src/index.js'; 8 | 9 | const mockCacheManagerStore: CacheManagerStore = { 10 | name: 'MockCacheManagerStore', 11 | isCacheable: vi.fn((value: unknown) => value !== undefined), 12 | get: vi.fn(async (key: string) => `Value for ${key}`), 13 | mget: vi.fn(async (...keys: string[]) => keys.map(key => `Value for ${key}`)), 14 | set: vi.fn(async (key: string, value: any, ttl?: number) => `Set ${key} to ${value} with TTL ${ttl}`), 15 | mset: vi.fn(async () => undefined), 16 | del: vi.fn(async () => undefined), 17 | mdel: vi.fn(async () => undefined), 18 | ttl: vi.fn(async () => 0), 19 | keys: vi.fn(async () => ['key1', 'key2', 'key3']), 20 | reset: vi.fn(async () => undefined), 21 | on: vi.fn((event: string) => { 22 | console.log(`Event ${event} registered.`); 23 | }), 24 | disconnect: vi.fn(async () => undefined), 25 | }; 26 | 27 | describe('keyv-adapter', async () => { 28 | it('able to handle redis yet third party conversion', async () => { 29 | const store = await redisYetStore(); 30 | const adapter = new KeyvAdapter(store); 31 | const keyv = new Keyv({store: adapter}); 32 | const cache = createCache({stores: [keyv]}); 33 | const key = faker.string.alpha(20); 34 | const value = faker.string.sample(); 35 | await cache.set(key, value); 36 | const result = await cache.get(key); 37 | expect(result).toEqual(value); 38 | }); 39 | 40 | it('returns undefined on get', async () => { 41 | const store = await redisYetStore(); 42 | const adapter = new KeyvAdapter(store); 43 | const keyv = new Keyv({store: adapter}); 44 | const cache = createCache({stores: [keyv]}); 45 | const result = await cache.get('key'); 46 | expect(result).toBeUndefined(); 47 | }); 48 | 49 | it('deletes a key', async () => { 50 | const store = await redisYetStore(); 51 | const adapter = new KeyvAdapter(store); 52 | const keyv = new Keyv({store: adapter}); 53 | const cache = createCache({stores: [keyv]}); 54 | const key = faker.string.alpha(20); 55 | const value = faker.string.sample(); 56 | await cache.set(key, value); 57 | const result = await cache.get(key); 58 | expect(result).toEqual(value); 59 | await cache.del(key); 60 | const result2 = await cache.get(key); 61 | expect(result2).toBeUndefined(); 62 | }); 63 | 64 | it('clears the cache', async () => { 65 | const store = await redisYetStore(); 66 | const adapter = new KeyvAdapter(store); 67 | const keyv = new Keyv({store: adapter}); 68 | const cache = createCache({stores: [keyv]}); 69 | const key = faker.string.alpha(20); 70 | const value = faker.string.sample(); 71 | await cache.set(key, value); 72 | const result = await cache.get(key); 73 | expect(result).toEqual(value); 74 | await cache.clear(); 75 | const result2 = await cache.get(key); 76 | expect(result2).toBeUndefined(); 77 | }); 78 | 79 | it('returns false on has', async () => { 80 | const store = await redisYetStore(); 81 | const adapter = new KeyvAdapter(store); 82 | const keyv = new Keyv({store: adapter}); 83 | const result = await keyv.has('key'); 84 | expect(result).toEqual(false); 85 | }); 86 | 87 | it('returns true on has', async () => { 88 | const store = await redisYetStore(); 89 | const adapter = new KeyvAdapter(store); 90 | const keyv = new Keyv({store: adapter}); 91 | const key = faker.string.alpha(20); 92 | const value = faker.string.sample(); 93 | await keyv.set(key, value); 94 | const result = await keyv.has(key); 95 | expect(result).toEqual(true); 96 | }); 97 | 98 | it('gets many keys', async () => { 99 | const store = await redisYetStore(); 100 | const adapter = new KeyvAdapter(store); 101 | const keyv = new Keyv({store: adapter}); 102 | const cache = createCache({stores: [keyv]}); 103 | const list = [ 104 | {key: faker.string.alpha(20), value: faker.string.sample()}, 105 | {key: faker.string.alpha(20), value: faker.string.sample()}, 106 | ]; 107 | 108 | await cache.mset(list); 109 | const keyvResult = await keyv.get(list.map(({key}) => key)); 110 | expect(keyvResult).toEqual(list.map(({value}) => value)); 111 | const result = await cache.mget(list.map(({key}) => key)); 112 | expect(result).toEqual([list[0].value, list[1].value]); 113 | }); 114 | 115 | it('should delete many keys', async () => { 116 | const store = await redisYetStore(); 117 | const adapter = new KeyvAdapter(store); 118 | const keyv = new Keyv({store: adapter}); 119 | const list = [ 120 | {key: faker.string.alpha(20), value: faker.string.sample()}, 121 | {key: faker.string.alpha(20), value: faker.string.sample()}, 122 | ]; 123 | 124 | await keyv.set(list[0].key, list[0].value); 125 | await keyv.set(list[1].key, list[1].value); 126 | await keyv.delete(list.map(({key}) => key)); 127 | const result = await keyv.get(list.map(({key}) => key)); 128 | expect(result).toEqual([undefined, undefined]); 129 | }); 130 | 131 | it('should disconnect', async () => { 132 | // Store without disconnect 133 | const storeNoDisconnect = await redisYetStore(); 134 | const adapterNoDisconnect = new KeyvAdapter(storeNoDisconnect); 135 | const keyvNoDisconnect = new Keyv({store: adapterNoDisconnect}); 136 | 137 | await keyvNoDisconnect.disconnect(); 138 | 139 | // Store with disconnect 140 | const adapterWithDisconnect = new KeyvAdapter(mockCacheManagerStore); 141 | const keyvWithDisconnect = new Keyv({store: adapterWithDisconnect}); 142 | 143 | await keyvWithDisconnect.disconnect(); 144 | expect(mockCacheManagerStore.disconnect).toBeCalled(); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /packages/cache-manager/test/mdel.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-floating-promises, promise/prefer-await-to-then */ 2 | import {Keyv} from 'keyv'; 3 | import { 4 | beforeEach, describe, expect, it, vi, 5 | } from 'vitest'; 6 | import {faker} from '@faker-js/faker'; 7 | import {createCache} from '../src/index.js'; 8 | import {sleep} from './sleep.js'; 9 | 10 | describe('mdel', () => { 11 | let keyv: Keyv; 12 | let cache: ReturnType; 13 | let ttl = 500; 14 | let list = [] as Array<{key: string; value: string}>; 15 | 16 | beforeEach(async () => { 17 | ttl = faker.number.int({min: 500, max: 1000}); 18 | keyv = new Keyv(); 19 | cache = createCache({stores: [keyv]}); 20 | list = [ 21 | {key: faker.string.alpha(20), value: faker.string.sample()}, 22 | {key: faker.string.alpha(20), value: faker.string.sample()}, 23 | {key: faker.string.alpha(20), value: faker.string.sample()}, 24 | ]; 25 | }); 26 | 27 | it('basic', async () => { 28 | await cache.mset(list); 29 | await expect(cache.get(list[0].key)).resolves.toEqual(list[0].value); 30 | await expect(cache.get(list[1].key)).resolves.toEqual(list[1].value); 31 | await expect(cache.get(list[2].key)).resolves.toEqual(list[2].value); 32 | await cache.mdel([list[0].key, list[1].key]); 33 | await expect(cache.get(list[0].key)).resolves.toBeUndefined(); 34 | await expect(cache.get(list[1].key)).resolves.toBeUndefined(); 35 | await expect(cache.get(list[2].key)).resolves.toEqual(list[2].value); 36 | }); 37 | 38 | it('should work blocking', async () => { 39 | let resolveDeleted: (value: boolean) => void = () => undefined; 40 | const deletePromise = new Promise(_resolve => { 41 | resolveDeleted = _resolve; 42 | }); 43 | const cache = createCache({stores: [keyv], nonBlocking: false}); 44 | await cache.mset(list); 45 | 46 | const delHandler = vi.spyOn(keyv, 'delete').mockReturnValue(deletePromise); 47 | const deleteResolved = vi.fn(); 48 | const deleteRejected = vi.fn(); 49 | cache.mdel(list.map(({key}) => key)).catch(deleteRejected).then(deleteResolved); 50 | 51 | expect(delHandler).toBeCalledTimes(list.length); 52 | 53 | await sleep(200); 54 | 55 | expect(deleteResolved).not.toBeCalled(); 56 | expect(deleteRejected).not.toBeCalled(); 57 | 58 | resolveDeleted(true); 59 | await sleep(1); 60 | 61 | expect(deleteResolved).toBeCalled(); 62 | expect(deleteRejected).not.toBeCalled(); 63 | }); 64 | 65 | it('should work non-blocking', async () => { 66 | const deletePromise = new Promise(_resolve => { 67 | // Do nothing, this will be a never resolved promise 68 | }); 69 | const cache = createCache({stores: [keyv], nonBlocking: true}); 70 | await cache.mset(list); 71 | 72 | const delHandler = vi.spyOn(keyv, 'delete').mockReturnValue(deletePromise); 73 | const deleteResolved = vi.fn(); 74 | const deleteRejected = vi.fn(); 75 | cache.mdel(list.map(({key}) => key)).catch(deleteRejected).then(deleteResolved); 76 | 77 | expect(delHandler).toBeCalledTimes(list.length); 78 | 79 | await sleep(1); 80 | 81 | expect(deleteResolved).toBeCalled(); 82 | expect(deleteRejected).not.toBeCalled(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/cache-manager/test/mget.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {createCache} from '../src/index.js'; 7 | 8 | describe('mget', () => { 9 | let keyv: Keyv; 10 | let cache: ReturnType; 11 | let ttl = 500; 12 | let list = [] as Array<{key: string; value: string}>; 13 | 14 | beforeEach(async () => { 15 | ttl = faker.number.int({min: 500, max: 1000}); 16 | keyv = new Keyv(); 17 | cache = createCache({stores: [keyv]}); 18 | list = [ 19 | {key: faker.string.alpha(20), value: faker.string.sample()}, 20 | {key: faker.string.alpha(20), value: faker.string.sample()}, 21 | {key: faker.string.alpha(20), value: faker.string.sample()}, 22 | ]; 23 | }); 24 | 25 | it('basic', async () => { 26 | await cache.mset(list); 27 | const keys = list.map(item => item.key); 28 | const values = list.map(item => item.value); 29 | await expect(cache.mget(keys)).resolves.toEqual(values); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/cache-manager/test/mset.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-floating-promises, promise/prefer-await-to-then */ 2 | import {Keyv} from 'keyv'; 3 | import { 4 | beforeEach, describe, expect, it, vi, 5 | } from 'vitest'; 6 | import {faker} from '@faker-js/faker'; 7 | import {createCache} from '../src/index.js'; 8 | import {sleep} from './sleep.js'; 9 | 10 | describe('mset', () => { 11 | let keyv: Keyv; 12 | let cache: ReturnType; 13 | let ttl = 500; 14 | 15 | beforeEach(async () => { 16 | ttl = faker.number.int({min: 500, max: 1000}); 17 | keyv = new Keyv(); 18 | cache = createCache({stores: [keyv]}); 19 | }); 20 | 21 | it('basic', async () => { 22 | const list = [ 23 | {key: faker.string.alpha(20), value: faker.string.sample()}, 24 | {key: faker.string.alpha(20), value: faker.string.sample()}, 25 | {key: faker.string.alpha(20), value: faker.string.sample()}, 26 | ]; 27 | 28 | await expect(cache.mset(list)).resolves.toEqual(list); 29 | await expect(cache.get(list[0].key)).resolves.toEqual(list[0].value); 30 | }); 31 | 32 | it('should work blocking', async () => { 33 | const list = [ 34 | {key: faker.string.alpha(20), value: faker.string.sample()}, 35 | {key: faker.string.alpha(20), value: faker.string.sample()}, 36 | {key: faker.string.alpha(20), value: faker.string.sample()}, 37 | ]; 38 | 39 | let resolveSet: (value: boolean) => void = () => undefined; 40 | const setPromise = new Promise(_resolve => { 41 | resolveSet = _resolve; 42 | }); 43 | 44 | const cache = createCache({stores: [keyv], nonBlocking: false}); 45 | const setHandler = vi.spyOn(keyv, 'set').mockReturnValue(setPromise); 46 | const setResolved = vi.fn(); 47 | const setRejected = vi.fn(); 48 | cache.mset(list).catch(setRejected).then(setResolved); 49 | 50 | expect(setHandler).toBeCalledTimes(list.length); 51 | 52 | await sleep(200); 53 | 54 | expect(setResolved).not.toBeCalled(); 55 | expect(setRejected).not.toBeCalled(); 56 | 57 | resolveSet(true); 58 | await sleep(1); 59 | 60 | expect(setResolved).toBeCalled(); 61 | expect(setRejected).not.toBeCalled(); 62 | }); 63 | 64 | it('should work non-blocking', async () => { 65 | const list = [ 66 | {key: faker.string.alpha(20), value: faker.string.sample()}, 67 | {key: faker.string.alpha(20), value: faker.string.sample()}, 68 | {key: faker.string.alpha(20), value: faker.string.sample()}, 69 | ]; 70 | 71 | const setPromise = new Promise(_resolve => { 72 | // Do nothing, this will be a never resolved promise 73 | }); 74 | 75 | const cache = createCache({stores: [keyv], nonBlocking: true}); 76 | const setHandler = vi.spyOn(keyv, 'set').mockReturnValue(setPromise); 77 | const setResolved = vi.fn(); 78 | const setRejected = vi.fn(); 79 | cache.mset(list).catch(setRejected).then(setResolved); 80 | 81 | expect(setHandler).toBeCalledTimes(list.length); 82 | 83 | await sleep(1); 84 | 85 | expect(setResolved).toBeCalled(); 86 | expect(setRejected).not.toBeCalled(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /packages/cache-manager/test/multiple-stores.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, vi, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {createCache} from '../src/index.js'; 7 | import {sleep} from './sleep.js'; 8 | 9 | describe('multiple stores', () => { 10 | let keyv1: Keyv; 11 | let keyv2: Keyv; 12 | let cache: ReturnType; 13 | let ttl = 500; 14 | const data = {key: '', value: ''}; 15 | 16 | beforeEach(async () => { 17 | data.key = faker.string.alpha(20); 18 | data.value = faker.string.sample(); 19 | ttl = faker.number.int({min: 500, max: 1000}); 20 | keyv1 = new Keyv(); 21 | keyv2 = new Keyv(); 22 | cache = createCache({stores: [keyv1, keyv2]}); 23 | }); 24 | 25 | it('set', async () => { 26 | await cache.set(data.key, data.value, ttl); 27 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value); 28 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value); 29 | await expect(cache.get(data.key)).resolves.toEqual(data.value); 30 | }); 31 | 32 | it('get - 1 store error', async () => { 33 | await cache.set(data.key, data.value, ttl); 34 | 35 | keyv1.get = () => { 36 | throw new Error('store 1 get error'); 37 | }; 38 | 39 | await expect(cache.get(data.key)).resolves.toEqual(data.value); 40 | }); 41 | 42 | it('get - 2 stores error', async () => { 43 | await cache.set(data.key, data.value, ttl); 44 | 45 | const getError = () => { 46 | throw new Error('store 1 get error'); 47 | }; 48 | 49 | keyv1.get = getError; 50 | keyv2.get = getError; 51 | 52 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 53 | }); 54 | 55 | it('del', async () => { 56 | await cache.set(data.key, data.value, ttl); 57 | 58 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value); 59 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value); 60 | 61 | await cache.del(data.key); 62 | 63 | await expect(keyv1.get(data.key)).resolves.toBeUndefined(); 64 | await expect(keyv2.get(data.key)).resolves.toBeUndefined(); 65 | }); 66 | 67 | it('wrap', async () => { 68 | await cache.wrap(data.key, () => data.value, ttl); 69 | 70 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value); 71 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value); 72 | 73 | // Store 1 get error 74 | keyv1.get = () => { 75 | throw new Error('store 1 get error'); 76 | }; 77 | 78 | // eslint-disable-next-line @typescript-eslint/no-empty-function 79 | const listener = vi.fn(() => {}); 80 | cache.on('set', listener); 81 | 82 | await expect(cache.wrap(data.key, () => data.value, ttl)).resolves.toEqual(data.value); 83 | await vi.waitUntil(() => listener.mock.calls.length > 0); 84 | expect(listener).toBeCalledWith({key: data.key, value: data.value}); 85 | }); 86 | 87 | it('wrap - refresh', async () => { 88 | const refreshThreshold = ttl / 2; 89 | await cache.wrap(data.key, () => data.value, ttl, refreshThreshold); 90 | 91 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value); 92 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value); 93 | 94 | // Store 1 get error 95 | const getOk = keyv1.get; 96 | const getError = () => { 97 | throw new Error('store 1 get error'); 98 | }; 99 | 100 | keyv1.get = getError; 101 | 102 | await sleep(ttl - refreshThreshold + 100); 103 | 104 | await expect(cache.wrap(data.key, () => 'new', ttl, refreshThreshold)).resolves.toEqual(data.value); 105 | 106 | keyv1.get = getOk; 107 | // Store 1 has been updated the latest value 108 | await expect(keyv1.get(data.key)).resolves.toEqual('new'); 109 | await expect(cache.wrap(data.key, () => 'latest', ttl)).resolves.toEqual('new'); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /packages/cache-manager/test/set.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {createCache} from '../src/index.js'; 7 | import {sleep} from './sleep.js'; 8 | 9 | describe('set', () => { 10 | let keyv: Keyv; 11 | let cache: ReturnType; 12 | let ttl = 500; 13 | const data = {key: '', value: ''}; 14 | 15 | beforeEach(async () => { 16 | data.key = faker.string.alpha(20); 17 | data.value = faker.string.sample(); 18 | ttl = faker.number.int({min: 500, max: 1000}); 19 | keyv = new Keyv(); 20 | cache = createCache({stores: [keyv]}); 21 | }); 22 | 23 | it('basic', async () => { 24 | await expect(cache.set(data.key, data.value)).resolves.toEqual(data.value); 25 | await expect(cache.set(data.key, data.value, ttl)).resolves.toEqual(data.value); 26 | await expect(cache.get(data.key)).resolves.toEqual(data.value); 27 | }); 28 | 29 | it('error', async () => { 30 | const error = new Error('set error'); 31 | keyv.set = () => { 32 | throw error; 33 | }; 34 | 35 | await expect(cache.set(data.key, data.value)).rejects.toThrowError(error); 36 | await expect(cache.get(data.key)).resolves.toBeUndefined(); 37 | }); 38 | it('set should be non-blocking', async () => { 39 | const secondKeyv = new Keyv(); 40 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true}); 41 | await cache.set(data.key, data.value); 42 | await sleep(300); 43 | await expect(cache.get(data.key)).resolves.toEqual(data.value); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/cache-manager/test/sleep.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable promise/param-names, no-promise-executor-return */ 2 | export const sleep = async (ms: number) => new Promise(r => setTimeout(r, ms)); 3 | -------------------------------------------------------------------------------- /packages/cache-manager/test/stores.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import {createKeyv} from '@keyv/redis'; 3 | import { 4 | describe, expect, it, 5 | } from 'vitest'; 6 | import {simpleFaker} from '@faker-js/faker'; 7 | import {createCache} from '../src/index.js'; 8 | 9 | describe('stores', () => { 10 | it('can get the keyv store', () => { 11 | const cache = createCache(); 12 | expect(cache.stores.length).toEqual(1); 13 | }); 14 | 15 | it('can see multiple stores', () => { 16 | const keyv = new Keyv(); 17 | const redis = createKeyv(); 18 | const cache = createCache({stores: [keyv, redis]}); 19 | expect(cache.stores.length).toEqual(2); 20 | expect(cache.stores[0]).toEqual(keyv); 21 | expect(cache.stores[1]).toEqual(redis); 22 | }); 23 | 24 | it('can get the keyv store and do iterator', async () => { 25 | const cache = createCache(); 26 | expect(cache.stores.length).toEqual(1); 27 | const keyName = simpleFaker.string.uuid(); 28 | const keyValue = simpleFaker.string.uuid(); 29 | await cache.set(keyName, keyValue); 30 | const keyv = cache.stores[0]; 31 | expect(keyv).toBeInstanceOf(Keyv); 32 | 33 | let returnValue; 34 | 35 | if (keyv?.iterator) { 36 | for await (const [key, value] of keyv.iterator({})) { 37 | if (key === keyName) { 38 | returnValue = value; 39 | } 40 | } 41 | } 42 | 43 | expect(returnValue).toEqual(keyValue); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/cache-manager/test/ttl.test.ts: -------------------------------------------------------------------------------- 1 | import {Keyv} from 'keyv'; 2 | import { 3 | beforeEach, describe, expect, it, 4 | } from 'vitest'; 5 | import {faker} from '@faker-js/faker'; 6 | import {createCache} from '../src/index.js'; 7 | import {sleep} from './sleep.js'; 8 | 9 | describe('get', () => { 10 | let keyv: Keyv; 11 | let cache: ReturnType; 12 | let ttl = 500; 13 | const data = {key: '', value: ''}; 14 | 15 | beforeEach(async () => { 16 | data.key = faker.string.alpha(20); 17 | data.value = faker.string.sample(); 18 | ttl = faker.number.int({min: 500, max: 1000}); 19 | keyv = new Keyv(); 20 | cache = createCache({stores: [keyv]}); 21 | }); 22 | 23 | it('basic', async () => { 24 | await cache.set(data.key, data.value); 25 | await expect(cache.ttl(data.key)).resolves.toBeUndefined(); 26 | }); 27 | 28 | it('expired', async () => { 29 | await cache.set(data.key, data.value, ttl); 30 | await sleep(ttl + 100); 31 | await expect(cache.ttl(data.key)).resolves.toBeUndefined(); 32 | }); 33 | 34 | it('error', async () => { 35 | await cache.set(data.key, data.value); 36 | keyv.get = () => { 37 | throw new Error('get error'); 38 | }; 39 | 40 | await expect(cache.ttl(data.key)).resolves.toBeUndefined(); 41 | }); 42 | it('error on non-blocking enabled', async () => { 43 | const secondKeyv = new Keyv(); 44 | keyv.get = () => { 45 | throw new Error('get error'); 46 | }; 47 | 48 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true}); 49 | await cache.set(data.key, data.value); 50 | await expect(cache.ttl(data.key)).resolves.toBeUndefined(); 51 | }); 52 | it('gets the expiration of a key', async () => { 53 | await cache.set(data.key, data.value, ttl); 54 | const expiration = Date.now() + ttl; 55 | await expect(cache.ttl(data.key)).resolves.toBeGreaterThanOrEqual(expiration - 100); 56 | }); 57 | 58 | it('gets the expiration of a key with nonBlocking', async () => { 59 | const secondKeyv = new Keyv(); 60 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true}); 61 | await cache.set(data.key, data.value, ttl); 62 | const expiration = Date.now() + ttl; 63 | await expect(cache.ttl(data.key)).resolves.toBeGreaterThanOrEqual(expiration - 100); 64 | }); 65 | 66 | it('gets null of a key with nonBlocking', async () => { 67 | const secondKeyv = new Keyv(); 68 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true}); 69 | await expect(cache.ttl('non-block-bad-key1')).resolves.toBeUndefined(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/cache-manager/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 7 | 8 | /* Emit */ 9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 12 | 13 | /* Interop Constraints */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | 17 | /* Type Checking */ 18 | "strict": true, /* Enable all strict type-checking options. */ 19 | 20 | /* Completeness */ 21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 22 | "lib": [ 23 | "ESNext", "DOM" 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/cache-manager/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ['json', 'text'], 7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/cacheable-request/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/cacheable-request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheable-request", 3 | "version": "13.0.7", 4 | "description": "Wrap native HTTP requests with RFC compliant cache support", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/jaredwray/cacheable.git", 9 | "directory": "packages/cacheable-request" 10 | }, 11 | "author": "Jared Wray (http://jaredwray.com)", 12 | "type": "module", 13 | "exports": "./dist/index.js", 14 | "types": "./dist/index.d.ts", 15 | "engines": { 16 | "node": ">=18" 17 | }, 18 | "scripts": { 19 | "test": "xo --fix && vitest run --coverage", 20 | "test:ci": "xo && vitest run", 21 | "prepublish": "pnpm run build", 22 | "build": "rimraf ./dist && tsc --project tsconfig.build.json", 23 | "clean": "rimraf node_modules ./coverage ./test/testdb.sqlite ./dist" 24 | }, 25 | "files": [ 26 | "dist", 27 | "LICENSE" 28 | ], 29 | "keywords": [ 30 | "HTTP", 31 | "HTTPS", 32 | "cache", 33 | "caching", 34 | "layer", 35 | "cacheable", 36 | "RFC 7234", 37 | "RFC", 38 | "7234", 39 | "compliant" 40 | ], 41 | "dependenciesComments": { 42 | "@types/http-cache-semantics": "It needs to be in the dependencies list and not devDependencies because otherwise projects that use this one will be getting `Could not find a declaration file for module 'http-cache-semantics'` error when running `tsc`, see https://github.com/jaredwray/cacheable-request/issues/194 for details" 43 | }, 44 | "dependencies": { 45 | "@types/http-cache-semantics": "^4.0.4", 46 | "get-stream": "^9.0.1", 47 | "http-cache-semantics": "^4.2.0", 48 | "keyv": "^5.3.3", 49 | "mimic-response": "^4.0.0", 50 | "normalize-url": "^8.0.1", 51 | "responselike": "^3.0.0" 52 | }, 53 | "devDependencies": { 54 | "@keyv/sqlite": "^4.0.4", 55 | "@types/node": "^22.15.30", 56 | "@types/responselike": "^1.0.3", 57 | "@vitest/coverage-v8": "^3.2.2", 58 | "body-parser": "^2.2.0", 59 | "delay": "^6.0.0", 60 | "express": "^4.21.2", 61 | "pify": "^6.1.0", 62 | "rimraf": "^6.0.1", 63 | "sqlite3": "^5.1.7", 64 | "tsup": "^8.5.0", 65 | "typescript": "^5.8.3", 66 | "vitest": "^3.2.2", 67 | "xo": "^1.1.0" 68 | }, 69 | "xo": { 70 | "rules": { 71 | "@typescript-eslint/triple-slash-reference": 0, 72 | "@typescript-eslint/no-namespace": 0, 73 | "@typescript-eslint/no-unsafe-assignment": 0, 74 | "@typescript-eslint/no-unsafe-call": 0, 75 | "@typescript-eslint/ban-types": 0, 76 | "@typescript-eslint/restrict-template-expressions": 0, 77 | "@typescript-eslint/no-unsafe-return": 0, 78 | "@typescript-eslint/no-unsafe-argument": 0, 79 | "new-cap": 0, 80 | "unicorn/no-abusive-eslint-disable": 0, 81 | "@typescript-eslint/restrict-plus-operands": 0, 82 | "@typescript-eslint/no-implicit-any-catch": 0, 83 | "@typescript-eslint/consistent-type-imports": 0, 84 | "@typescript-eslint/consistent-type-definitions": 0, 85 | "@typescript-eslint/prefer-nullish-coalescing": 0, 86 | "n/prefer-global/url": 0, 87 | "n/no-deprecated-api": 0, 88 | "unicorn/prefer-event-target": 0, 89 | "@typescript-eslint/no-unnecessary-type-assertion": 0, 90 | "promise/prefer-await-to-then": 0, 91 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": 0 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/cacheable-request/src/types.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for cacheable-request 6.0 2 | // Project: https://github.com/lukechilds/cacheable-request#readme 3 | // Definitions by: BendingBender 4 | // Paul Melnikow 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | // TypeScript Version: 2.3 7 | 8 | /// 9 | 10 | import { 11 | request, RequestOptions, ClientRequest, ServerResponse, 12 | } from 'node:http'; 13 | import {URL} from 'node:url'; 14 | import {EventEmitter} from 'node:events'; 15 | import {Buffer} from 'node:buffer'; 16 | import ResponseLike from 'responselike'; 17 | import {CachePolicyObject} from 'http-cache-semantics'; 18 | 19 | export type RequestFn = typeof request; 20 | export type RequestFunction = typeof request; 21 | export type CacheResponse = ServerResponse | typeof ResponseLike; 22 | 23 | export type CacheableRequestFunction = ( 24 | options: CacheableOptions, 25 | callback?: (response: CacheResponse) => void 26 | ) => Emitter; 27 | 28 | export type CacheableOptions = Options & RequestOptions | string | URL; 29 | 30 | export interface Options { 31 | /** 32 | * If the cache should be used. Setting this to `false` will completely bypass the cache for the current request. 33 | * @default true 34 | */ 35 | cache?: boolean | undefined; 36 | 37 | /** 38 | * If set to `true` once a cached resource has expired it is deleted and will have to be re-requested. 39 | * 40 | * If set to `false`, after a cached resource's TTL expires it is kept in the cache and will be revalidated 41 | * on the next request with `If-None-Match`/`If-Modified-Since` headers. 42 | * @default false 43 | */ 44 | strictTtl?: boolean | undefined; 45 | 46 | /** 47 | * Limits TTL. The `number` represents milliseconds. 48 | * @default undefined 49 | */ 50 | maxTtl?: number | undefined; 51 | 52 | /** 53 | * When set to `true`, if the DB connection fails we will automatically fallback to a network request. 54 | * DB errors will still be emitted to notify you of the problem even though the request callback may succeed. 55 | * @default false 56 | */ 57 | automaticFailover?: boolean | undefined; 58 | 59 | /** 60 | * Forces refreshing the cache. If the response could be retrieved from the cache, it will perform a 61 | * new request and override the cache instead. 62 | * @default false 63 | */ 64 | forceRefresh?: boolean | undefined; 65 | remoteAddress?: boolean | undefined; 66 | 67 | url?: string | undefined; 68 | 69 | headers?: Record; 70 | 71 | // eslint-disable-next-line @typescript-eslint/no-restricted-types 72 | body?: Buffer; 73 | } 74 | 75 | export interface CacheValue extends Record { 76 | url: string; 77 | statusCode: number; 78 | // eslint-disable-next-line @typescript-eslint/no-restricted-types 79 | body: Buffer | string; 80 | cachePolicy: CachePolicyObject; 81 | } 82 | 83 | export interface Emitter extends EventEmitter { 84 | addListener(event: 'request', listener: (request: ClientRequest) => void): this; 85 | addListener( 86 | event: 'response', 87 | listener: (response: CacheResponse) => void 88 | ): this; 89 | addListener(event: 'error', listener: (error: RequestError | CacheError) => void): this; 90 | on(event: 'request', listener: (request: ClientRequest) => void): this; 91 | on(event: 'response', listener: (response: CacheResponse) => void): this; 92 | on(event: 'error', listener: (error: RequestError | CacheError) => void): this; 93 | once(event: 'request', listener: (request: ClientRequest) => void): this; 94 | once(event: 'response', listener: (response: CacheResponse) => void): this; 95 | once(event: 'error', listener: (error: RequestError | CacheError) => void): this; 96 | prependListener(event: 'request', listener: (request: ClientRequest) => void): this; 97 | prependListener( 98 | event: 'response', 99 | listener: (response: CacheResponse) => void 100 | ): this; 101 | prependListener(event: 'error', listener: (error: RequestError | CacheError) => void): this; 102 | prependOnceListener(event: 'request', listener: (request: ClientRequest) => void): this; 103 | prependOnceListener( 104 | event: 'response', 105 | listener: (response: CacheResponse) => void 106 | ): this; 107 | prependOnceListener( 108 | event: 'error', 109 | listener: (error: RequestError | CacheError) => void 110 | ): this; 111 | removeListener(event: 'request', listener: (request: ClientRequest) => void): this; 112 | removeListener( 113 | event: 'response', 114 | listener: (response: CacheResponse) => void 115 | ): this; 116 | removeListener(event: 'error', listener: (error: RequestError | CacheError) => void): this; 117 | off(event: 'request', listener: (request: ClientRequest) => void): this; 118 | off(event: 'response', listener: (response: CacheResponse) => void): this; 119 | off(event: 'error', listener: (error: RequestError | CacheError) => void): this; 120 | removeAllListeners(event?: 'request' | 'response' | 'error'): this; 121 | listeners(event: 'request'): Array<(request: ClientRequest) => void>; 122 | listeners(event: 'response'): Array<(response: CacheResponse) => void>; 123 | listeners(event: 'error'): Array<(error: RequestError | CacheError) => void>; 124 | rawListeners(event: 'request'): Array<(request: ClientRequest) => void>; 125 | rawListeners(event: 'response'): Array<(response: CacheResponse) => void>; 126 | rawListeners(event: 'error'): Array<(error: RequestError | CacheError) => void>; 127 | emit(event: 'request', request: ClientRequest): boolean; 128 | emit(event: 'response', response: CacheResponse): boolean; 129 | emit(event: 'error', error: RequestError | CacheError): boolean; 130 | eventNames(): Array<'request' | 'response' | 'error'>; 131 | listenerCount(type: 'request' | 'response' | 'error'): number; 132 | } 133 | 134 | export class RequestError extends Error { 135 | constructor(error: Error) { 136 | super(error.message); 137 | Object.defineProperties(this, Object.getOwnPropertyDescriptors(error)); 138 | } 139 | } 140 | 141 | export class CacheError extends Error { 142 | constructor(error: Error) { 143 | super(error.message); 144 | Object.defineProperties(this, Object.getOwnPropertyDescriptors(error)); 145 | } 146 | } 147 | 148 | export interface UrlOption { 149 | path: string; 150 | pathname?: string; 151 | search?: string; 152 | } 153 | -------------------------------------------------------------------------------- /packages/cacheable-request/test/cacheable-request-class.test.ts: -------------------------------------------------------------------------------- 1 | import {request} from 'node:http'; 2 | import {test, expect} from 'vitest'; 3 | import {Keyv} from 'keyv'; 4 | import CacheableRequest from '../src/index.js'; 5 | 6 | test('CacheableRequest is a function', () => { 7 | expect(typeof CacheableRequest).toBe('function'); 8 | }); 9 | test('CacheableRequest accepts Keyv instance', () => { 10 | expect(() => new CacheableRequest(request, new Keyv())).not.toThrow(); 11 | }); 12 | 13 | test('CacheableRequest should accept hook', () => { 14 | const cacheableRequest = new CacheableRequest(request); 15 | cacheableRequest.addHook('response', (response: any) => response); 16 | expect(cacheableRequest.getHook('response')).not.toBeUndefined(); 17 | expect(cacheableRequest.getHook('not')).toBeUndefined(); 18 | }); 19 | 20 | test('CacheableRequest should remove hook', () => { 21 | const cacheableRequest = new CacheableRequest(request); 22 | cacheableRequest.addHook('response', (response: any) => response); 23 | expect(cacheableRequest.getHook('response')).not.toBeUndefined(); 24 | cacheableRequest.removeHook('response'); 25 | expect(cacheableRequest.getHook('response')).toBeUndefined(); 26 | }); 27 | 28 | test('CacheableRequest should run hook', async () => { 29 | const cacheableRequest = new CacheableRequest(request); 30 | cacheableRequest.addHook('response', (response: any) => response); 31 | expect(cacheableRequest.getHook('response')).not.toBeUndefined(); 32 | const value = await cacheableRequest.runHook('response', 10); 33 | expect(value).toBe(10); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/cacheable-request/test/create-test-server/index.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | import http from 'node:http'; 5 | import express from 'express'; 6 | import pify from 'pify'; 7 | import bodyParser from 'body-parser'; 8 | 9 | const createTestServer = (opts = {}) => { 10 | const server = express(); 11 | server.http = http.createServer(server); 12 | 13 | server.set('etag', false); 14 | 15 | if (opts.bodyParser !== false) { 16 | server.use(bodyParser.json(Object.assign({ limit: '1mb', type: 'application/json' }, opts.bodyParser))); 17 | server.use(bodyParser.text(Object.assign({ limit: '1mb', type: 'text/plain' }, opts.bodyParser))); 18 | server.use(bodyParser.urlencoded(Object.assign({ limit: '1mb', type: 'application/x-www-form-urlencoded', extended: true }, opts.bodyParser))); 19 | server.use(bodyParser.raw(Object.assign({ limit: '1mb', type: 'application/octet-stream' }, opts.bodyParser))); 20 | } 21 | 22 | const send = fn => (req, res, next) => { 23 | const cb = typeof fn === 'function' ? fn(req, res, next) : fn; 24 | 25 | Promise.resolve(cb).then(val => { 26 | if (val) { 27 | /* c8 ignore next 3 */ 28 | res.send(val); 29 | } 30 | }); 31 | }; 32 | 33 | const get = server.get.bind(server); 34 | server.get = function () { 35 | const [path, ...handlers] = [...arguments]; 36 | 37 | for (const handler of handlers) { 38 | get(path, send(handler)); 39 | } 40 | }; 41 | 42 | server.listen = () => Promise.all([ 43 | pify(server.http.listen.bind(server.http))().then(() => { 44 | server.port = server.http.address().port; 45 | server.url = `http://localhost:${server.port}`; 46 | }) 47 | ]); 48 | 49 | server.close = () => Promise.all([ 50 | pify(server.http.close.bind(server.http))().then(() => { 51 | server.port = undefined; 52 | server.url = undefined; 53 | }) 54 | ]); 55 | 56 | return server.listen().then(() => server); 57 | }; 58 | 59 | export default createTestServer; -------------------------------------------------------------------------------- /packages/cacheable-request/test/dependencies.test.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync} from 'node:fs'; 2 | import {test, expect} from 'vitest'; 3 | 4 | test('@types/http-cache-semantics is a regular (not dev) dependency', () => { 5 | // Required to avoid `Could not find a declaration file for module 'http-cache-semantics'` error from `tsc` when using this package in other projects 6 | 7 | // Arrange 8 | const packageJsonContents = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); 9 | 10 | // Assert 11 | expect(packageJsonContents).toHaveProperty('dependencies.@types/http-cache-semantics'); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/cacheable-request/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/cacheable-request/vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | exclude: [ 7 | 'site/docula.config.cjs', 8 | 'site-output/**', 9 | '.pnp.*', 10 | '.yarn/**', 11 | 'test/**', 12 | 'vitest.config.mjs', 13 | 'dist/**', 14 | ], 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/cacheable/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/cacheable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheable", 3 | "version": "1.10.0", 4 | "description": "High Performance Layer 1 / Layer 2 Caching with Keyv Storage", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jaredwray/cacheable.git", 18 | "directory": "packages/cacheable" 19 | }, 20 | "author": "Jared Wray ", 21 | "license": "MIT", 22 | "private": false, 23 | "scripts": { 24 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean", 25 | "prepublish": "pnpm build", 26 | "test": "xo --fix && vitest run --coverage", 27 | "test:ci": "xo && vitest run", 28 | "clean": "rimraf ./dist ./coverage ./node_modules" 29 | }, 30 | "devDependencies": { 31 | "@faker-js/faker": "^9.7.0", 32 | "@keyv/redis": "^4.4.0", 33 | "@types/node": "^22.15.3", 34 | "@vitest/coverage-v8": "^3.1.3", 35 | "lru-cache": "^11.1.0", 36 | "rimraf": "^6.0.1", 37 | "tsup": "^8.4.0", 38 | "typescript": "^5.8.3", 39 | "vitest": "^3.1.3", 40 | "xo": "^0.60.0" 41 | }, 42 | "dependencies": { 43 | "hookified": "^1.8.2", 44 | "keyv": "^5.3.3" 45 | }, 46 | "keywords": [ 47 | "cacheable", 48 | "high performance", 49 | "layer 1 caching", 50 | "layer 2 caching", 51 | "distributed caching", 52 | "Keyv storage engine", 53 | "memory caching", 54 | "LRU cache", 55 | "expiration", 56 | "CacheableMemory", 57 | "offline support", 58 | "distributed sync", 59 | "secondary store", 60 | "primary store", 61 | "non-blocking operations", 62 | "cache statistics", 63 | "layered caching", 64 | "fault tolerant", 65 | "scalable cache", 66 | "in-memory cache", 67 | "distributed cache", 68 | "lruSize", 69 | "lru", 70 | "multi-tier cache" 71 | ], 72 | "files": [ 73 | "dist", 74 | "LICENSE" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /packages/cacheable/src/cacheable-item-types.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * CacheableItem 4 | * @typedef {Object} CacheableItem 5 | * @property {string} key - The key of the cacheable item 6 | * @property {any} value - The value of the cacheable item 7 | * @property {number|string} [ttl] - Time to Live - If you set a number it is miliseconds, if you set a string it is a human-readable 8 | * format such as `1s` for 1 second or `1h` for 1 hour. Setting undefined means that it will use the default time-to-live. If both are 9 | * undefined then it will not have a time-to-live. 10 | */ 11 | export type CacheableItem = { 12 | key: string; 13 | value: any; 14 | ttl?: number | string; 15 | }; 16 | 17 | export type CacheableStoreItem = { 18 | key: string; 19 | value: any; 20 | expires?: number; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cacheable/src/coalesce-async.ts: -------------------------------------------------------------------------------- 1 | type PromiseCallback = { 2 | resolve: (value: T | PromiseLike) => void; 3 | reject: (reason: E) => void; 4 | }; 5 | 6 | const callbacks = new Map(); 7 | 8 | function hasKey(key: string): boolean { 9 | return callbacks.has(key); 10 | } 11 | 12 | function addKey(key: string): void { 13 | callbacks.set(key, []); 14 | } 15 | 16 | function removeKey(key: string): void { 17 | callbacks.delete(key); 18 | } 19 | 20 | function addCallbackToKey(key: string, callback: PromiseCallback): void { 21 | const stash = getCallbacksByKey(key); 22 | stash.push(callback); 23 | callbacks.set(key, stash); 24 | } 25 | 26 | function getCallbacksByKey(key: string): Array> { 27 | /* c8 ignore next 1 */ 28 | return callbacks.get(key) ?? []; 29 | } 30 | 31 | async function enqueue(key: string): Promise { 32 | return new Promise((resolve, reject) => { 33 | const callback: PromiseCallback = {resolve, reject}; 34 | addCallbackToKey(key, callback); 35 | }); 36 | } 37 | 38 | function dequeue(key: string): Array> { 39 | const stash = getCallbacksByKey(key); 40 | removeKey(key); 41 | return stash; 42 | } 43 | 44 | function coalesce(options: {key: string; error?: Error; result?: T}): void { 45 | const {key, error, result} = options; 46 | 47 | for (const callback of dequeue(key)) { 48 | /* c8 ignore next 1 */ 49 | if (error) { 50 | /* c8 ignore next 3 */ 51 | callback.reject(error); 52 | } else { 53 | callback.resolve(result); 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * Enqueue a promise for the group identified by `key`. 60 | * 61 | * All requests received for the same key while a request for that key 62 | * is already being executed will wait. Once the running request settles 63 | * then all the waiting requests in the group will settle, too. 64 | * This minimizes how many times the function itself runs at the same time. 65 | * This function resolves or rejects according to the given function argument. 66 | * 67 | * @url https://github.com/douglascayers/promise-coalesce 68 | */ 69 | export async function coalesceAsync( 70 | /** 71 | * Any identifier to group requests together. 72 | */ 73 | key: string, 74 | /** 75 | * The function to run. 76 | */ 77 | fnc: () => T | PromiseLike, 78 | ): Promise { 79 | if (!hasKey(key)) { 80 | addKey(key); 81 | try { 82 | const result = await Promise.resolve(fnc()); 83 | coalesce({key, result}); 84 | return result; 85 | /* c8 ignore next 1 */ 86 | } catch (error: any) { 87 | /* c8 ignore next 5 */ 88 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 89 | coalesce({key, error}); 90 | // eslint-disable-next-line @typescript-eslint/only-throw-error 91 | throw error; 92 | } 93 | } 94 | 95 | return enqueue(key); 96 | } 97 | -------------------------------------------------------------------------------- /packages/cacheable/src/hash.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'node:crypto'; 2 | 3 | /** 4 | * Hashes an object using the specified algorithm. The default algorithm is 'sha256'. 5 | * @param object The object to hash 6 | * @param algorithm The hash algorithm to use 7 | * @returns {string} The hash of the object 8 | */ 9 | export function hash(object: any, algorithm = 'sha256'): string { 10 | // Convert the object to a string 11 | const objectString = JSON.stringify(object); 12 | 13 | // Check if the algorithm is supported 14 | if (!crypto.getHashes().includes(algorithm)) { 15 | throw new Error(`Unsupported hash algorithm: '${algorithm}'`); 16 | } 17 | 18 | const hasher = crypto.createHash(algorithm); 19 | hasher.update(objectString); 20 | return hasher.digest('hex'); 21 | } 22 | 23 | export function hashToNumber(object: any, min = 0, max = 10, algorithm = 'sha256'): number { 24 | // Convert the object to a string 25 | const objectString = JSON.stringify(object); 26 | 27 | // Check if the algorithm is supported 28 | if (!crypto.getHashes().includes(algorithm)) { 29 | throw new Error(`Unsupported hash algorithm: '${algorithm}'`); 30 | } 31 | 32 | // Create a hasher and update it with the object string 33 | const hasher = crypto.createHash(algorithm); 34 | hasher.update(objectString); 35 | 36 | // Get the hash as a hexadecimal string 37 | const hashHex = hasher.digest('hex'); 38 | 39 | // Convert the hex string to a number (base 16) 40 | const hashNumber = Number.parseInt(hashHex, 16); 41 | 42 | // Calculate the range size 43 | const range = max - min + 1; 44 | 45 | // Return a number within the specified range 46 | return min + (hashNumber % range); 47 | } 48 | 49 | export function djb2Hash(string_: string, min = 0, max = 10): number { 50 | // DJB2 hash algorithm 51 | let hash = 5381; 52 | for (let i = 0; i < string_.length; i++) { 53 | // eslint-disable-next-line no-bitwise, unicorn/prefer-code-point 54 | hash = (hash * 33) ^ string_.charCodeAt(i); // 33 is a prime multiplier 55 | } 56 | 57 | // Calculate the range size 58 | const range = max - min + 1; 59 | 60 | // Return a value within the specified range 61 | return min + (Math.abs(hash) % range); 62 | } 63 | -------------------------------------------------------------------------------- /packages/cacheable/src/keyv-memory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Keyv, type KeyvOptions, type KeyvStoreAdapter, type StoredData, 3 | } from 'keyv'; 4 | import {CacheableMemory, type CacheableMemoryOptions} from './memory.js'; 5 | 6 | export type KeyvCacheableMemoryOptions = CacheableMemoryOptions & { 7 | namespace?: string; 8 | }; 9 | 10 | export class KeyvCacheableMemory implements KeyvStoreAdapter { 11 | opts: CacheableMemoryOptions = { 12 | ttl: 0, 13 | useClone: true, 14 | lruSize: 0, 15 | checkInterval: 0, 16 | }; 17 | 18 | private readonly _defaultCache = new CacheableMemory(); 19 | private readonly _nCache = new Map(); 20 | private _namespace?: string; 21 | 22 | constructor(options?: KeyvCacheableMemoryOptions) { 23 | if (options) { 24 | this.opts = options; 25 | this._defaultCache = new CacheableMemory(options); 26 | 27 | if (options.namespace) { 28 | this._namespace = options.namespace; 29 | this._nCache.set(this._namespace, new CacheableMemory(options)); 30 | } 31 | } 32 | } 33 | 34 | get namespace(): string | undefined { 35 | return this._namespace; 36 | } 37 | 38 | set namespace(value: string | undefined) { 39 | this._namespace = value; 40 | } 41 | 42 | public get store(): CacheableMemory { 43 | return this.getStore(this._namespace); 44 | } 45 | 46 | async get(key: string): Promise | undefined> { 47 | const result = this.getStore(this._namespace).get(key); 48 | if (result) { 49 | return result; 50 | } 51 | 52 | return undefined; 53 | } 54 | 55 | async getMany(keys: string[]): Promise>> { 56 | const result = this.getStore(this._namespace).getMany(keys); 57 | 58 | return result; 59 | } 60 | 61 | async set(key: string, value: any, ttl?: number): Promise { 62 | this.getStore(this._namespace).set(key, value, ttl); 63 | } 64 | 65 | async setMany(values: Array<{key: string; value: any; ttl?: number}>): Promise { 66 | this.getStore(this._namespace).setMany(values); 67 | } 68 | 69 | async delete(key: string): Promise { 70 | this.getStore(this._namespace).delete(key); 71 | return true; 72 | } 73 | 74 | async deleteMany?(key: string[]): Promise { 75 | this.getStore(this._namespace).deleteMany(key); 76 | return true; 77 | } 78 | 79 | async clear(): Promise { 80 | this.getStore(this._namespace).clear(); 81 | } 82 | 83 | async has?(key: string): Promise { 84 | return this.getStore(this._namespace).has(key); 85 | } 86 | 87 | on(event: string, listener: (...arguments_: any[]) => void): this { 88 | this.getStore(this._namespace).on(event, listener); 89 | return this; 90 | } 91 | 92 | public getStore(namespace?: string): CacheableMemory { 93 | if (!namespace) { 94 | return this._defaultCache; 95 | } 96 | 97 | if (!this._nCache.has(namespace)) { 98 | this._nCache.set(namespace, new CacheableMemory(this.opts)); 99 | } 100 | 101 | return this._nCache.get(namespace)!; 102 | } 103 | } 104 | 105 | /** 106 | * Creates a new Keyv instance with a new KeyvCacheableMemory store. This also removes the serialize/deserialize methods from the Keyv instance for optimization. 107 | * @param options 108 | * @returns 109 | */ 110 | export function createKeyv(options?: KeyvCacheableMemoryOptions): Keyv { 111 | const store = new KeyvCacheableMemory(options); 112 | const namespace = options?.namespace; 113 | 114 | let ttl; 115 | if (options?.ttl && Number.isInteger(options.ttl)) { 116 | ttl = options?.ttl as number; 117 | } 118 | 119 | const keyv = new Keyv({store, namespace, ttl}); 120 | // Remove seriazlize/deserialize 121 | keyv.serialize = undefined; 122 | keyv.deserialize = undefined; 123 | return keyv; 124 | } 125 | -------------------------------------------------------------------------------- /packages/cacheable/src/memory-lru.ts: -------------------------------------------------------------------------------- 1 | export class ListNode { 2 | // eslint-disable-next-line @typescript-eslint/parameter-properties 3 | value: T; 4 | prev: ListNode | undefined = undefined; 5 | next: ListNode | undefined = undefined; 6 | 7 | constructor(value: T) { 8 | this.value = value; 9 | } 10 | } 11 | 12 | export class DoublyLinkedList { 13 | private head: ListNode | undefined = undefined; 14 | private tail: ListNode | undefined = undefined; 15 | private readonly nodesMap = new Map>(); 16 | 17 | // Add a new node to the front (most recently used) 18 | addToFront(value: T): void { 19 | const newNode = new ListNode(value); 20 | 21 | if (this.head) { 22 | newNode.next = this.head; 23 | this.head.prev = newNode; 24 | this.head = newNode; 25 | } else { 26 | // eslint-disable-next-line no-multi-assign 27 | this.head = this.tail = newNode; 28 | } 29 | 30 | // Store the node reference in the map 31 | this.nodesMap.set(value, newNode); 32 | } 33 | 34 | // Move an existing node to the front (most recently used) 35 | moveToFront(value: T): void { 36 | const node = this.nodesMap.get(value); 37 | if (!node || this.head === node) { 38 | return; 39 | } // Node doesn't exist or is already at the front 40 | 41 | // Remove the node from its current position 42 | if (node.prev) { 43 | node.prev.next = node.next; 44 | } 45 | 46 | /* c8 ignore next 3 */ 47 | if (node.next) { 48 | node.next.prev = node.prev; 49 | } 50 | 51 | // Update tail if necessary 52 | if (node === this.tail) { 53 | this.tail = node.prev; 54 | } 55 | 56 | // Move node to the front 57 | node.prev = undefined; 58 | node.next = this.head; 59 | if (this.head) { 60 | this.head.prev = node; 61 | } 62 | 63 | this.head = node; 64 | 65 | // If list was empty, update tail 66 | this.tail ||= node; 67 | } 68 | 69 | // Get the oldest node (tail) 70 | getOldest(): T | undefined { 71 | return this.tail ? this.tail.value : undefined; 72 | } 73 | 74 | // Remove the oldest node (tail) 75 | removeOldest(): T | undefined { 76 | /* c8 ignore next 3 */ 77 | if (!this.tail) { 78 | return undefined; 79 | } 80 | 81 | const oldValue = this.tail.value; 82 | 83 | if (this.tail.prev) { 84 | this.tail = this.tail.prev; 85 | this.tail.next = undefined; 86 | /* c8 ignore next 4 */ 87 | } else { 88 | // eslint-disable-next-line no-multi-assign 89 | this.head = this.tail = undefined; 90 | } 91 | 92 | // Remove the node from the map 93 | this.nodesMap.delete(oldValue); 94 | return oldValue; 95 | } 96 | 97 | get size(): number { 98 | return this.nodesMap.size; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/cacheable/src/shorthand-time.ts: -------------------------------------------------------------------------------- 1 | 2 | export const shorthandToMilliseconds = (shorthand?: string | number): number | undefined => { 3 | let milliseconds: number; 4 | 5 | if (shorthand === undefined) { 6 | return undefined; 7 | } 8 | 9 | if (typeof shorthand === 'number') { 10 | milliseconds = shorthand; 11 | } else if (typeof shorthand === 'string') { 12 | shorthand = shorthand.trim(); 13 | 14 | // Check if the string is purely numeric 15 | if (Number.isNaN(Number(shorthand))) { 16 | // Use a case-insensitive regex that supports decimals and 'ms' unit 17 | const match = /^([\d.]+)\s*(ms|s|m|h|hr|d)$/i.exec(shorthand); 18 | 19 | if (!match) { 20 | throw new Error( 21 | `Unsupported time format: "${shorthand}". Use 'ms', 's', 'm', 'h', 'hr', or 'd'.`, 22 | ); 23 | } 24 | 25 | const [, value, unit] = match; 26 | const numericValue = Number.parseFloat(value); 27 | const unitLower = unit.toLowerCase(); 28 | 29 | switch (unitLower) { 30 | case 'ms': { 31 | milliseconds = numericValue; 32 | break; 33 | } 34 | 35 | case 's': { 36 | milliseconds = numericValue * 1000; 37 | break; 38 | } 39 | 40 | case 'm': { 41 | milliseconds = numericValue * 1000 * 60; 42 | break; 43 | } 44 | 45 | case 'h': { 46 | milliseconds = numericValue * 1000 * 60 * 60; 47 | break; 48 | } 49 | 50 | case 'hr': { 51 | milliseconds = numericValue * 1000 * 60 * 60; 52 | break; 53 | } 54 | 55 | case 'd': { 56 | milliseconds = numericValue * 1000 * 60 * 60 * 24; 57 | break; 58 | } 59 | 60 | /* c8 ignore next 3 */ 61 | default: { 62 | milliseconds = Number(shorthand); 63 | } 64 | } 65 | /* c8 ignore next 6 */ 66 | } else { 67 | milliseconds = Number(shorthand); 68 | } 69 | } else { 70 | throw new TypeError('Time must be a string or a number.'); 71 | } 72 | 73 | return milliseconds; 74 | }; 75 | 76 | export const shorthandToTime = (shorthand?: string | number, fromDate?: Date): number => { 77 | fromDate ||= new Date(); 78 | 79 | const milliseconds = shorthandToMilliseconds(shorthand); 80 | if (milliseconds === undefined) { 81 | return fromDate.getTime(); 82 | } 83 | 84 | return fromDate.getTime() + milliseconds; 85 | }; 86 | 87 | -------------------------------------------------------------------------------- /packages/cacheable/src/stats.ts: -------------------------------------------------------------------------------- 1 | 2 | export type CacheableOptions = { 3 | enabled?: boolean; 4 | }; 5 | 6 | export class CacheableStats { 7 | private _hits = 0; 8 | private _misses = 0; 9 | private _gets = 0; 10 | private _sets = 0; 11 | private _deletes = 0; 12 | private _clears = 0; 13 | private _vsize = 0; 14 | private _ksize = 0; 15 | private _count = 0; 16 | private _enabled = false; 17 | 18 | constructor(options?: CacheableOptions) { 19 | if (options?.enabled) { 20 | this._enabled = options.enabled; 21 | } 22 | } 23 | 24 | /** 25 | * @returns {boolean} - Whether the stats are enabled 26 | */ 27 | public get enabled(): boolean { 28 | return this._enabled; 29 | } 30 | 31 | /** 32 | * @param {boolean} enabled - Whether to enable the stats 33 | */ 34 | public set enabled(enabled: boolean) { 35 | this._enabled = enabled; 36 | } 37 | 38 | /** 39 | * @returns {number} - The number of hits 40 | * @readonly 41 | */ 42 | public get hits(): number { 43 | return this._hits; 44 | } 45 | 46 | /** 47 | * @returns {number} - The number of misses 48 | * @readonly 49 | */ 50 | public get misses(): number { 51 | return this._misses; 52 | } 53 | 54 | /** 55 | * @returns {number} - The number of gets 56 | * @readonly 57 | */ 58 | public get gets(): number { 59 | return this._gets; 60 | } 61 | 62 | /** 63 | * @returns {number} - The number of sets 64 | * @readonly 65 | */ 66 | public get sets(): number { 67 | return this._sets; 68 | } 69 | 70 | /** 71 | * @returns {number} - The number of deletes 72 | * @readonly 73 | */ 74 | public get deletes(): number { 75 | return this._deletes; 76 | } 77 | 78 | /** 79 | * @returns {number} - The number of clears 80 | * @readonly 81 | */ 82 | public get clears(): number { 83 | return this._clears; 84 | } 85 | 86 | /** 87 | * @returns {number} - The vsize (value size) of the cache instance 88 | * @readonly 89 | */ 90 | public get vsize(): number { 91 | return this._vsize; 92 | } 93 | 94 | /** 95 | * @returns {number} - The ksize (key size) of the cache instance 96 | * @readonly 97 | */ 98 | public get ksize(): number { 99 | return this._ksize; 100 | } 101 | 102 | /** 103 | * @returns {number} - The count of the cache instance 104 | * @readonly 105 | */ 106 | public get count(): number { 107 | return this._count; 108 | } 109 | 110 | public incrementHits(): void { 111 | if (!this._enabled) { 112 | return; 113 | } 114 | 115 | this._hits++; 116 | } 117 | 118 | public incrementMisses(): void { 119 | if (!this._enabled) { 120 | return; 121 | } 122 | 123 | this._misses++; 124 | } 125 | 126 | public incrementGets(): void { 127 | if (!this._enabled) { 128 | return; 129 | } 130 | 131 | this._gets++; 132 | } 133 | 134 | public incrementSets(): void { 135 | if (!this._enabled) { 136 | return; 137 | } 138 | 139 | this._sets++; 140 | } 141 | 142 | public incrementDeletes(): void { 143 | if (!this._enabled) { 144 | return; 145 | } 146 | 147 | this._deletes++; 148 | } 149 | 150 | public incrementClears(): void { 151 | if (!this._enabled) { 152 | return; 153 | } 154 | 155 | this._clears++; 156 | } 157 | 158 | // eslint-disable-next-line @typescript-eslint/naming-convention 159 | public incrementVSize(value: any): void { 160 | if (!this._enabled) { 161 | return; 162 | } 163 | 164 | this._vsize += this.roughSizeOfObject(value); 165 | } 166 | 167 | // eslint-disable-next-line @typescript-eslint/naming-convention 168 | public decreaseVSize(value: any): void { 169 | if (!this._enabled) { 170 | return; 171 | } 172 | 173 | this._vsize -= this.roughSizeOfObject(value); 174 | } 175 | 176 | // eslint-disable-next-line @typescript-eslint/naming-convention 177 | public incrementKSize(key: string): void { 178 | if (!this._enabled) { 179 | return; 180 | } 181 | 182 | this._ksize += this.roughSizeOfString(key); 183 | } 184 | 185 | // eslint-disable-next-line @typescript-eslint/naming-convention 186 | public decreaseKSize(key: string): void { 187 | if (!this._enabled) { 188 | return; 189 | } 190 | 191 | this._ksize -= this.roughSizeOfString(key); 192 | } 193 | 194 | public incrementCount(): void { 195 | if (!this._enabled) { 196 | return; 197 | } 198 | 199 | this._count++; 200 | } 201 | 202 | public decreaseCount(): void { 203 | if (!this._enabled) { 204 | return; 205 | } 206 | 207 | this._count--; 208 | } 209 | 210 | public setCount(count: number): void { 211 | if (!this._enabled) { 212 | return; 213 | } 214 | 215 | this._count = count; 216 | } 217 | 218 | public roughSizeOfString(value: string): number { 219 | // Keys are strings (UTF-16) 220 | return value.length * 2; 221 | } 222 | 223 | public roughSizeOfObject(object: any): number { 224 | const objectList: any[] = []; 225 | const stack: any[] = [object]; 226 | let bytes = 0; 227 | 228 | while (stack.length > 0) { 229 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 230 | const value = stack.pop(); 231 | 232 | if (typeof value === 'boolean') { 233 | bytes += 4; // Booleans are 4 bytes 234 | } else if (typeof value === 'string') { 235 | bytes += value.length * 2; // Each character is 2 bytes (UTF-16 encoding) 236 | } else if (typeof value === 'number') { 237 | bytes += 8; // Numbers are 8 bytes (IEEE 754 format) 238 | } else if (typeof value === 'object' && value !== null && !objectList.includes(value)) { 239 | objectList.push(value); 240 | 241 | // Estimate object overhead, and then recursively estimate the size of properties 242 | // eslint-disable-next-line guard-for-in 243 | for (const key in value) { 244 | bytes += key.length * 2; // Keys are strings (UTF-16) 245 | stack.push(value[key]); // Add values to the stack to compute their size 246 | } 247 | } 248 | } 249 | 250 | return bytes; 251 | } 252 | 253 | public reset(): void { 254 | this._hits = 0; 255 | this._misses = 0; 256 | this._gets = 0; 257 | this._sets = 0; 258 | this._deletes = 0; 259 | this._clears = 0; 260 | this._vsize = 0; 261 | this._ksize = 0; 262 | this._count = 0; 263 | } 264 | 265 | public resetStoreValues(): void { 266 | this._vsize = 0; 267 | this._ksize = 0; 268 | this._count = 0; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /packages/cacheable/src/ttl.ts: -------------------------------------------------------------------------------- 1 | import {shorthandToMilliseconds} from '../src/shorthand-time.js'; 2 | 3 | /** 4 | * Converts a exspires value to a TTL value. 5 | * @param expires - The expires value to convert. 6 | * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid. 7 | */ 8 | export function getTtlFromExpires(expires: number | undefined): number | undefined { 9 | if (expires === undefined || expires === null) { 10 | return undefined; 11 | } 12 | 13 | const now = Date.now(); 14 | if (expires < now) { 15 | return undefined; 16 | } 17 | 18 | return expires - now; 19 | } 20 | 21 | /** 22 | * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values. 23 | * @param cacheableTtl - The cacheableTtl value to use. 24 | * @param primaryTtl - The primaryTtl value to use. 25 | * @param secondaryTtl - The secondaryTtl value to use. 26 | * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined. 27 | */ 28 | export function getCascadingTtl(cacheableTtl?: number | string, primaryTtl?: number, secondaryTtl?: number): number | undefined { 29 | return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl); 30 | } 31 | 32 | /** 33 | * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the 34 | * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used. 35 | * @param ttl 36 | * @param expires 37 | * @returns 38 | */ 39 | export function calculateTtlFromExpiration(ttl: number | undefined, expires: number | undefined): number | undefined { 40 | const ttlFromExpires = getTtlFromExpires(expires); 41 | const expiresFromTtl = ttl ? Date.now() + ttl : undefined; 42 | if (ttlFromExpires === undefined) { 43 | return ttl; 44 | } 45 | 46 | if (expiresFromTtl === undefined) { 47 | return ttlFromExpires; 48 | } 49 | 50 | if (expires! > expiresFromTtl) { 51 | return ttl; 52 | } 53 | 54 | return ttlFromExpires; 55 | } 56 | -------------------------------------------------------------------------------- /packages/cacheable/src/wrap.ts: -------------------------------------------------------------------------------- 1 | import {hash} from './hash.js'; 2 | import {coalesceAsync} from './coalesce-async.js'; 3 | import {type Cacheable, type CacheableMemory} from './index.js'; 4 | 5 | export type GetOrSetFunctionOptions = { 6 | ttl?: number | string; 7 | cacheErrors?: boolean; 8 | }; 9 | 10 | export type GetOrSetOptions = GetOrSetFunctionOptions & { 11 | cacheId?: string; 12 | cache: Cacheable; 13 | }; 14 | 15 | export type WrapFunctionOptions = { 16 | ttl?: number | string; 17 | keyPrefix?: string; 18 | cacheErrors?: boolean; 19 | cacheId?: string; 20 | }; 21 | 22 | export type WrapOptions = WrapFunctionOptions & { 23 | cache: Cacheable; 24 | }; 25 | 26 | export type WrapSyncOptions = WrapFunctionOptions & { 27 | cache: CacheableMemory; 28 | }; 29 | 30 | export type AnyFunction = (...arguments_: any[]) => any; 31 | 32 | export function wrapSync(function_: AnyFunction, options: WrapSyncOptions): AnyFunction { 33 | const {ttl, keyPrefix, cache} = options; 34 | 35 | return function (...arguments_: any[]) { 36 | const cacheKey = createWrapKey(function_, arguments_, keyPrefix); 37 | let value = cache.get(cacheKey); 38 | 39 | if (value === undefined) { 40 | try { 41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 42 | value = function_(...arguments_); 43 | cache.set(cacheKey, value, ttl); 44 | } catch (error) { 45 | cache.emit('error', error); 46 | if (options.cacheErrors) { 47 | cache.set(cacheKey, error, ttl); 48 | } 49 | } 50 | } 51 | 52 | return value as T; 53 | }; 54 | } 55 | 56 | export async function getOrSet(key: string, function_: () => Promise, options: GetOrSetOptions): Promise { 57 | let value = await options.cache.get(key) as T | undefined; 58 | if (value === undefined) { 59 | const cacheId = options.cacheId ?? 'default'; 60 | const coalesceKey = `${cacheId}::${key}`; 61 | value = await coalesceAsync(coalesceKey, async () => { 62 | try { 63 | const result = await function_() as T; 64 | await options.cache.set(key, result, options.ttl); 65 | return result; 66 | } catch (error) { 67 | options.cache.emit('error', error); 68 | if (options.cacheErrors) { 69 | await options.cache.set(key, error, options.ttl); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | return value; 76 | } 77 | 78 | export function wrap(function_: AnyFunction, options: WrapOptions): AnyFunction { 79 | const {keyPrefix, cache} = options; 80 | 81 | return async function (...arguments_: any[]) { 82 | const cacheKey = createWrapKey(function_, arguments_, keyPrefix); 83 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return 84 | return cache.getOrSet(cacheKey, async (): Promise => function_(...arguments_), options); 85 | }; 86 | } 87 | 88 | export function createWrapKey(function_: AnyFunction, arguments_: any[], keyPrefix?: string): string { 89 | if (!keyPrefix) { 90 | return `${function_.name}::${hash(arguments_)}`; 91 | } 92 | 93 | return `${keyPrefix}::${function_.name}::${hash(arguments_)}`; 94 | } 95 | -------------------------------------------------------------------------------- /packages/cacheable/test/hash.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect} from 'vitest'; 2 | import {hash, hashToNumber} from '../src/hash.js'; 3 | 4 | describe('hash', () => { 5 | test('hashes an object using the specified algorithm', () => { 6 | // Arrange 7 | const object = {foo: 'bar'}; 8 | const algorithm = 'sha256'; 9 | 10 | // Act 11 | const result = hash(object, algorithm); 12 | 13 | // Assert 14 | expect(result).toBe('7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b'); 15 | }); 16 | test('hashes a string using the default algorithm', () => { 17 | // Arrange 18 | const object = 'foo'; 19 | 20 | // Act 21 | const result = hash(object); 22 | 23 | // Assert 24 | expect(result).toBe('b2213295d564916f89a6a42455567c87c3f480fcd7a1c15e220f17d7169a790b'); 25 | }); 26 | 27 | test('hashes a number using the default algorithm', () => { 28 | // Arrange 29 | const object = '123'; 30 | 31 | // Act 32 | const result = hashToNumber(object); 33 | 34 | // Assert 35 | expect(result).toBeDefined(); 36 | }); 37 | 38 | test('throws an error when the algorithm is not supported', () => { 39 | // Arrange 40 | const object = {foo: 'bar'}; 41 | const algorithm = 'md5foo'; 42 | 43 | // Act & Assert 44 | expect(() => hashToNumber(object, 0, 100, algorithm)).toThrowError('Unsupported hash algorithm: \'md5foo\''); 45 | }); 46 | 47 | test('throws an error when the algorithm is not supported', () => { 48 | expect(() => hash('foo', 'md5foo')).toThrowError('Unsupported hash algorithm: \'md5foo\''); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/cacheable/test/keyv-memory.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect} from 'vitest'; 2 | import {Keyv} from 'keyv'; 3 | import {KeyvCacheableMemory, createKeyv} from '../src/keyv-memory.js'; 4 | 5 | describe('Keyv Cacheable Memory', () => { 6 | test('should initialize keyv cacheable memory', async () => { 7 | const keyvCacheableMemory = new KeyvCacheableMemory(); 8 | expect(keyvCacheableMemory).toBeDefined(); 9 | const keyv = new Keyv({store: keyvCacheableMemory}); 10 | expect(keyv).toBeDefined(); 11 | }); 12 | test('should set namespace for keyv cacheable memory', async () => { 13 | const namespace = 'ns1'; 14 | const keyvCacheableMemory = new KeyvCacheableMemory({namespace}); 15 | expect(keyvCacheableMemory.namespace).toBe(namespace); 16 | keyvCacheableMemory.namespace = 'ns2'; 17 | expect(keyvCacheableMemory.namespace).toBe('ns2'); 18 | }); 19 | test('should set options for keyv cacheable memory', async () => { 20 | const keyvCacheableMemory = new KeyvCacheableMemory({ttl: 1000, lruSize: 1000}); 21 | expect(keyvCacheableMemory).toBeDefined(); 22 | const keyv = new Keyv({store: keyvCacheableMemory}); 23 | expect(keyv).toBeDefined(); 24 | }); 25 | test('should get undefined from keyv cacheable memory', async () => { 26 | const keyvCacheableMemory = new KeyvCacheableMemory(); 27 | const keyv = new Keyv({store: keyvCacheableMemory}); 28 | const value = await keyv.get('key') as string | undefined; 29 | expect(value).toBe(undefined); 30 | }); 31 | test('should set and get value from keyv cacheable memory', async () => { 32 | const keyvCacheableMemory = new KeyvCacheableMemory(); 33 | const keyv = new Keyv({store: keyvCacheableMemory}); 34 | await keyv.set('key', 'value'); 35 | const value = await keyv.get('key'); 36 | expect(value).toBe('value'); 37 | }); 38 | test('should delete value from keyv cacheable memory', async () => { 39 | const keyvCacheableMemory = new KeyvCacheableMemory(); 40 | const keyv = new Keyv({store: keyvCacheableMemory}); 41 | await keyv.set('key', 'value'); 42 | await keyv.delete('key'); 43 | const value = await keyv.get('key'); 44 | expect(value).toBe(undefined); 45 | }); 46 | test('should clear keyv cacheable memory', async () => { 47 | const keyvCacheableMemory = new KeyvCacheableMemory(); 48 | const keyv = new Keyv({store: keyvCacheableMemory}); 49 | await keyv.set('key', 'value'); 50 | await keyv.clear(); 51 | const value = await keyv.get('key'); 52 | expect(value).toBe(undefined); 53 | }); 54 | test('should check if key exists in keyv cacheable memory', async () => { 55 | const keyvCacheableMemory = new KeyvCacheableMemory(); 56 | const keyv = new Keyv({store: keyvCacheableMemory}); 57 | await keyv.set('key', 'value'); 58 | const exists = await keyv.has('key'); 59 | expect(exists).toBe(true); 60 | }); 61 | test('should get many values from keyv cacheable memory', async () => { 62 | const keyvCacheableMemory = new KeyvCacheableMemory(); 63 | const keyv = new Keyv({store: keyvCacheableMemory}); 64 | await keyv.set('key', 'value'); 65 | const values = await keyv.get(['key']); 66 | expect(values).toEqual(['value']); 67 | }); 68 | test('should delete many values from keyv cacheable memory', async () => { 69 | const keyvCacheableMemory = new KeyvCacheableMemory(); 70 | const keyv = new Keyv({store: keyvCacheableMemory}); 71 | await keyv.set('key', 'value'); 72 | await keyv.delete(['key']); 73 | const value = await keyv.get('key'); 74 | expect(value).toBe(undefined); 75 | }); 76 | test('should set many values in keyv cacheable memory', async () => { 77 | const keyvCacheableMemory = new KeyvCacheableMemory(); 78 | await keyvCacheableMemory.setMany([{key: 'key', value: 'value'}, {key: 'key1', value: 'value1'}]); 79 | const value = await keyvCacheableMemory.get('key1'); 80 | expect(value).toBe('value1'); 81 | }); 82 | test('should be able to get the store based on namespace', async () => { 83 | const cache = new KeyvCacheableMemory(); 84 | await cache.set('key1', 'default'); 85 | expect(await cache.get('key1')).toBe('default'); 86 | cache.namespace = 'ns1'; 87 | expect(await cache.get('key1')).toBe(undefined); 88 | expect(cache.store.get('key1')).toBe(undefined); 89 | await cache.set('key1', 'ns1'); 90 | expect(await cache.get('key1')).toBe('ns1'); 91 | expect(cache.store.get('key1')).toBe('ns1'); 92 | cache.namespace = undefined; 93 | expect(await cache.get('key1')).toBe('default'); 94 | expect(cache.store.get('key1')).toBe('default'); 95 | }); 96 | 97 | test('should be able to createKeyv with cacheable memory store', async () => { 98 | const keyv = createKeyv({ttl: 1000, lruSize: 1000}); 99 | expect(keyv).toBeDefined(); 100 | expect(keyv.store).toBeInstanceOf(KeyvCacheableMemory); 101 | expect(keyv.store.opts.ttl).toBe(1000); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/cacheable/test/secondary-primary.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest'; 2 | import {Keyv} from 'keyv'; 3 | import {faker} from '@faker-js/faker'; 4 | import {Cacheable, CacheableHooks} from '../src/index.js'; 5 | import {getTtlFromExpires} from '../src/ttl.js'; 6 | import {sleep} from './sleep.js'; 7 | 8 | test('should set a new ttl when secondary is setting primary', async () => { 9 | const secondary = new Keyv({ttl: 100}); 10 | const cacheable = new Cacheable({secondary}); 11 | const data = { 12 | key: faker.string.uuid(), 13 | value: faker.string.uuid(), 14 | }; 15 | 16 | cacheable.onHook(CacheableHooks.BEFORE_SECONDARY_SETS_PRIMARY, async item => { 17 | item.ttl = 10; 18 | }); 19 | 20 | await cacheable.set(data.key, data.value); 21 | const result = await cacheable.get(data.key); 22 | expect(result).toEqual(data.value); 23 | 24 | // Remove the item from primary 25 | await cacheable.primary.delete(data.key); 26 | const primaryResult1 = await cacheable.primary.get(data.key, {raw: true}); 27 | expect(primaryResult1).toEqual(undefined); 28 | 29 | // Update the item from secondary 30 | await cacheable.get(data.key); 31 | const primaryResult2 = await cacheable.primary.get(data.key, {raw: true}); 32 | expect(primaryResult2?.value).toEqual(data.value); 33 | const ttlFromExpires = getTtlFromExpires(primaryResult2?.expires as number | undefined); 34 | expect(ttlFromExpires).toBeLessThan(12); 35 | 36 | // Now make sure that it expires after 10 seconds 37 | await sleep(20); 38 | const primaryResult3 = await cacheable.primary.get(data.key, {raw: true}); 39 | expect(primaryResult3).toEqual(undefined); 40 | 41 | // Verify that the secondary is still there 42 | const secondaryResult = await cacheable.secondary?.get(data.key, {raw: true}); 43 | expect(secondaryResult?.value).toEqual(data.value); 44 | }); 45 | 46 | test('should use the cacheable default ttl on secondary -> primary', async () => { 47 | const data = { 48 | key: faker.string.uuid(), 49 | value: faker.string.uuid(), 50 | }; 51 | 52 | const secondary = new Keyv(); 53 | const cacheable = new Cacheable({secondary, ttl: 100}); 54 | 55 | // Set the value on secondary with no ttl 56 | await cacheable.secondary?.set(data.key, data.value); 57 | 58 | const result = await cacheable.get(data.key); 59 | expect(result).toEqual(data.value); 60 | 61 | // Get the value from primary raw to validate it has expires 62 | const primaryResult = await cacheable.primary.get(data.key, {raw: true}); 63 | expect(primaryResult?.value).toEqual(data.value); 64 | const ttlFromExpires = getTtlFromExpires(primaryResult?.expires as number | undefined); 65 | expect(ttlFromExpires).toBeGreaterThan(95); 66 | expect(ttlFromExpires).toBeLessThan(105); 67 | }); 68 | 69 | test('should use the primary ttl on secondary -> primary', async () => { 70 | const data = { 71 | key: faker.string.uuid(), 72 | value: faker.string.uuid(), 73 | }; 74 | 75 | const secondary = new Keyv(); 76 | const primary = new Keyv({ttl: 50}); 77 | const cacheable = new Cacheable({secondary, primary, ttl: 100}); 78 | 79 | // Set the value on secondary with no ttl 80 | await cacheable.secondary?.set(data.key, data.value); 81 | 82 | const result = await cacheable.get(data.key); 83 | expect(result).toEqual(data.value); 84 | 85 | // Get the value from primary raw to validate it has expires 86 | const primaryResult = await cacheable.primary.get(data.key, {raw: true}); 87 | expect(primaryResult?.value).toEqual(data.value); 88 | const ttlFromExpires = getTtlFromExpires(primaryResult?.expires as number | undefined); 89 | expect(ttlFromExpires).toBeGreaterThan(45); 90 | expect(ttlFromExpires).toBeLessThan(55); 91 | }); 92 | 93 | test('should use the secondary ttl on secondary -> primary', async () => { 94 | const data = { 95 | key: faker.string.uuid(), 96 | value: faker.string.uuid(), 97 | }; 98 | 99 | const secondary = new Keyv({ttl: 50}); 100 | const primary = new Keyv(); 101 | const cacheable = new Cacheable({secondary, primary, ttl: 100}); 102 | 103 | // Set the value on secondary with no ttl 104 | await cacheable.secondary?.set(data.key, data.value); 105 | 106 | const result = await cacheable.get(data.key); 107 | expect(result).toEqual(data.value); 108 | 109 | // Get the value from primary raw to validate it has expires 110 | const primaryResult = await cacheable.primary.get(data.key, {raw: true}); 111 | expect(primaryResult?.value).toEqual(data.value); 112 | const ttlFromExpires = getTtlFromExpires(primaryResult?.expires as number | undefined); 113 | expect(ttlFromExpires).toBeGreaterThan(45); 114 | expect(ttlFromExpires).toBeLessThan(55); 115 | }); 116 | -------------------------------------------------------------------------------- /packages/cacheable/test/shared-secondary.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | test, expect, 3 | } from 'vitest'; 4 | import {Keyv} from 'keyv'; 5 | import {Cacheable} from '../src/index.js'; 6 | import {sleep} from './sleep.js'; 7 | 8 | /* 9 | Should get a value from the secondary store and respect its ttl when setting the value in the primary store (item specific ttl) 10 | */ 11 | test('should get a value from the secondary store and respect its ttl', async () => { 12 | const instance1Primary = new Keyv(); 13 | const instance2Primary = new Keyv(); 14 | 15 | const sharedSecondary = new Keyv(); 16 | 17 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary}); 18 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary}); 19 | 20 | // Set the value in the first instance 21 | await instance1.set('key', 'value', 50); 22 | 23 | await sleep(25); 24 | 25 | // Get the value in the second instance 26 | const result = await instance2.get('key'); 27 | expect(result).toEqual('value'); 28 | 29 | // Wait for the value to expire 30 | await sleep(100); 31 | 32 | // Get the value in the second instance (it should be expired) 33 | const result2 = await instance2.get('key'); 34 | expect(result2, 'result should have expired').toBeUndefined(); 35 | }); 36 | 37 | /* 38 | Should get a value from the secondary store and respect its zero-ttl when setting the value in the primary store (item specific zero-ttl) 39 | */ 40 | test('secondar store as a zero ttl set', async () => { 41 | const instance1Primary = new Keyv(); 42 | const instance2Primary = new Keyv(); 43 | 44 | const sharedSecondary = new Keyv(); 45 | 46 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 50}); 47 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 50}); 48 | 49 | // Set the value in the first instance 50 | await instance1.set('key', 'value', 0); 51 | 52 | await sleep(25); 53 | 54 | // Get the value in the second instance 55 | const result = await instance2.get('key'); 56 | expect(result).toEqual('value'); 57 | 58 | // Wait past the time of the default TTL of 500ms 59 | await sleep(75); 60 | 61 | // Get the value in the second instance (it should be valid) 62 | const result2 = await instance2.get('key'); 63 | expect(result2).toEqual('value'); 64 | }); 65 | 66 | /* 67 | Should get a value from the secondary store and respect its ttl when setting the value in the primary store (default ttl when setting) 68 | */ 69 | test('default ttl when setting', async () => { 70 | const instance1Primary = new Keyv(); 71 | const instance2Primary = new Keyv(); 72 | 73 | const sharedSecondary = new Keyv(); 74 | 75 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 50}); 76 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary}); 77 | 78 | // Set the value in the first instance 79 | await instance1.set('key', 'value'); 80 | 81 | await sleep(25); 82 | 83 | // Get the value in the second instance 84 | const result = await instance2.get('key'); 85 | expect(result).toEqual('value'); 86 | 87 | // Wait for the value to expire 88 | await sleep(75); 89 | 90 | // Get the value in the second instance (it should be expired) 91 | const result2 = await instance2.get('key'); 92 | expect(result2, 'result should have expired').toBeUndefined(); 93 | }); 94 | 95 | /* 96 | Should get a value from the secondary store and respect its zero-ttl when setting the value in the primary store (default zero-ttl when setting) 97 | */ 98 | test('should get a value from the secondary store and respect its zero-ttl', async () => { 99 | const instance1Primary = new Keyv(); 100 | const instance2Primary = new Keyv(); 101 | 102 | const sharedSecondary = new Keyv(); 103 | 104 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 0}); 105 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 100}); 106 | 107 | // Set the value in the first instance 108 | await instance1.set('key', 'value'); 109 | 110 | await sleep(50); 111 | 112 | // Get the value in the second instance 113 | const result = await instance2.get('key'); 114 | expect(result).toEqual('value'); 115 | 116 | // Wait past instance2's default TTL of 500ms 117 | await sleep(125); 118 | 119 | // Get the value in the second instance (it should be valid) 120 | const result2 = await instance2.get('key'); 121 | expect(result2).toEqual('value'); 122 | }); 123 | 124 | /* 125 | Should get a value from the secondary store and respect its ttl when setting the value in the primary store (default ttl when setting in the first instance, despite alternative ttl when getting in the second instance) 126 | */ 127 | test('default ttl when setting in the first instance, despite alternative ttl', async () => { 128 | const instance1Primary = new Keyv(); 129 | const instance2Primary = new Keyv(); 130 | 131 | const sharedSecondary = new Keyv(); 132 | 133 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 50}); 134 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 100}); 135 | 136 | // Set the value in the first instance 137 | await instance1.set('key', 'value'); 138 | 139 | await sleep(25); 140 | 141 | // Get the value in the second instance 142 | const result = await instance2.get('key'); 143 | expect(result).toEqual('value'); 144 | 145 | // Wait for the value to expire 146 | await sleep(100); 147 | 148 | // Get the value in the second instance (it should be expired) 149 | const result2 = await instance2.get('key'); 150 | expect(result2, 'result should have expired').toBeUndefined(); 151 | }); 152 | 153 | /* 154 | Should get a value from the secondary store and respect its zero-ttl when setting the value in the primary store 155 | (default zero-ttl when setting in the first instance, despite alternative ttl when getting in the second instance) 156 | */ 157 | test('default zero-ttl when setting in the first instance, despite alternative ttl', async () => { 158 | const instance1Primary = new Keyv(); 159 | const instance2Primary = new Keyv(); 160 | 161 | const sharedSecondary = new Keyv(); 162 | 163 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 0}); 164 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 100}); 165 | 166 | // Set the value in the first instance 167 | await instance1.set('key', 'value'); 168 | 169 | await sleep(50); 170 | 171 | // Get the value in the second instance 172 | const result = await instance2.get('key'); 173 | expect(result).toEqual('value'); 174 | 175 | // Wait past instance2's default TTL of 500ms 176 | await sleep(125); 177 | 178 | // Get the value in the second instance (it should be valid) 179 | const result2 = await instance2.get('key'); 180 | expect(result2).toEqual('value'); 181 | }); 182 | 183 | /* 184 | Should not return a value from the secondary store or set it in the primary store when the value is expired in the secondary store 185 | */ 186 | test('should not set in primary store if expired', async () => { 187 | const instance1Primary = new Keyv(); 188 | const instance2Primary = new Keyv(); 189 | 190 | // A custom Keyv class designed return an expired value 191 | class CustomKeyv extends Keyv { 192 | async get(key: string | string[], options?: {raw?: boolean}): Promise { 193 | const value = await super.get(key as unknown as string, options?.raw ? {raw: true} : undefined); 194 | 195 | await sleep(100); 196 | 197 | return value; 198 | } 199 | } 200 | const sharedSecondary = new CustomKeyv(); 201 | 202 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary}); 203 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary}); 204 | 205 | // Set the value in the secondary store 206 | await instance1.set('key', 'value', 25); 207 | 208 | await sleep(50); 209 | 210 | // Get the value in the second instance 211 | const result = await instance2.get('key'); 212 | expect(result, 'result should have expired').toBeUndefined(); 213 | 214 | // Get the value in the primary store 215 | const result2 = await instance2Primary.get('key') as unknown; 216 | expect(result2, 'result should not be placed in the primary store').toBeUndefined(); 217 | }); 218 | -------------------------------------------------------------------------------- /packages/cacheable/test/shorthand-time.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect} from 'vitest'; 2 | import {shorthandToMilliseconds, shorthandToTime} from '../src/shorthand-time.js'; 3 | 4 | describe('time parser', () => { 5 | test('send in number', () => { 6 | expect(shorthandToMilliseconds(1000)).toBe(1000); 7 | }); 8 | test('send in string with milliseconds', () => { 9 | expect(shorthandToMilliseconds('1ms')).toBe(1); 10 | }); 11 | test('send in string', () => { 12 | expect(shorthandToMilliseconds('1s')).toBe(1000); 13 | }); 14 | test('send in string with spaces', () => { 15 | expect(shorthandToMilliseconds('1 s')).toBe(1000); 16 | }); 17 | test('send in string with decimal', () => { 18 | expect(shorthandToMilliseconds('1.5s')).toBe(1500); 19 | }); 20 | test('send in string with minutes', () => { 21 | expect(shorthandToMilliseconds('1m')).toBe(60_000); 22 | }); 23 | test('send in string with hours', () => { 24 | expect(shorthandToMilliseconds('1h')).toBe(3_600_000); 25 | }); 26 | test('send in string with days', () => { 27 | expect(shorthandToMilliseconds('1d')).toBe(86_400_000); 28 | }); 29 | test('send in string with unsupported unit', () => { 30 | expect(() => shorthandToMilliseconds('1z')).toThrowError('Unsupported time format: "1z". Use \'ms\', \'s\', \'m\', \'h\', \'hr\', or \'d\'.'); 31 | }); 32 | test('send in string with number', () => { 33 | expect(shorthandToMilliseconds('1000')).toBe(1000); 34 | }); 35 | test('send in string with number and decimal', () => { 36 | expect(shorthandToMilliseconds('1.5h')).toBe(5_400_000); 37 | }); 38 | test('send in string with number and decimal', () => { 39 | expect(shorthandToMilliseconds('1.5hr')).toBe(5_400_000); 40 | }); 41 | }); 42 | 43 | describe('parse to time', () => { 44 | test('send in number', () => { 45 | expect(shorthandToTime(1000)).toBeGreaterThan(Date.now()); 46 | }); 47 | 48 | test('send in string', () => { 49 | expect(shorthandToTime('10s')).toBeGreaterThan(Date.now()); 50 | }); 51 | 52 | test('send in nothing', () => { 53 | expect(shorthandToTime()).toBeDefined(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/cacheable/test/sleep.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-promise-executor-return 2 | export const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 3 | -------------------------------------------------------------------------------- /packages/cacheable/test/stats.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect} from 'vitest'; 2 | import {CacheableStats} from '../src/stats.js'; 3 | 4 | describe('cacheable stats', () => { 5 | test('should be able to instantiate', () => { 6 | const stats = new CacheableStats(); 7 | expect(stats).toBeDefined(); 8 | }); 9 | 10 | test('properties should be initialized', () => { 11 | const stats = new CacheableStats(); 12 | expect(stats.hits).toBe(0); 13 | expect(stats.misses).toBe(0); 14 | expect(stats.gets).toBe(0); 15 | expect(stats.sets).toBe(0); 16 | expect(stats.deletes).toBe(0); 17 | expect(stats.clears).toBe(0); 18 | expect(stats.vsize).toBe(0); 19 | expect(stats.ksize).toBe(0); 20 | expect(stats.count).toBe(0); 21 | }); 22 | 23 | test('should be able to enable stats', () => { 24 | const stats = new CacheableStats({enabled: true}); 25 | expect(stats.enabled).toBe(true); 26 | stats.enabled = false; 27 | expect(stats.enabled).toBe(false); 28 | }); 29 | 30 | test('should be able to increment stats', () => { 31 | const stats = new CacheableStats({enabled: true}); 32 | stats.incrementHits(); 33 | stats.incrementMisses(); 34 | stats.incrementGets(); 35 | stats.incrementSets(); 36 | stats.incrementDeletes(); 37 | stats.incrementClears(); 38 | stats.incrementVSize('foo'); 39 | stats.incrementKSize('foo'); 40 | stats.incrementCount(); 41 | expect(stats.hits).toBe(1); 42 | expect(stats.misses).toBe(1); 43 | expect(stats.gets).toBe(1); 44 | expect(stats.sets).toBe(1); 45 | expect(stats.deletes).toBe(1); 46 | expect(stats.clears).toBe(1); 47 | expect(stats.vsize).toBe(6); 48 | expect(stats.ksize).toBe(6); 49 | expect(stats.count).toBe(1); 50 | }); 51 | 52 | test('should be able to reset stats', () => { 53 | const stats = new CacheableStats({enabled: true}); 54 | stats.incrementHits(); 55 | stats.incrementMisses(); 56 | stats.incrementGets(); 57 | stats.incrementSets(); 58 | stats.incrementDeletes(); 59 | stats.incrementClears(); 60 | stats.incrementVSize('foo'); 61 | stats.incrementKSize('foo'); 62 | stats.incrementCount(); 63 | stats.reset(); 64 | expect(stats.hits).toBe(0); 65 | expect(stats.misses).toBe(0); 66 | expect(stats.gets).toBe(0); 67 | expect(stats.sets).toBe(0); 68 | expect(stats.deletes).toBe(0); 69 | expect(stats.clears).toBe(0); 70 | expect(stats.vsize).toBe(0); 71 | expect(stats.ksize).toBe(0); 72 | expect(stats.count).toBe(0); 73 | }); 74 | 75 | test('should be able to decrease certain stats', () => { 76 | const stats = new CacheableStats({enabled: true}); 77 | stats.incrementVSize('foo'); 78 | stats.incrementKSize('foo'); 79 | stats.incrementCount(); 80 | expect(stats.vsize).toBe(6); 81 | expect(stats.ksize).toBe(6); 82 | expect(stats.count).toBe(1); 83 | stats.decreaseVSize('foo'); 84 | stats.decreaseKSize('foo'); 85 | stats.decreaseCount(); 86 | expect(stats.vsize).toBe(0); 87 | expect(stats.ksize).toBe(0); 88 | expect(stats.count).toBe(0); 89 | }); 90 | 91 | test('should not keep going if stats are disabled', () => { 92 | const stats = new CacheableStats({enabled: false}); 93 | stats.incrementHits(); 94 | stats.incrementMisses(); 95 | stats.incrementGets(); 96 | stats.incrementSets(); 97 | stats.incrementDeletes(); 98 | stats.incrementClears(); 99 | stats.incrementVSize('foo'); 100 | stats.incrementKSize('foo'); 101 | stats.incrementCount(); 102 | expect(stats.hits).toBe(0); 103 | expect(stats.misses).toBe(0); 104 | expect(stats.gets).toBe(0); 105 | expect(stats.sets).toBe(0); 106 | expect(stats.deletes).toBe(0); 107 | expect(stats.clears).toBe(0); 108 | expect(stats.vsize).toBe(0); 109 | expect(stats.ksize).toBe(0); 110 | expect(stats.count).toBe(0); 111 | stats.enabled = true; 112 | stats.incrementHits(); 113 | stats.incrementMisses(); 114 | stats.incrementGets(); 115 | stats.incrementSets(); 116 | stats.incrementDeletes(); 117 | stats.incrementClears(); 118 | stats.incrementVSize('foo'); 119 | stats.incrementKSize('foo'); 120 | stats.incrementCount(); 121 | expect(stats.hits).toBe(1); 122 | expect(stats.misses).toBe(1); 123 | expect(stats.gets).toBe(1); 124 | expect(stats.sets).toBe(1); 125 | expect(stats.deletes).toBe(1); 126 | expect(stats.clears).toBe(1); 127 | expect(stats.vsize).toBe(6); 128 | expect(stats.ksize).toBe(6); 129 | expect(stats.count).toBe(1); 130 | stats.enabled = false; 131 | stats.decreaseKSize('foo'); 132 | stats.decreaseVSize('foo'); 133 | stats.decreaseCount(); 134 | expect(stats.vsize).toBe(6); 135 | expect(stats.ksize).toBe(6); 136 | expect(stats.count).toBe(1); 137 | }); 138 | test('should get the rough size of the stats object', () => { 139 | const stats = new CacheableStats(); 140 | expect(stats.roughSizeOfObject(true)).toBeGreaterThan(0); 141 | expect(stats.roughSizeOfObject('wow')).toBeGreaterThan(0); 142 | expect(stats.roughSizeOfObject(123)).toBeGreaterThan(0); 143 | expect(stats.roughSizeOfObject({foo: 'bar'})).toBeGreaterThan(0); 144 | expect(stats.roughSizeOfObject([1, 2, 3])).toBeGreaterThan(0); 145 | }); 146 | test('set the count property', () => { 147 | const stats = new CacheableStats(); 148 | stats.setCount(10); 149 | expect(stats.count).toBe(0); 150 | stats.enabled = true; 151 | stats.setCount(10); 152 | expect(stats.count).toBe(10); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /packages/cacheable/test/ttl.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from 'vitest'; 2 | import {faker} from '@faker-js/faker'; 3 | import {Cacheable} from '../src/index.js'; 4 | import {getTtlFromExpires, getCascadingTtl, calculateTtlFromExpiration} from '../src/ttl.js'; 5 | import {sleep} from './sleep.js'; 6 | 7 | test('should set a value with ttl', async () => { 8 | const data = { 9 | key: faker.string.uuid(), 10 | value: faker.string.uuid(), 11 | }; 12 | const cacheable = new Cacheable({ttl: 100}); 13 | await cacheable.set(data.key, data.value); 14 | await sleep(150); 15 | const result = await cacheable.get(data.key); 16 | expect(result).toBeUndefined(); 17 | }); 18 | 19 | test('should set a ttl on parameter', {timeout: 2000}, async () => { 20 | const cacheable = new Cacheable({ttl: 50}); 21 | await cacheable.set('key', 'value', 1000); 22 | await sleep(100); 23 | const result = await cacheable.get('key'); 24 | expect(result).toEqual('value'); 25 | }); 26 | 27 | test('should get the ttl from expires', () => { 28 | const now = Date.now(); 29 | const expires = now + 2000; 30 | const result = getTtlFromExpires(expires); 31 | expect(result).toBeGreaterThan(1995); 32 | expect(result).toBeLessThan(2005); 33 | }); 34 | 35 | test('should get undefined when expires is undefined', () => { 36 | const result = getTtlFromExpires(undefined); 37 | expect(result).toBeUndefined(); 38 | }); 39 | 40 | test('should get undefined when expires is in the past', () => { 41 | const now = Date.now(); 42 | const expires = now - 1000; 43 | const result = getTtlFromExpires(expires); 44 | expect(result).toBeUndefined(); 45 | }); 46 | 47 | test('should cascade ttl from secondary', () => { 48 | const result = getCascadingTtl(1000, undefined, 3000); 49 | expect(result).toBe(3000); 50 | }); 51 | 52 | test('should cascade ttl from primary', () => { 53 | const result = getCascadingTtl(1000, 2000); 54 | expect(result).toBe(2000); 55 | }); 56 | 57 | test('should cascade ttl from cacheable', () => { 58 | const result = getCascadingTtl(1000, undefined, undefined); 59 | expect(result).toBe(1000); 60 | }); 61 | 62 | test('should cascade ttl with shorthand on cacheable', () => { 63 | const result = getCascadingTtl('1s', undefined, undefined); 64 | expect(result).toBe(1000); 65 | }); 66 | 67 | test('should calculate and choose the ttl as it is lower', () => { 68 | const now = Date.now(); 69 | const expires = now + 3000; 70 | const ttl = 2000; 71 | const result = calculateTtlFromExpiration(ttl, expires); 72 | expect(result).toBeLessThan(2002); 73 | expect(result).toBeGreaterThan(1998); 74 | }); 75 | 76 | test('should calculate and choose the expires ttl as it is lower', () => { 77 | const now = Date.now(); 78 | const expires = now + 1000; 79 | const ttl = 2000; 80 | const result = calculateTtlFromExpiration(ttl, expires); 81 | expect(result).toBeLessThan(1002); 82 | expect(result).toBeGreaterThan(998); 83 | }); 84 | 85 | test('should calculate and choose ttl as expires is undefined', () => { 86 | const ttl = 2000; 87 | const result = calculateTtlFromExpiration(ttl, undefined); 88 | expect(result).toBeLessThan(2002); 89 | expect(result).toBeGreaterThan(1998); 90 | }); 91 | 92 | test('should calculate and choose expires as ttl is undefined', () => { 93 | const now = Date.now(); 94 | const expires = now + 1000; 95 | const result = calculateTtlFromExpiration(undefined, expires); 96 | expect(result).toBe(1000); 97 | expect(result).toBeLessThan(1002); 98 | expect(result).toBeGreaterThan(998); 99 | }); 100 | 101 | test('should calculate and choose undefined as both are undefined', () => { 102 | const result = calculateTtlFromExpiration(undefined, undefined); 103 | expect(result).toBeUndefined(); 104 | }); 105 | -------------------------------------------------------------------------------- /packages/cacheable/test/wrap.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import { 3 | describe, it, expect, vi, 4 | } from 'vitest'; 5 | import {Cacheable, CacheableMemory} from '../src/index.js'; 6 | import { 7 | wrap, createWrapKey, wrapSync, type WrapOptions, type WrapSyncOptions, 8 | } from '../src/wrap.js'; 9 | import {sleep} from './sleep.js'; 10 | 11 | describe('wrap function', () => { 12 | it('should cache asynchronous function results', async () => { 13 | const asyncFunction = async (a: number, b: number) => a + b; 14 | const cache = new Cacheable(); 15 | 16 | const options: WrapOptions = { 17 | keyPrefix: 'cacheKey', 18 | cache, 19 | }; 20 | 21 | // Wrap the async function 22 | const wrapped = wrap(asyncFunction, options); 23 | 24 | // Call the wrapped function 25 | const result = await wrapped(1, 2); 26 | 27 | // Expectations 28 | expect(result).toBe(3); 29 | const cacheKey = createWrapKey(asyncFunction, [1, 2], options.keyPrefix); 30 | const cacheResult = await cache.get(cacheKey); 31 | expect(cacheResult).toBe(3); 32 | }); 33 | 34 | it('should return cached async value with hash', async () => { 35 | // Mock cache and async function 36 | const asyncFunction = async (value: number) => Math.random() * value; 37 | const cache = new Cacheable(); 38 | 39 | const options: WrapOptions = { 40 | cache, 41 | }; 42 | 43 | // Wrap the async function 44 | const wrapped = wrap(asyncFunction, options); 45 | 46 | // Call the wrapped function 47 | const result = await wrapped(12); 48 | const result2 = await wrapped(12); 49 | // Expectations 50 | expect(result).toBe(result2); 51 | }); 52 | 53 | it('should cache synchronous function results', () => { 54 | // Mock cache and sync function 55 | const syncFunction = (value: number) => Math.random() * value; 56 | const cache = new CacheableMemory(); 57 | const options: WrapSyncOptions = { 58 | cache, 59 | }; 60 | 61 | // Wrap the sync function 62 | const wrapped = wrapSync(syncFunction, options); 63 | 64 | // Call the wrapped function 65 | const result = wrapped(1, 2); 66 | const result2 = wrapped(1, 2); 67 | 68 | // Expectations 69 | expect(result).toBe(result2); 70 | const cacheKey = createWrapKey(syncFunction, [1, 2], options.keyPrefix); 71 | const cacheResult = cache.get(cacheKey); 72 | expect(cacheResult).toBe(result); 73 | }); 74 | 75 | it('should cache synchronous function results with hash', () => { 76 | // Mock cache and sync function 77 | const syncFunction = (value: number) => Math.random() * value; 78 | const cache = new CacheableMemory(); 79 | const options: WrapSyncOptions = { 80 | keyPrefix: 'testPrefix', 81 | cache, 82 | }; 83 | 84 | // Wrap the sync function 85 | const wrapped = wrapSync(syncFunction, options); 86 | 87 | // Call the wrapped function 88 | const result = wrapped(1, 2); 89 | const result2 = wrapped(1, 2); 90 | 91 | // Expectations 92 | expect(result).toBe(result2); 93 | }); 94 | 95 | it('should cache synchronous function results with key and ttl', async () => { 96 | // Mock cache and sync function 97 | const syncFunction = (value: number) => Math.random() * value; 98 | const cache = new CacheableMemory(); 99 | const options: WrapSyncOptions = { 100 | cache, 101 | ttl: 10, 102 | keyPrefix: 'cacheKey', 103 | }; 104 | 105 | // Wrap the sync function 106 | const wrapped = wrapSync(syncFunction, options); 107 | 108 | // Call the wrapped function 109 | const result = wrapped(1, 2); 110 | const result2 = wrapped(1, 2); 111 | 112 | // Expectations 113 | expect(result).toBe(result2); 114 | await sleep(30); 115 | const cacheKey = createWrapKey(syncFunction, [1, 2], options.keyPrefix); 116 | const cacheResult = cache.get(cacheKey); 117 | expect(cacheResult).toBe(undefined); 118 | }); 119 | 120 | it('should cache synchronous function results with complex args', async () => { 121 | // Mock cache and sync function 122 | const syncFunction = (value: number, person: {first: string; last: string; meta: any}) => Math.random() * value; 123 | const cache = new CacheableMemory(); 124 | const options: WrapSyncOptions = { 125 | keyPrefix: 'cacheKey', 126 | cache, 127 | }; 128 | 129 | // Wrap the sync function 130 | const wrapped = wrapSync(syncFunction, options); 131 | 132 | // Call the wrapped function 133 | const result = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}}); 134 | const result2 = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}}); 135 | 136 | // Expectations 137 | expect(result).toBe(result2); 138 | }); 139 | 140 | it('should cache synchronous function results with complex args and shorthand ttl', async () => { 141 | // Mock cache and sync function 142 | const syncFunction = (value: number, person: {first: string; last: string; meta: any}) => Math.random() * value; 143 | const cache = new CacheableMemory(); 144 | const options: WrapSyncOptions = { 145 | cache, 146 | ttl: '100ms', 147 | keyPrefix: 'cacheKey', 148 | }; 149 | 150 | // Wrap the sync function 151 | const wrapped = wrapSync(syncFunction, options); 152 | 153 | // Call the wrapped function 154 | const result = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}}); 155 | const result2 = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}}); 156 | 157 | // Expectations 158 | expect(result).toBe(result2); 159 | await sleep(200); 160 | const cacheKey = createWrapKey(wrapSync, [1, {first: 'John', last: 'Doe', meta: {age: 30}}], options.keyPrefix); 161 | const cacheResult = cache.get(cacheKey); 162 | expect(cacheResult).toBe(undefined); 163 | }); 164 | }); 165 | 166 | describe('wrap function with stampede protection', () => { 167 | it('should only execute the wrapped function once when called concurrently with the same key', async () => { 168 | const cache = new Cacheable(); 169 | const mockFunction = vi.fn().mockResolvedValue('result'); 170 | const mockedKey = createWrapKey(mockFunction, ['arg1'], 'test'); 171 | const wrappedFunction = wrap(mockFunction, {cache, keyPrefix: 'test'}); 172 | 173 | // Call the wrapped function concurrently 174 | const [result1, result2, result3, result4] = await Promise.all([wrappedFunction('arg1'), wrappedFunction('arg1'), wrappedFunction('arg2'), wrappedFunction('arg2')]); 175 | 176 | // Verify that the wrapped function was only called two times do to arg1 and arg2 177 | expect(mockFunction).toHaveBeenCalledTimes(2); 178 | 179 | // Verify that both calls returned the same result 180 | expect(result1).toBe('result'); 181 | expect(result2).toBe('result'); 182 | expect(result3).toBe('result'); 183 | 184 | // Verify that the result was cached 185 | expect(await cache.has(mockedKey)).toBe(true); 186 | }); 187 | 188 | it('should handle error if the function fails', async () => { 189 | const cache = new Cacheable(); 190 | const mockFunction = vi.fn().mockRejectedValue(new Error('Function failed')); 191 | const mockedKey = createWrapKey(mockFunction, ['arg1'], 'test'); 192 | const wrappedFunction = wrap(mockFunction, {cache, keyPrefix: 'test'}); 193 | 194 | await wrappedFunction('arg1'); 195 | 196 | // Verify that the wrapped function was only called once 197 | expect(mockFunction).toHaveBeenCalledTimes(1); 198 | }); 199 | }); 200 | 201 | describe('wrap functions handling thrown errors', () => { 202 | it('wrapSync should emit an error by default and return undefined but not cache errors', () => { 203 | const cache = new CacheableMemory(); 204 | const options: WrapSyncOptions = { 205 | cache, 206 | ttl: '1s', 207 | keyPrefix: 'cacheKey', 208 | }; 209 | 210 | const wrapped = wrapSync(() => { 211 | throw new Error('Test error'); 212 | }, options); 213 | 214 | let errorCallCount = 0; 215 | 216 | cache.on('error', error => { 217 | expect(error.message).toBe('Test error'); 218 | errorCallCount++; 219 | }); 220 | 221 | const result = wrapped(); 222 | 223 | expect(result).toBe(undefined); 224 | expect(errorCallCount).toBe(1); 225 | const values = [...cache.items]; 226 | expect(values.length).toBe(0); 227 | }); 228 | 229 | it('wrapSync should cache the error when the property is set', () => { 230 | const cache = new CacheableMemory(); 231 | const options: WrapSyncOptions = { 232 | cache, 233 | ttl: '1s', 234 | keyPrefix: 'cacheKey', 235 | cacheErrors: true, 236 | }; 237 | 238 | const wrapped = wrapSync(() => { 239 | throw new Error('Test error'); 240 | }, options); 241 | 242 | let errorCallCount = 0; 243 | 244 | cache.on('error', error => { 245 | expect(error.message).toBe('Test error'); 246 | errorCallCount++; 247 | }); 248 | 249 | wrapped(); 250 | wrapped(); // Should be cached 251 | 252 | expect(errorCallCount).toBe(1); 253 | }); 254 | 255 | it('wrap should throw an error if the wrapped function throws an error', async () => { 256 | const cache = new Cacheable(); 257 | const error = new Error('Test error'); 258 | const options: WrapOptions = { 259 | cache, 260 | ttl: '1s', 261 | keyPrefix: 'cacheKey', 262 | }; 263 | const wrapped = wrap(() => { 264 | throw error; 265 | }, options); 266 | 267 | let errorCallCount = 0; 268 | 269 | cache.on('error', error_ => { 270 | expect(error_).toBe(error); 271 | errorCallCount++; 272 | }); 273 | 274 | expect(await wrapped()).toBe(undefined); 275 | const cacheKey = createWrapKey(() => { 276 | throw error; 277 | }, [], options.keyPrefix); 278 | const result = await cache.get(cacheKey); 279 | expect(result).toBe(undefined); 280 | expect(errorCallCount).toBe(1); 281 | }); 282 | 283 | it('wrap should cache the error when the property is set', async () => { 284 | const cache = new Cacheable(); 285 | const error = new Error('Test error'); 286 | const options: WrapOptions = { 287 | cache, 288 | ttl: '1s', 289 | keyPrefix: 'cacheKey', 290 | cacheErrors: true, 291 | }; 292 | const wrapped = wrap(() => { 293 | throw error; 294 | }, options); 295 | 296 | let errorCallCount = 0; 297 | 298 | cache.on('error', error_ => { 299 | expect(error_).toBe(error); 300 | errorCallCount++; 301 | }); 302 | 303 | await wrapped(); 304 | await wrapped(); // Should be cached 305 | 306 | expect(errorCallCount).toBe(1); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /packages/cacheable/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"] 4 | } -------------------------------------------------------------------------------- /packages/cacheable/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 7 | 8 | /* Emit */ 9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 12 | 13 | /* Interop Constraints */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | 17 | /* Type Checking */ 18 | "strict": true, /* Enable all strict type-checking options. */ 19 | 20 | /* Completeness */ 21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 22 | "lib": [ 23 | "ESNext", "DOM" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /packages/cacheable/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | slowTestThreshold: 750, 6 | coverage: { 7 | reporter: ['json', 'text'], 8 | exclude: ['test', 'src/cacheable-item-types.ts', 'vite.config.ts', 'dist', 'node_modules'], 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/file-entry-cache/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/file-entry-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-entry-cache", 3 | "version": "10.1.1", 4 | "description": "A lightweight cache for file metadata, ideal for processes that work on a specific set of files and only need to reprocess files that have changed since the last run", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jaredwray/cacheable.git", 18 | "directory": "packages/file-entry-cache" 19 | }, 20 | "author": "Jared Wray ", 21 | "license": "MIT", 22 | "private": false, 23 | "keywords": [ 24 | "file cache", 25 | "task cache files", 26 | "file cache", 27 | "key par", 28 | "key value", 29 | "cache" 30 | ], 31 | "scripts": { 32 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean", 33 | "prepublish": "pnpm build", 34 | "test": "xo --fix && vitest run --coverage", 35 | "test:ci": "xo && vitest run", 36 | "clean": "rimraf ./dist ./coverage ./node_modules" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^22.15.30", 40 | "@vitest/coverage-v8": "^3.2.2", 41 | "rimraf": "^6.0.1", 42 | "tsup": "^8.5.0", 43 | "typescript": "^5.8.3", 44 | "vitest": "^3.2.2", 45 | "xo": "^1.1.0" 46 | }, 47 | "dependencies": { 48 | "flat-cache": "workspace:^" 49 | }, 50 | "files": [ 51 | "dist", 52 | "license" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /packages/file-entry-cache/test/eslint.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import { 4 | describe, test, expect, beforeEach, afterEach, 5 | } from 'vitest'; 6 | import fileEntryCache from '../src/index.js'; 7 | 8 | describe('eslint tests scenarios', () => { 9 | const fileCacheName = 'eslint-files'; 10 | const eslintCacheName = '.eslintcache'; 11 | const eslintDirectory = 'cache'; 12 | const useCheckSum = true; 13 | beforeEach(() => { 14 | // Generate files for testing 15 | fs.mkdirSync(path.resolve(`./${fileCacheName}`)); 16 | fs.writeFileSync(path.resolve(`./${fileCacheName}/test1.txt`), 'test'); 17 | fs.writeFileSync(path.resolve(`./${fileCacheName}/test2.txt`), 'test sdfljsdlfjsdflsj'); 18 | fs.writeFileSync(path.resolve(`./${fileCacheName}/test3.txt`), 'test3'); 19 | // Src files 20 | fs.mkdirSync(path.resolve(`./${fileCacheName}/src`)); 21 | fs.writeFileSync(path.resolve(`./${fileCacheName}/src/my-file.js`), 'var foo = \'bar\';\r\n'); 22 | }); 23 | 24 | afterEach(() => { 25 | fs.rmSync(path.resolve(`./${fileCacheName}`), {recursive: true, force: true}); 26 | fs.rmSync(path.resolve(`./${eslintDirectory}`), {recursive: true, force: true}); 27 | }); 28 | test('about to do absolute paths', () => { 29 | // Make sure the cache doesnt exist before we start 30 | fs.rmSync(path.resolve(`./${eslintDirectory}`), {recursive: true, force: true}); 31 | // This is setting .eslintcache with cache directory 32 | const cache = fileEntryCache.create(eslintCacheName, eslintDirectory, useCheckSum); 33 | const myFileJavascriptPath = path.resolve(`./${fileCacheName}/src/my-file.js`); // Absolute path 34 | const myFileJavascriptDescriptor = cache.getFileDescriptor(myFileJavascriptPath); 35 | expect(myFileJavascriptDescriptor.key).toBe(myFileJavascriptPath); 36 | expect(myFileJavascriptDescriptor.meta).toBeDefined(); 37 | expect(myFileJavascriptDescriptor.changed).toBe(true); // First run 38 | expect(myFileJavascriptDescriptor.meta?.hash).toBeDefined(); 39 | 40 | // Now lets set the data and reconcile 41 | if (myFileJavascriptDescriptor.meta) { 42 | myFileJavascriptDescriptor.meta.data = {foo: 'bar'}; 43 | } 44 | 45 | // Reconcile 46 | cache.reconcile(); 47 | 48 | // Verify that the data is set 49 | const myFileJavascriptData = cache.getFileDescriptor(myFileJavascriptPath); 50 | expect(myFileJavascriptData.meta.data).toStrictEqual({foo: 'bar'}); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/file-entry-cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 7 | 8 | /* Emit */ 9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 12 | 13 | /* Interop Constraints */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | 17 | /* Type Checking */ 18 | "strict": true, /* Enable all strict type-checking options. */ 19 | 20 | /* Completeness */ 21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 22 | "lib": [ 23 | "ESNext", "DOM" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /packages/file-entry-cache/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ['json', 'text'], 7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/flat-cache/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/flat-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flat-cache", 3 | "version": "6.1.10", 4 | "description": "A simple key/value storage using files to persist the data", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jaredwray/cacheable.git", 18 | "directory": "packages/flat-cache" 19 | }, 20 | "author": "Jared Wray ", 21 | "license": "MIT", 22 | "private": false, 23 | "keywords": [ 24 | "cache", 25 | "caching", 26 | "cacheable", 27 | "flat-cache", 28 | "flat", 29 | "file", 30 | "file-cache", 31 | "file-caching", 32 | "file-based-cache", 33 | "file-persist", 34 | "file-persistence", 35 | "file-storage", 36 | "file-system", 37 | "file-management", 38 | "filesystem-cache", 39 | "disk-cache", 40 | "cache-persistence", 41 | "cache-persist", 42 | "persistent-cache", 43 | "persistent-storage", 44 | "cache-to-file", 45 | "cache-on-disk", 46 | "cache-file", 47 | "cache-expiration", 48 | "cache-lifetime", 49 | "data-persistence", 50 | "data-storage", 51 | "local-storage", 52 | "file-system-cache" 53 | ], 54 | "scripts": { 55 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean", 56 | "prepublish": "pnpm build", 57 | "test": "xo --fix && vitest run --coverage", 58 | "test:ci": "xo && vitest run", 59 | "clean": "rimraf ./dist ./coverage ./node_modules" 60 | }, 61 | "devDependencies": { 62 | "@types/node": "^22.15.30", 63 | "@vitest/coverage-v8": "^3.2.2", 64 | "rimraf": "^6.0.1", 65 | "tsup": "^8.5.0", 66 | "typescript": "^5.8.3", 67 | "vitest": "^3.2.2", 68 | "xo": "^1.1.0" 69 | }, 70 | "dependencies": { 71 | "cacheable": "workspace:^", 72 | "flatted": "^3.3.3", 73 | "hookified": "^1.9.1" 74 | }, 75 | "files": [ 76 | "dist", 77 | "license" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /packages/flat-cache/test/fixtures/.cache/cache1: -------------------------------------------------------------------------------- 1 | [{"key1":"1","key2":"2","key3":"3","key4":"4"},"value1","value2",{"bar":"5"},{"foo":"6"},"bar2","foo2"] -------------------------------------------------------------------------------- /packages/flat-cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 7 | 8 | /* Emit */ 9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 12 | 13 | /* Interop Constraints */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | 17 | /* Type Checking */ 18 | "strict": true, /* Enable all strict type-checking options. */ 19 | 20 | /* Completeness */ 21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 22 | "lib": [ 23 | "ESNext", "DOM" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /packages/flat-cache/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ['json', 'text'], 7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/node-cache/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredwray/cacheable/bc5387b7f0d99ac4c19a2dc69619db9163355fbe/packages/node-cache/.DS_Store -------------------------------------------------------------------------------- /packages/node-cache/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License & © Jared Wray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/node-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cacheable/node-cache", 3 | "version": "1.5.6", 4 | "description": "Simple and Maintained fast NodeJS internal caching", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jaredwray/cacheable.git", 18 | "directory": "packages/node-cache" 19 | }, 20 | "author": "Jared Wray ", 21 | "license": "MIT", 22 | "private": false, 23 | "keywords": [ 24 | "cache", 25 | "caching", 26 | "node", 27 | "nodejs", 28 | "cacheable", 29 | "cacheable-node-cache", 30 | "node-cache", 31 | "cacheable-node" 32 | ], 33 | "scripts": { 34 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean", 35 | "prepublish": "pnpm build", 36 | "test": "xo --fix && vitest run --coverage", 37 | "test:ci": "xo && vitest run", 38 | "clean": "rimraf ./dist ./coverage ./node_modules" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^22.15.30", 42 | "@vitest/coverage-v8": "^3.2.2", 43 | "rimraf": "^6.0.1", 44 | "tsup": "^8.5.0", 45 | "typescript": "^5.8.3", 46 | "vitest": "^3.2.2", 47 | "xo": "^1.1.0" 48 | }, 49 | "dependencies": { 50 | "cacheable": "workspace:^", 51 | "hookified": "^1.9.1", 52 | "keyv": "^5.3.3" 53 | }, 54 | "files": [ 55 | "dist", 56 | "license" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /packages/node-cache/src/store.ts: -------------------------------------------------------------------------------- 1 | import {Cacheable, CacheableMemory, type CacheableItem} from 'cacheable'; 2 | import {Keyv} from 'keyv'; 3 | import {type NodeCacheItem} from 'index.js'; 4 | import {Hookified} from 'hookified'; 5 | 6 | export type NodeCacheStoreOptions = { 7 | /** 8 | * Time to live in milliseconds. This is a breaking change from the original NodeCache. 9 | */ 10 | ttl?: number | string; 11 | /** 12 | * Maximum number of keys to store in the cache. If this is set to a value greater than 0, the cache will keep track of the number of keys and will not store more than the specified number of keys. 13 | */ 14 | maxKeys?: number; 15 | /** 16 | * Primary cache store. 17 | */ 18 | primary?: Keyv; 19 | /** 20 | * Secondary cache store. Learn more about the secondary cache store in the cacheable documentation. 21 | * [storage-tiering-and-caching](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching) 22 | */ 23 | secondary?: Keyv; 24 | 25 | /** 26 | * Enable stats tracking. This is a breaking change from the original NodeCache. 27 | */ 28 | stats?: boolean; 29 | }; 30 | 31 | export class NodeCacheStore extends Hookified { 32 | private _maxKeys = 0; 33 | private readonly _cache = new Cacheable({primary: new Keyv({store: new CacheableMemory()})}); 34 | constructor(options?: NodeCacheStoreOptions) { 35 | super(); 36 | if (options) { 37 | const cacheOptions = { 38 | ttl: options.ttl, 39 | primary: options.primary, 40 | secondary: options.secondary, 41 | stats: options.stats ?? true, 42 | }; 43 | 44 | this._cache = new Cacheable(cacheOptions); 45 | 46 | if (options.maxKeys) { 47 | this._maxKeys = options.maxKeys; 48 | } 49 | } 50 | 51 | // Hook up the cacheable events 52 | this._cache.on('error', (error: Error) => { 53 | /* c8 ignore next 1 */ 54 | this.emit('error', error); 55 | }); 56 | } 57 | 58 | /** 59 | * Cacheable instance. 60 | * @returns {Cacheable} 61 | * @readonly 62 | */ 63 | public get cache(): Cacheable { 64 | return this._cache; 65 | } 66 | 67 | /** 68 | * Time to live in milliseconds. 69 | * @returns {number | string | undefined} 70 | * @readonly 71 | */ 72 | public get ttl(): number | string | undefined { 73 | return this._cache.ttl; 74 | } 75 | 76 | /** 77 | * Time to live in milliseconds. 78 | * @param {number | string | undefined} ttl 79 | */ 80 | public set ttl(ttl: number | string | undefined) { 81 | this._cache.ttl = ttl; 82 | } 83 | 84 | /** 85 | * Primary cache store. 86 | * @returns {Keyv} 87 | * @readonly 88 | */ 89 | public get primary(): Keyv { 90 | return this._cache.primary; 91 | } 92 | 93 | /** 94 | * Primary cache store. 95 | * @param {Keyv} primary 96 | */ 97 | public set primary(primary: Keyv) { 98 | this._cache.primary = primary; 99 | } 100 | 101 | /** 102 | * Secondary cache store. Learn more about the secondary cache store in the 103 | * [cacheable](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching) documentation. 104 | * @returns {Keyv | undefined} 105 | */ 106 | public get secondary(): Keyv | undefined { 107 | return this._cache.secondary; 108 | } 109 | 110 | /** 111 | * Secondary cache store. Learn more about the secondary cache store in the 112 | * [cacheable](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching) documentation. 113 | * @param {Keyv | undefined} secondary 114 | */ 115 | public set secondary(secondary: Keyv | undefined) { 116 | this._cache.secondary = secondary; 117 | } 118 | 119 | /** 120 | * Maximum number of keys to store in the cache. if this is set to a value greater than 0, 121 | * the cache will keep track of the number of keys and will not store more than the specified number of keys. 122 | * @returns {number} 123 | * @readonly 124 | */ 125 | public get maxKeys(): number { 126 | return this._maxKeys; 127 | } 128 | 129 | /** 130 | * Maximum number of keys to store in the cache. if this is set to a value greater than 0, 131 | * the cache will keep track of the number of keys and will not store more than the specified number of keys. 132 | * @param {number} maxKeys 133 | */ 134 | public set maxKeys(maxKeys: number) { 135 | this._maxKeys = maxKeys; 136 | if (this._maxKeys > 0) { 137 | this._cache.stats.enabled = true; 138 | } 139 | } 140 | 141 | /** 142 | * Set a key/value pair in the cache. 143 | * @param {string | number} key 144 | * @param {any} value 145 | * @param {number} [ttl] 146 | * @returns {boolean} 147 | */ 148 | public async set(key: string | number, value: any, ttl?: number): Promise { 149 | if (this._maxKeys > 0) { 150 | // eslint-disable-next-line unicorn/no-lonely-if 151 | if (this._cache.stats.count >= this._maxKeys) { 152 | return false; 153 | } 154 | } 155 | 156 | await this._cache.set(key.toString(), value, ttl); 157 | return true; 158 | } 159 | 160 | /** 161 | * Set multiple key/value pairs in the cache. 162 | * @param {NodeCacheItem[]} list 163 | * @returns {void} 164 | */ 165 | public async mset(list: NodeCacheItem[]): Promise { 166 | const items = new Array(); 167 | for (const item of list) { 168 | items.push({key: item.key.toString(), value: item.value, ttl: item.ttl}); 169 | } 170 | 171 | await this._cache.setMany(items); 172 | } 173 | 174 | /** 175 | * Get a value from the cache. 176 | * @param {string | number} key 177 | * @returns {any | undefined} 178 | */ 179 | public async get(key: string | number): Promise { 180 | return this._cache.get(key.toString()); 181 | } 182 | 183 | /** 184 | * Get multiple values from the cache. 185 | * @param {Array} keys 186 | * @returns {Record} 187 | */ 188 | public async mget(keys: Array): Promise> { 189 | const result: Record = {}; 190 | for (const key of keys) { 191 | // eslint-disable-next-line no-await-in-loop 192 | result[key.toString()] = await this._cache.get(key.toString()); 193 | } 194 | 195 | return result; 196 | } 197 | 198 | /** 199 | * Delete a key from the cache. 200 | * @param {string | number} key 201 | * @returns {boolean} 202 | */ 203 | public async del(key: string | number): Promise { 204 | return this._cache.delete(key.toString()); 205 | } 206 | 207 | /** 208 | * Delete multiple keys from the cache. 209 | * @param {Array} keys 210 | * @returns {boolean} 211 | */ 212 | public async mdel(keys: Array): Promise { 213 | return this._cache.deleteMany(keys.map(key => key.toString())); 214 | } 215 | 216 | /** 217 | * Clear the cache. 218 | * @returns {void} 219 | */ 220 | public async clear(): Promise { 221 | return this._cache.clear(); 222 | } 223 | 224 | /** 225 | * Check if a key exists in the cache. 226 | * @param {string | number} key 227 | * @returns {boolean} 228 | */ 229 | public async setTtl(key: string | number, ttl?: number): Promise { 230 | const item = await this._cache.get(key.toString()); 231 | if (item) { 232 | await this._cache.set(key.toString(), item, ttl); 233 | return true; 234 | } 235 | 236 | return false; 237 | } 238 | 239 | /** 240 | * Check if a key exists in the cache. If it does exist it will get the value and delete the item from the cache. 241 | * @param {string | number} key 242 | * @returns {any | undefined} 243 | */ 244 | public async take(key: string | number): Promise { 245 | return this._cache.take(key.toString()); 246 | } 247 | 248 | /** 249 | * Disconnect from the cache. 250 | * @returns {void} 251 | */ 252 | public async disconnect(): Promise { 253 | await this._cache.disconnect(); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /packages/node-cache/test/export.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect} from 'vitest'; 2 | import {NodeCache} from '../src/index.js'; 3 | 4 | const cache = new NodeCache({checkperiod: 0}); 5 | 6 | describe('NodeCache', () => { 7 | test('should create a new instance of NodeCache', () => { 8 | const cache = new NodeCache({checkperiod: 0}); 9 | expect(cache).toBeInstanceOf(NodeCache); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/node-cache/test/store.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect} from 'vitest'; 2 | import {Keyv} from 'keyv'; 3 | import {NodeCacheStore} from '../src/store.js'; 4 | 5 | // eslint-disable-next-line no-promise-executor-return 6 | const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 7 | 8 | describe('NodeCacheStore', () => { 9 | test('should create a new instance', () => { 10 | const store = new NodeCacheStore(); 11 | expect(store).toBeDefined(); 12 | }); 13 | test('should create a new instance with options', () => { 14 | const store = new NodeCacheStore({maxKeys: 100}); 15 | expect(store.maxKeys).toBe(100); 16 | store.maxKeys = 200; 17 | expect(store.maxKeys).toBe(200); 18 | }); 19 | test('should set a ttl', () => { 20 | const store = new NodeCacheStore({ttl: 100}); 21 | expect(store.ttl).toBe(100); 22 | store.ttl = 200; 23 | expect(store.ttl).toBe(200); 24 | }); 25 | test('should set a primary keyv store', () => { 26 | const store = new NodeCacheStore(); 27 | expect(store.primary).toBeDefined(); 28 | const keyv = new Keyv(); 29 | store.primary = keyv; 30 | expect(store.primary).toBe(keyv); 31 | }); 32 | test('should set a secondary keyv store', () => { 33 | const store = new NodeCacheStore(); 34 | expect(store.secondary).toBeUndefined(); 35 | const keyv = new Keyv(); 36 | store.secondary = keyv; 37 | expect(store.secondary).toBe(keyv); 38 | }); 39 | test('should be able to get and set primary and secondary keyv stores', async () => { 40 | const store = new NodeCacheStore(); 41 | expect(store.primary).toBeDefined(); 42 | expect(store.secondary).toBeUndefined(); 43 | const primary = new Keyv(); 44 | const secondary = new Keyv(); 45 | store.primary = primary; 46 | store.secondary = secondary; 47 | expect(store.primary).toBe(primary); 48 | expect(store.secondary).toBe(secondary); 49 | await store.set('test', 'value'); 50 | const restult1 = await store.get('test'); 51 | expect(restult1).toBe('value'); 52 | await store.set('test', 'value', 100); 53 | const restult2 = await store.get('test'); 54 | expect(restult2).toBe('value'); 55 | await sleep(200); 56 | const restult3 = await store.get('test'); 57 | expect(restult3).toBeUndefined(); 58 | }); 59 | test('should set a maxKeys limit', async () => { 60 | const store = new NodeCacheStore({maxKeys: 3}); 61 | expect(store.maxKeys).toBe(3); 62 | expect(store.cache.stats.enabled).toBe(true); 63 | await store.set('test1', 'value1'); 64 | await store.set('test2', 'value2'); 65 | await store.set('test3', 'value3'); 66 | await store.set('test4', 'value4'); 67 | const result1 = await store.get('test4'); 68 | expect(result1).toBeUndefined(); 69 | }); 70 | test('should clear the cache', async () => { 71 | const store = new NodeCacheStore(); 72 | await store.set('test', 'value'); 73 | await store.clear(); 74 | const result1 = await store.get('test'); 75 | expect(result1).toBeUndefined(); 76 | }); 77 | test('should delete a key', async () => { 78 | const store = new NodeCacheStore(); 79 | await store.set('test', 'value'); 80 | await store.del('test'); 81 | const result1 = await store.get('test'); 82 | expect(result1).toBeUndefined(); 83 | }); 84 | test('should be able to get and set an object', async () => { 85 | const store = new NodeCacheStore(); 86 | await store.set('test', {foo: 'bar'}); 87 | const result1 = await store.get('test'); 88 | expect(result1).toEqual({foo: 'bar'}); 89 | }); 90 | test('should be able to get and set an array', async () => { 91 | const store = new NodeCacheStore(); 92 | await store.set('test', ['foo', 'bar']); 93 | const result1 = await store.get('test'); 94 | expect(result1).toEqual(['foo', 'bar']); 95 | }); 96 | test('should be able to get and set a number', async () => { 97 | const store = new NodeCacheStore(); 98 | await store.set('test', 123); 99 | const result1 = await store.get('test'); 100 | expect(result1).toBe(123); 101 | }); 102 | test('should be able to set multiple keys', async () => { 103 | const store = new NodeCacheStore(); 104 | await store.mset([ 105 | {key: 'test1', value: 'value1'}, 106 | {key: 'test2', value: 'value2'}, 107 | ]); 108 | const result1 = await store.get('test1'); 109 | const result2 = await store.get('test2'); 110 | expect(result1).toBe('value1'); 111 | expect(result2).toBe('value2'); 112 | }); 113 | test('should be able to get multiple keys', async () => { 114 | const store = new NodeCacheStore(); 115 | await store.set('test1', 'value1'); 116 | await store.set('test2', 'value2'); 117 | const result1 = await store.mget(['test1', 'test2']); 118 | expect(result1).toEqual({test1: 'value1', test2: 'value2'}); 119 | }); 120 | test('should be able to delete multiple keys', async () => { 121 | const store = new NodeCacheStore(); 122 | await store.set('test1', 'value1'); 123 | await store.set('test2', 'value2'); 124 | await store.mdel(['test1', 'test2']); 125 | const result1 = await store.get('test1'); 126 | const result2 = await store.get('test2'); 127 | expect(result1).toBeUndefined(); 128 | expect(result2).toBeUndefined(); 129 | }); 130 | test('should be able to set a key with ttl', async () => { 131 | const store = new NodeCacheStore(); 132 | await store.set('test', 'value', 1000); 133 | const result1 = await store.get('test'); 134 | expect(result1).toBe('value'); 135 | const result2 = await store.setTtl('test', 1000); 136 | expect(result2).toBe(true); 137 | }); 138 | test('should return false if no ttl is set', async () => { 139 | const store = new NodeCacheStore({ttl: 1000}); 140 | const result1 = await store.setTtl('test'); 141 | expect(result1).toBe(false); 142 | }); 143 | test('should be able to disconnect', async () => { 144 | const store = new NodeCacheStore(); 145 | await store.disconnect(); 146 | }); 147 | test('should be able to take a key', async () => { 148 | const store = new NodeCacheStore(); 149 | await store.set('test', 'value'); 150 | const result1 = await store.take('test'); 151 | expect(result1).toBe('value'); 152 | const result2 = await store.get('test'); 153 | expect(result2).toBeUndefined(); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /packages/node-cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ 7 | 8 | /* Emit */ 9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 12 | 13 | /* Interop Constraints */ 14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 16 | 17 | /* Type Checking */ 18 | "strict": true, /* Enable all strict type-checking options. */ 19 | 20 | /* Completeness */ 21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 22 | "lib": [ 23 | "ESNext", "DOM" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /packages/node-cache/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ['json', 'text'], 7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cacheable/website", 3 | "version": "1.0.0", 4 | "description": "Cacheable Website", 5 | "repository": "https://github.com/jaredwray/cacheable.git", 6 | "author": "Jared Wray ", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "generate-docs": "npx tsx ./src/docs.cts", 11 | "website:build": "rimraf ./site/docs && pnpm generate-docs && docula build", 12 | "website:serve": "rimraf ./site/docs && pnpm generate-docs && docula serve", 13 | "clean": "rimraf ./dist" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^22.15.30", 17 | "docula": "^0.12.2", 18 | "rimraf": "^6.0.1", 19 | "tsx": "^4.19.4" 20 | }, 21 | "dependencies": { 22 | "@types/fs-extra": "^11.0.4", 23 | "fs-extra": "^11.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/website/site/docula.config.mjs: -------------------------------------------------------------------------------- 1 | export const options = { 2 | githubPath: 'jaredwray/cacheable', 3 | siteTitle: 'Cacheable', 4 | siteDescription: 'Caching for Node.js', 5 | siteUrl: 'https://cacheable.org', 6 | sections: [ 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/website/site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredwray/cacheable/bc5387b7f0d99ac4c19a2dc69619db9163355fbe/packages/website/site/favicon.ico -------------------------------------------------------------------------------- /packages/website/site/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/site/symbol.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/site/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-family: 'Open Sans', sans-serif; 3 | 4 | --color-primary: #fa3c32; 5 | --color-secondary: #ff7800; 6 | --color-secondary-dark: #322d28; 7 | --color-text: #322d28; 8 | 9 | --background: #ffffff; 10 | --home-background: #ffffff; 11 | --header-background: #ffffff; 12 | 13 | --sidebar-background: #ffffff; 14 | --sidebar-text: #322d28; 15 | --sidebar-text-active: #ff7800; 16 | 17 | --border: rgba(238,238,245,1); 18 | 19 | --background-search-highlight: var(--color-secondary-dark); 20 | --color-search-highlight: #ffffff; 21 | --search-input-background: var(--header-background); 22 | 23 | --code: rgba(238,238,245,1); 24 | 25 | --pagefind-ui-text: var(--color-text) !important; 26 | --pagefind-ui-font: var(--font-family) !important; 27 | --pagefind-ui-background: var(--background) !important; 28 | --pagefind-ui-border: var(--border) !important; 29 | --pagefind-ui-scale: .9 !important; 30 | } -------------------------------------------------------------------------------- /packages/website/src/docs.cts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | 3 | async function main() { 4 | 5 | console.log("packages path:" + getRelativePackagePath()); 6 | 7 | await copyPackages(); 8 | await copyGettingStarted(); 9 | await copyCacheableSymbol(); 10 | }; 11 | 12 | async function copyPackages() { 13 | const packagesPath = getRelativePackagePath(); 14 | const packageList = await fs.promises.readdir(`${packagesPath}`); 15 | const filterList = ["website", ".DS_Store"]; 16 | 17 | for (const packageName of packageList) { 18 | if((filterList.indexOf(packageName) > -1) !== true ) { 19 | console.log("Adding package: " + packageName); 20 | await createDoc(packageName, `${packagesPath}`, `${packagesPath}/website/site/docs`); 21 | } 22 | }; 23 | } 24 | 25 | async function copyCacheableSymbol() { 26 | const rootPath = getRelativeRootPath(); 27 | const packagesPath = getRelativePackagePath(); 28 | const outputPath = `${packagesPath}/website/dist`; 29 | await fs.ensureDir(`${outputPath}`); 30 | await fs.copy(`${packagesPath}/website/site/symbol.svg`, `${outputPath}/symbol.svg`); 31 | } 32 | 33 | async function copyGettingStarted() { 34 | console.log("Adding Getting Started"); 35 | const rootPath = getRelativeRootPath(); 36 | const packagesPath = getRelativePackagePath(); 37 | const outputPath = `${packagesPath}/website/site/docs`; 38 | const originalFileText = await fs.readFile(`${rootPath}/README.md`, "utf8"); 39 | let newFileText = "---\n"; 40 | newFileText += `title: 'Getting Started Guide'\n`; 41 | newFileText += `order: 1\n`; 42 | //newFileText += `parent: '${parent}'\n`; 43 | newFileText += "---\n"; 44 | newFileText += "\n"; 45 | newFileText += originalFileText; 46 | 47 | newFileText = cleanDocumentFromImage(newFileText); 48 | 49 | await fs.ensureDir(`${outputPath}`); 50 | await fs.writeFile(`${outputPath}/index.md`, newFileText); 51 | } 52 | 53 | function cleanDocumentFromImage(document: string) { 54 | document = document.replace(`[Cacheable](https://github.com/jaredwray/cacheable)`, ""); 55 | document = document.replace(`[Cacheable](https://github.com/jaredwray/cacheable)`, ""); 56 | return document; 57 | }; 58 | 59 | function getRelativePackagePath() { 60 | if(fs.pathExistsSync("packages")) { 61 | //we are in the root 62 | return "packages"; 63 | } 64 | 65 | //we are in the website folder 66 | return "../../packages" 67 | } 68 | 69 | function getRelativeRootPath() { 70 | if(fs.pathExistsSync("packages")) { 71 | //we are in the root 72 | return "./"; 73 | } 74 | 75 | //we are in the website folder 76 | return "../../" 77 | } 78 | 79 | async function createDoc(packageName: string, path: string, outputPath: string) { 80 | const originalFileName = "README.md"; 81 | const newFileName = `${packageName}.md`; 82 | const packageJSONPath = `${path}/${packageName}/package.json`; 83 | const packageJSON = await fs.readJSON(packageJSONPath); 84 | const originalFileText = await fs.readFile(`${path}/${packageName}/${originalFileName}`, "utf8"); 85 | let newFileText = "---\n"; 86 | newFileText += `title: '${packageJSON.name}'\n`; 87 | newFileText += `sidebarTitle: '${packageJSON.name}'\n`; 88 | //newFileText += `parent: '${parent}'\n`; 89 | newFileText += "---\n"; 90 | newFileText += "\n"; 91 | newFileText += originalFileText; 92 | 93 | newFileText = cleanDocumentFromImage(newFileText); 94 | 95 | await fs.ensureDir(`${outputPath}`); 96 | await fs.writeFile(`${outputPath}/${newFileName}`, newFileText); 97 | } 98 | 99 | main(); 100 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config' 2 | 3 | export default defineWorkspace([ 4 | "./packages/node-cache/vite.config.ts", 5 | "./packages/cache-manager/vite.config.ts", 6 | "./packages/file-entry-cache/vite.config.ts", 7 | "./packages/flat-cache/vite.config.ts", 8 | "./packages/cacheable-request/vitest.config.mjs", 9 | "./packages/cacheable/vite.config.ts" 10 | ]) 11 | --------------------------------------------------------------------------------