├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── eslint.config.js ├── index.js ├── package.json ├── test └── index.test.js └── types ├── index.d.ts └── index.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci-redis.yml@v5 26 | with: 27 | lint: true 28 | license-check: true 29 | license-check-allowed-additional: 'Apache*' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | 154 | # test tap report 155 | out.tap 156 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fastify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @fastify/redis 2 | 3 | [![CI](https://github.com/fastify/fastify-redis/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-redis/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/@fastify/redis.svg?style=flat)](https://www.npmjs.com/package/@fastify/redis) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | Fastify Redis connection plugin; with this you can share the same Redis connection in every part of your server. 8 | 9 | ## Install 10 | 11 | ``` 12 | npm i @fastify/redis 13 | ``` 14 | 15 | ### Compatibility 16 | | Plugin version | Fastify version | 17 | | ---------------|-----------------| 18 | | `>=7.x` | `^5.x` | 19 | | `^6.x` | `^4.x` | 20 | | `>=4.x <6.x` | `^3.x` | 21 | | `^3.x` | `^2.x` | 22 | | `>=1.x <3.x` | `^1.x` | 23 | 24 | 25 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 26 | in the table above. 27 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 28 | 29 | ## Usage 30 | 31 | Add it to your project with `register` and you are done! 32 | 33 | ### Create a new Redis Client 34 | 35 | Under the hood [ioredis](https://github.com/luin/ioredis) is used as client, the ``options`` that you pass to `register` will be passed to the Redis client. 36 | 37 | ```js 38 | const fastify = require('fastify')() 39 | 40 | // create by specifying host 41 | fastify.register(require('@fastify/redis'), { host: '127.0.0.1' }) 42 | 43 | // OR by specifying Redis URL 44 | fastify.register(require('@fastify/redis'), { url: 'redis://127.0.0.1', /* other redis options */ }) 45 | 46 | // OR with more options 47 | fastify.register(require('@fastify/redis'), { 48 | host: '127.0.0.1', 49 | password: '***', 50 | port: 6379, // Redis port 51 | family: 4 // 4 (IPv4) or 6 (IPv6) 52 | }) 53 | ``` 54 | 55 | ### Accessing the Redis Client 56 | 57 | Once you have registered your plugin, you can access the Redis client via `fastify.redis`. 58 | 59 | The client is automatically closed when the fastify instance is closed. 60 | 61 | ```js 62 | 'use strict' 63 | 64 | const Fastify = require('fastify') 65 | const fastifyRedis = require('@fastify/redis') 66 | 67 | const fastify = Fastify({ logger: true }) 68 | 69 | fastify.register(fastifyRedis, { 70 | host: '127.0.0.1', 71 | password: 'your strong password here', 72 | port: 6379, // Redis port 73 | family: 4 // 4 (IPv4) or 6 (IPv6) 74 | }) 75 | 76 | fastify.get('/foo', (req, reply) => { 77 | const { redis } = fastify 78 | redis.get(req.query.key, (err, val) => { 79 | reply.send(err || val) 80 | }) 81 | }) 82 | 83 | fastify.post('/foo', (req, reply) => { 84 | const { redis } = fastify 85 | redis.set(req.body.key, req.body.value, (err) => { 86 | reply.send(err || { status: 'ok' }) 87 | }) 88 | }) 89 | 90 | fastify.listen({ port: 3000 }, err => { 91 | if (err) throw err 92 | console.log(`server listening on ${fastify.server.address().port}`) 93 | }) 94 | ``` 95 | 96 | ### Using an existing Redis client 97 | 98 | You may also supply an existing *Redis* client instance by passing an options 99 | object with the `client` property set to the instance. In this case, 100 | the client is not automatically closed when the Fastify instance is 101 | closed. 102 | 103 | ```js 104 | 'use strict' 105 | 106 | const fastify = require('fastify')() 107 | const Redis = require('ioredis') 108 | 109 | const client = new Redis({ host: 'localhost', port: 6379 }) 110 | 111 | fastify.register(require('@fastify/redis'), { client }) 112 | ``` 113 | 114 | You can also supply a *Redis Cluster* instance to the client: 115 | 116 | ```js 117 | 'use strict' 118 | 119 | const fastify = require('fastify')() 120 | const Redis = require('ioredis') 121 | 122 | const client = new Redis.Cluster([{ host: 'localhost', port: 6379 }]); 123 | 124 | fastify.register(require('@fastify/redis'), { client }) 125 | ``` 126 | 127 | Note: by default, *@fastify/redis* will **not** automatically close the client 128 | connection when the Fastify server shuts down. 129 | 130 | To automatically close the client connection, set clientClose to true. 131 | 132 | ```js 133 | fastify.register(require('@fastify/redis'), { client, closeClient: true }) 134 | ``` 135 | 136 | ## Registering multiple Redis client instances 137 | 138 | By using the `namespace` option you can register multiple Redis client instances. 139 | 140 | ```js 141 | 'use strict' 142 | 143 | const fastify = require('fastify')() 144 | 145 | fastify 146 | .register(require('@fastify/redis'), { 147 | host: '127.0.0.1', 148 | port: 6380, 149 | namespace: 'hello' 150 | }) 151 | .register(require('@fastify/redis'), { 152 | client: redis, 153 | namespace: 'world' 154 | }) 155 | 156 | // Here we will use the `hello` named instance 157 | fastify.get('/hello', (req, reply) => { 158 | const { redis } = fastify 159 | 160 | redis.hello.get(req.query.key, (err, val) => { 161 | reply.send(err || val) 162 | }) 163 | }) 164 | 165 | fastify.post('/hello', (req, reply) => { 166 | const { redis } = fastify 167 | 168 | redis['hello'].set(req.body.key, req.body.value, (err) => { 169 | reply.send(err || { status: 'ok' }) 170 | }) 171 | }) 172 | 173 | // Here we will use the `world` named instance 174 | fastify.get('/world', (req, reply) => { 175 | const { redis } = fastify 176 | 177 | redis['world'].get(req.query.key, (err, val) => { 178 | reply.send(err || val) 179 | }) 180 | }) 181 | 182 | fastify.post('/world', (req, reply) => { 183 | const { redis } = fastify 184 | 185 | redis.world.set(req.body.key, req.body.value, (err) => { 186 | reply.send(err || { status: 'ok' }) 187 | }) 188 | }) 189 | 190 | fastify.listen({ port: 3000 }, function (err) { 191 | if (err) { 192 | fastify.log.error(err) 193 | process.exit(1) 194 | } 195 | }) 196 | 197 | ``` 198 | 199 | ## Redis streams (Redis 5.0 or greater is required) 200 | 201 | `@fastify/redis` supports Redis streams out of the box. 202 | 203 | ```js 204 | 'use strict' 205 | 206 | const fastify = require('fastify')() 207 | 208 | fastify.register(require('@fastify/redis'), { 209 | host: '127.0.0.1', 210 | port: 6380 211 | }) 212 | 213 | fastify.get('/streams', async (request, reply) => { 214 | // We write an event to the stream 'my awesome fastify stream name', setting 'key' to 'value' 215 | await fastify.redis.xadd(['my awesome fastify stream name', '*', 'hello', 'fastify is awesome']) 216 | 217 | // We read events from the beginning of the stream called 'my awesome fastify stream name' 218 | let redisStream = await fastify.redis.xread(['STREAMS', 'my awesome fastify stream name', 0]) 219 | 220 | // We parse the results 221 | let response = [] 222 | let events = redisStream[0][1] 223 | 224 | for (let i = 0; i < events.length; i++) { 225 | const e = events[i] 226 | response.push(`#LOG: id is ${e[0].toString()}`) 227 | 228 | // We log each key 229 | for (const key in e[1]) { 230 | response.push(e[1][key].toString()) 231 | } 232 | } 233 | 234 | reply.status(200) 235 | return { output: response } 236 | // Will return something like this : 237 | // { "output": ["#LOG: id is 1559985742035-0", "hello", "fastify is awesome"] } 238 | }) 239 | 240 | fastify.listen({ port: 3000 }, function (err) { 241 | if (err) { 242 | fastify.log.error(err) 243 | process.exit(1) 244 | } 245 | }) 246 | ``` 247 | *NB you can find more information about Redis streams and the relevant commands [here](https://redis.io/topics/streams-intro) and [here](https://redis.io/commands#stream).* 248 | 249 | ## Redis connection error 250 | The majority of errors are silent due to the `ioredis` silent error handling but during the plugin registration it will check that the connection with the redis instance is correctly estabilished. 251 | In this case, you can receive an `ERR_AVVIO_PLUGIN_TIMEOUT` error if the connection cannot be established in the expected time frame or a dedicated error for an invalid connection. 252 | 253 | ## Acknowledgments 254 | 255 | This project is kindly sponsored by: 256 | - [nearForm](https://nearform.com) 257 | - [LetzDoIt](https://www.letzdoitapp.com/) 258 | 259 | ## License 260 | 261 | Licensed under [MIT](./LICENSE). 262 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | const Redis = require('ioredis') 5 | 6 | function fastifyRedis (fastify, options, next) { 7 | const { namespace, url, closeClient = false, ...redisOptions } = options 8 | 9 | let client = options.client || null 10 | 11 | if (namespace) { 12 | if (!fastify.redis) { 13 | fastify.decorate('redis', Object.create(null)) 14 | } 15 | 16 | if (fastify.redis[namespace]) { 17 | return next(new Error(`Redis '${namespace}' instance namespace has already been registered`)) 18 | } 19 | 20 | const closeNamedInstance = (fastify) => fastify.redis[namespace].quit() 21 | 22 | if (client) { 23 | if (closeClient === true) { 24 | fastify.addHook('onClose', closeNamedInstance) 25 | } 26 | } else { 27 | try { 28 | if (url) { 29 | client = new Redis(url, redisOptions) 30 | } else { 31 | client = new Redis(redisOptions) 32 | } 33 | } catch (err) { 34 | return next(err) 35 | } 36 | 37 | fastify.addHook('onClose', closeNamedInstance) 38 | } 39 | 40 | fastify.redis[namespace] = client 41 | } else { 42 | if (fastify.redis) { 43 | return next(new Error('@fastify/redis has already been registered')) 44 | } else { 45 | if (client) { 46 | if (closeClient === true) { 47 | fastify.addHook('onClose', close) 48 | } 49 | } else { 50 | try { 51 | if (url) { 52 | client = new Redis(url, redisOptions) 53 | } else { 54 | client = new Redis(redisOptions) 55 | } 56 | } catch (err) { 57 | return next(err) 58 | } 59 | 60 | fastify.addHook('onClose', close) 61 | } 62 | 63 | fastify.decorate('redis', client) 64 | } 65 | } 66 | 67 | // Testing this make the process crash on latest TAP :( 68 | /* c8 ignore start */ 69 | const onEnd = function (err) { 70 | client 71 | .off('ready', onReady) 72 | .off('error', onError) 73 | .off('end', onEnd) 74 | .quit() 75 | 76 | next(err) 77 | } 78 | /* c8 ignore stop */ 79 | 80 | const onReady = function () { 81 | client 82 | .off('end', onEnd) 83 | .off('error', onError) 84 | .off('ready', onReady) 85 | 86 | next() 87 | } 88 | 89 | // Testing this make the process crash on latest TAP :( 90 | /* c8 ignore start */ 91 | const onError = function (err) { 92 | if (err.code === 'ENOTFOUND') { 93 | onEnd(err) 94 | return 95 | } 96 | 97 | // Swallow network errors to allow ioredis 98 | // to perform reconnection and emit 'end' 99 | // event if reconnection eventually 100 | // fails. 101 | // Any other errors during startup will 102 | // trigger the 'end' event. 103 | if (err instanceof Redis.ReplyError) { 104 | onEnd(err) 105 | } 106 | } 107 | /* c8 ignore stop */ 108 | 109 | // ioredis provides it in a .status property 110 | if (client.status === 'ready') { 111 | // client is already connected, do not register event handlers 112 | // call next() directly to avoid ERR_AVVIO_PLUGIN_TIMEOUT 113 | next() 114 | } else { 115 | // ready event can still be emitted 116 | client 117 | .on('end', onEnd) 118 | .on('error', onError) 119 | .on('ready', onReady) 120 | 121 | client.ping().catch(onError) 122 | } 123 | } 124 | 125 | function close (fastify) { 126 | return fastify.redis.quit() 127 | } 128 | 129 | module.exports = fp(fastifyRedis, { 130 | fastify: '5.x', 131 | name: '@fastify/redis' 132 | }) 133 | module.exports.default = fastifyRedis 134 | module.exports.fastifyRedis = fastifyRedis 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/redis", 3 | "version": "7.0.2", 4 | "description": "Plugin to share a common Redis connection across Fastify.", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "redis": "docker run -p 6379:6379 --rm redis", 12 | "valkey": "docker run -p 6379:6379 --rm valkey/valkey:7.2", 13 | "test": "npm run unit && npm run typescript", 14 | "typescript": "tsd", 15 | "unit": "c8 --100 node --test" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/fastify/fastify-redis.git" 20 | }, 21 | "keywords": [ 22 | "fastify", 23 | "redis", 24 | "database", 25 | "speed", 26 | "cache", 27 | "ioredis" 28 | ], 29 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 30 | "contributors": [ 31 | { 32 | "name": "Matteo Collina", 33 | "email": "hello@matteocollina.com" 34 | }, 35 | { 36 | "name": "Manuel Spigolon", 37 | "email": "behemoth89@gmail.com" 38 | }, 39 | { 40 | "name": "James Sumners", 41 | "url": "https://james.sumners.info" 42 | }, 43 | { 44 | "name": "Frazer Smith", 45 | "email": "frazer.dev@icloud.com", 46 | "url": "https://github.com/fdawgs" 47 | } 48 | ], 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/fastify/fastify-redis/issues" 52 | }, 53 | "homepage": "https://github.com/fastify/fastify-redis#readme", 54 | "funding": [ 55 | { 56 | "type": "github", 57 | "url": "https://github.com/sponsors/fastify" 58 | }, 59 | { 60 | "type": "opencollective", 61 | "url": "https://opencollective.com/fastify" 62 | } 63 | ], 64 | "devDependencies": { 65 | "@fastify/pre-commit": "^2.1.0", 66 | "@types/node": "^22.0.0", 67 | "c8": "^10.1.3", 68 | "eslint": "^9.17.0", 69 | "fastify": "^5.0.0", 70 | "neostandard": "^0.12.0", 71 | "proxyquire": "^2.1.3", 72 | "tsd": "^0.32.0", 73 | "why-is-node-running": "^2.2.2" 74 | }, 75 | "dependencies": { 76 | "fastify-plugin": "^5.0.0", 77 | "ioredis": "^5.3.2" 78 | }, 79 | "publishConfig": { 80 | "access": "public" 81 | }, 82 | "pre-commit": [ 83 | "lint", 84 | "test" 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const whyIsNodeRunning = require('why-is-node-running') 4 | const { test } = require('node:test') 5 | const proxyquire = require('proxyquire') 6 | const Fastify = require('fastify') 7 | const Redis = require('ioredis') 8 | const fastifyRedis = require('..') 9 | 10 | test.beforeEach(async () => { 11 | const fastify = Fastify() 12 | 13 | fastify.register(fastifyRedis, { 14 | host: '127.0.0.1' 15 | }) 16 | 17 | await fastify.ready() 18 | await fastify.redis.flushall() 19 | await fastify.close() 20 | }) 21 | 22 | test('fastify.redis should exist', async (t) => { 23 | t.plan(1) 24 | const fastify = Fastify() 25 | fastify.register(fastifyRedis, { 26 | host: '127.0.0.1' 27 | }) 28 | 29 | await fastify.ready() 30 | t.assert.ok(fastify.redis) 31 | 32 | await fastify.close() 33 | }) 34 | 35 | test('fastify.redis should support url', async (t) => { 36 | t.plan(2) 37 | const fastify = Fastify() 38 | 39 | const fastifyRedis = proxyquire('..', { 40 | ioredis: function Redis (path, options) { 41 | t.assert.deepStrictEqual(path, 'redis://127.0.0.1') 42 | t.assert.deepStrictEqual(options, { 43 | otherOption: 'foo' 44 | }) 45 | this.quit = () => {} 46 | this.info = cb => cb(null, 'info') 47 | this.on = function (name, handler) { 48 | if (name === 'ready') { 49 | handler(null, 'ready') 50 | } 51 | 52 | return this 53 | } 54 | this.status = 'ready' 55 | this.off = function () { return this } 56 | 57 | return this 58 | } 59 | }) 60 | 61 | fastify.register(fastifyRedis, { 62 | url: 'redis://127.0.0.1', 63 | otherOption: 'foo' 64 | }) 65 | 66 | await fastify.ready() 67 | 68 | await fastify.close() 69 | }) 70 | 71 | test('fastify.redis should be the redis client', async (t) => { 72 | t.plan(1) 73 | const fastify = Fastify() 74 | 75 | fastify.register(fastifyRedis, { 76 | host: '127.0.0.1' 77 | }) 78 | 79 | await fastify.ready() 80 | 81 | await fastify.redis.set('key', 'value') 82 | const val = await fastify.redis.get('key') 83 | t.assert.deepStrictEqual(val, 'value') 84 | 85 | await fastify.close() 86 | }) 87 | 88 | test('fastify.redis.test namespace should exist', async (t) => { 89 | t.plan(2) 90 | 91 | const fastify = Fastify() 92 | fastify.register(fastifyRedis, { 93 | host: '127.0.0.1', 94 | namespace: 'test' 95 | }) 96 | 97 | await fastify.ready() 98 | 99 | t.assert.ok(fastify.redis) 100 | t.assert.ok(fastify.redis.test) 101 | 102 | await fastify.close() 103 | }) 104 | 105 | test('fastify.redis.test should be the redis client', async (t) => { 106 | t.plan(1) 107 | const fastify = Fastify() 108 | 109 | fastify.register(fastifyRedis, { 110 | host: '127.0.0.1', 111 | namespace: 'test' 112 | }) 113 | 114 | await fastify.ready() 115 | 116 | await fastify.redis.test.set('key_namespace', 'value_namespace') 117 | const val = await fastify.redis.test.get('key_namespace') 118 | t.assert.deepStrictEqual(val, 'value_namespace') 119 | 120 | await fastify.close() 121 | }) 122 | 123 | test('promises support', async (t) => { 124 | t.plan(1) 125 | const fastify = Fastify() 126 | 127 | fastify.register(fastifyRedis, { 128 | host: '127.0.0.1' 129 | }) 130 | 131 | await fastify.ready() 132 | 133 | await fastify.redis.set('key', 'value') 134 | const val = await fastify.redis.get('key') 135 | t.assert.deepStrictEqual(val, 'value') 136 | 137 | await fastify.close() 138 | }) 139 | 140 | test('custom ioredis client that is already connected', async (t) => { 141 | t.plan(3) 142 | const fastify = Fastify() 143 | const Redis = require('ioredis') 144 | const redis = new Redis({ host: 'localhost', port: 6379 }) 145 | 146 | await redis.set('key', 'value') 147 | const val = await redis.get('key') 148 | t.assert.deepStrictEqual(val, 'value') 149 | 150 | fastify.register(fastifyRedis, { 151 | client: redis, 152 | lazyConnect: false 153 | }) 154 | 155 | await fastify.ready() 156 | 157 | t.assert.deepStrictEqual(fastify.redis, redis) 158 | 159 | await fastify.redis.set('key2', 'value2') 160 | const val2 = await fastify.redis.get('key2') 161 | t.assert.deepStrictEqual(val2, 'value2') 162 | 163 | await fastify.close() 164 | await fastify.redis.quit() 165 | }) 166 | 167 | test('If closeClient is enabled, close the client.', async (t) => { 168 | t.plan(4) 169 | const fastify = Fastify() 170 | const Redis = require('ioredis') 171 | const redis = new Redis({ host: 'localhost', port: 6379 }) 172 | 173 | await redis.set('key', 'value') 174 | const val = await redis.get('key') 175 | t.assert.deepStrictEqual(val, 'value') 176 | 177 | fastify.register(fastifyRedis, { 178 | client: redis, 179 | closeClient: true 180 | }) 181 | 182 | await fastify.ready() 183 | 184 | t.assert.deepStrictEqual(fastify.redis, redis) 185 | 186 | await fastify.redis.set('key2', 'value2') 187 | const val2 = await fastify.redis.get('key2') 188 | t.assert.deepStrictEqual(val2, 'value2') 189 | 190 | const originalQuit = fastify.redis.quit 191 | fastify.redis.quit = (callback) => { 192 | t.assert.ok('redis client closed') 193 | originalQuit.call(fastify.redis, callback) 194 | } 195 | 196 | await fastify.close() 197 | }) 198 | 199 | test('If closeClient is enabled, close the client namespace.', async (t) => { 200 | t.plan(4) 201 | const fastify = Fastify() 202 | const Redis = require('ioredis') 203 | const redis = new Redis({ host: 'localhost', port: 6379 }) 204 | 205 | await redis.set('key', 'value') 206 | const val = await redis.get('key') 207 | t.assert.deepStrictEqual(val, 'value') 208 | 209 | fastify.register(fastifyRedis, { 210 | client: redis, 211 | namespace: 'foo', 212 | closeClient: true 213 | }) 214 | 215 | await fastify.ready() 216 | 217 | t.assert.deepStrictEqual(fastify.redis.foo, redis) 218 | 219 | await fastify.redis.foo.set('key2', 'value2') 220 | const val2 = await fastify.redis.foo.get('key2') 221 | t.assert.deepStrictEqual(val2, 'value2') 222 | 223 | const originalQuit = fastify.redis.foo.quit 224 | fastify.redis.foo.quit = (callback) => { 225 | t.assert.ok('redis client closed') 226 | originalQuit.call(fastify.redis.foo, callback) 227 | } 228 | 229 | await fastify.close() 230 | }) 231 | 232 | test('fastify.redis.test should throw with duplicate connection namespaces', async (t) => { 233 | t.plan(1) 234 | 235 | const namespace = 'test' 236 | 237 | const fastify = Fastify() 238 | t.after(() => fastify.close()) 239 | 240 | fastify 241 | .register(fastifyRedis, { 242 | host: '127.0.0.1', 243 | namespace 244 | }) 245 | .register(fastifyRedis, { 246 | host: '127.0.0.1', 247 | namespace 248 | }) 249 | 250 | await t.assert.rejects(fastify.ready(), new Error(`Redis '${namespace}' instance namespace has already been registered`)) 251 | }) 252 | 253 | test('Should throw when trying to register multiple instances without giving a namespace', async (t) => { 254 | t.plan(1) 255 | 256 | const fastify = Fastify() 257 | t.after(() => fastify.close()) 258 | 259 | fastify 260 | .register(fastifyRedis, { 261 | host: '127.0.0.1' 262 | }) 263 | .register(fastifyRedis, { 264 | host: '127.0.0.1' 265 | }) 266 | 267 | await t.assert.rejects(fastify.ready(), new Error('@fastify/redis has already been registered')) 268 | }) 269 | 270 | test('Should not throw within different contexts', async (t) => { 271 | t.plan(1) 272 | 273 | const fastify = Fastify() 274 | t.after(() => fastify.close()) 275 | 276 | fastify.register(function (instance, _options, next) { 277 | instance.register(fastifyRedis, { 278 | host: '127.0.0.1' 279 | }) 280 | next() 281 | }) 282 | 283 | fastify.register(function (instance, _options, next) { 284 | instance 285 | .register(fastifyRedis, { 286 | host: '127.0.0.1', 287 | namespace: 'test1' 288 | }) 289 | .register(fastifyRedis, { 290 | host: '127.0.0.1', 291 | namespace: 'test2' 292 | }) 293 | next() 294 | }) 295 | 296 | await fastify.ready() 297 | t.assert.ok(fastify) 298 | }) 299 | 300 | // Skipped because it makes TAP crash 301 | test('Should throw when trying to connect on an invalid host', { skip: true }, async (t) => { 302 | t.plan(1) 303 | 304 | const fastify = Fastify({ pluginTimeout: 20000 }) 305 | t.after(() => fastify.close()) 306 | 307 | fastify 308 | .register(fastifyRedis, { 309 | host: 'invalid_host' 310 | }) 311 | 312 | await t.assert.rejects(fastify.ready()) 313 | }) 314 | 315 | test('Should successfully create a Redis client when registered with a `url` option and without a `client` option in a namespaced instance', async t => { 316 | t.plan(2) 317 | 318 | const fastify = Fastify() 319 | t.after(() => fastify.close()) 320 | 321 | await fastify.register(fastifyRedis, { 322 | url: 'redis://127.0.0.1', 323 | namespace: 'test' 324 | }) 325 | 326 | await fastify.ready() 327 | t.assert.ok(fastify.redis) 328 | t.assert.ok(fastify.redis.test) 329 | }) 330 | 331 | test('Should be able to register multiple namespaced @fastify/redis instances', async t => { 332 | t.plan(3) 333 | 334 | const fastify = Fastify() 335 | t.after(() => fastify.close()) 336 | 337 | await fastify.register(fastifyRedis, { 338 | url: 'redis://127.0.0.1', 339 | namespace: 'one' 340 | }) 341 | 342 | await fastify.register(fastifyRedis, { 343 | url: 'redis://127.0.0.1', 344 | namespace: 'two' 345 | }) 346 | 347 | await fastify.ready() 348 | t.assert.ok(fastify.redis) 349 | t.assert.ok(fastify.redis.one) 350 | t.assert.ok(fastify.redis.two) 351 | }) 352 | 353 | test('Should throw when @fastify/redis is initialized with an option that makes Redis throw', async (t) => { 354 | t.plan(1) 355 | 356 | const fastify = Fastify() 357 | t.after(() => fastify.close()) 358 | 359 | // This will throw a `TypeError: this.options.Connector is not a constructor` 360 | fastify.register(fastifyRedis, { 361 | Connector: 'should_fail' 362 | }) 363 | 364 | await t.assert.rejects(fastify.ready()) 365 | }) 366 | 367 | test('Should throw when @fastify/redis is initialized with a namespace and an option that makes Redis throw', async (t) => { 368 | t.plan(1) 369 | 370 | const fastify = Fastify() 371 | t.after(() => fastify.close()) 372 | 373 | // This will throw a `TypeError: this.options.Connector is not a constructor` 374 | fastify.register(fastifyRedis, { 375 | Connector: 'should_fail', 376 | namespace: 'fail' 377 | }) 378 | 379 | await t.assert.rejects(fastify.ready()) 380 | }) 381 | 382 | test('catch .ping() errors', async (t) => { 383 | t.plan(1) 384 | const fastify = Fastify() 385 | t.after(() => fastify.close()) 386 | 387 | const fastifyRedis = proxyquire('..', { 388 | ioredis: function Redis () { 389 | this.ping = () => { 390 | return Promise.reject(new Redis.ReplyError('ping error')) 391 | } 392 | this.quit = () => {} 393 | this.info = cb => cb(null, 'info') 394 | this.on = function () { 395 | return this 396 | } 397 | this.off = function () { return this } 398 | 399 | return this 400 | } 401 | }) 402 | 403 | fastify.register(fastifyRedis) 404 | 405 | await t.assert.rejects(fastify.ready(), new Redis.ReplyError('ping error')) 406 | }) 407 | 408 | setInterval(() => { 409 | whyIsNodeRunning() 410 | }, 5000).unref() 411 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginCallback } from 'fastify' 2 | import { Cluster, Redis, RedisOptions } from 'ioredis' 3 | 4 | type FastifyRedisPluginType = FastifyPluginCallback 5 | 6 | declare module 'fastify' { 7 | interface FastifyInstance { 8 | redis: fastifyRedis.FastifyRedis; 9 | } 10 | } 11 | 12 | declare namespace fastifyRedis { 13 | 14 | export interface FastifyRedisNamespacedInstance { 15 | [namespace: string]: Redis; 16 | } 17 | 18 | export type FastifyRedis = FastifyRedisNamespacedInstance & Redis 19 | 20 | export type FastifyRedisPluginOptions = (RedisOptions & 21 | { 22 | url?: string; 23 | namespace?: string; 24 | }) | { 25 | client: Redis | Cluster; 26 | namespace?: string; 27 | /** 28 | * @default false 29 | */ 30 | closeClient?: boolean; 31 | } 32 | /* 33 | * @deprecated Use `FastifyRedisPluginOptions` instead 34 | */ 35 | export type FastifyRedisPlugin = FastifyRedisPluginOptions 36 | export const fastifyRedis: FastifyRedisPluginType 37 | export { fastifyRedis as default } 38 | } 39 | 40 | declare function fastifyRedis (...params: Parameters): ReturnType 41 | export = fastifyRedis 42 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import Fastify, { FastifyInstance } from 'fastify' 2 | import IORedis, { Redis } from 'ioredis' 3 | import { expectAssignable, expectDeprecated, expectError, expectType } from 'tsd' 4 | import fastifyRedis, { FastifyRedis, FastifyRedisPlugin, FastifyRedisNamespacedInstance, FastifyRedisPluginOptions } from '..' 5 | 6 | const app: FastifyInstance = Fastify() 7 | const redis: Redis = new IORedis({ host: 'localhost', port: 6379 }) 8 | const redisCluster = new IORedis.Cluster([{ host: 'localhost', port: 6379 }]) 9 | 10 | app.register(fastifyRedis, { host: '127.0.0.1' }) 11 | 12 | app.register(fastifyRedis, { 13 | client: redis, 14 | closeClient: true, 15 | namespace: 'one' 16 | }) 17 | 18 | app.register(fastifyRedis, { 19 | namespace: 'two', 20 | url: 'redis://127.0.0.1:6379' 21 | }) 22 | 23 | expectAssignable({ 24 | client: redisCluster 25 | }) 26 | 27 | expectError(app.register(fastifyRedis, { 28 | namespace: 'three', 29 | unknownOption: 'this should trigger a typescript error' 30 | })) 31 | 32 | // Plugin property available 33 | app.after(() => { 34 | expectAssignable(app.redis) 35 | expectType(app.redis) 36 | 37 | expectAssignable(app.redis) 38 | expectType(app.redis.one) 39 | expectType(app.redis.two) 40 | }) 41 | 42 | expectDeprecated({} as FastifyRedisPlugin) 43 | --------------------------------------------------------------------------------