From f40e350fdf57cb7430a3110c4e266ef7b5d81160 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 20 Jun 2023 22:25:35 +0200 Subject: [PATCH 01/71] chore: update link to main branch --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5c8d18f..7093451 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,7 +18,7 @@ _Put an `x` in the boxes that apply_ _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/redis/blob/master/.github/CONTRIBUTING.md) doc +- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/redis/blob/main/.github/CONTRIBUTING.md) doc - [ ] Lint and unit tests pass locally with my changes - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added necessary documentation (if appropriate) From 3f835b4f95bc3bc0bf3d8ea6422b74ea1ff682a0 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:11:31 +0200 Subject: [PATCH 02/71] refactor: migrate to ESM + V6 --- .env.example | 3 + .github/COMMIT_CONVENTION.md | 69 -- .github/CONTRIBUTING.md | 46 -- .github/ISSUE_TEMPLATE/bug_report.md | 29 - .github/ISSUE_TEMPLATE/feature_request.md | 28 - .github/PULL_REQUEST_TEMPLATE.md | 28 - .github/labels.json | 170 +++++ .github/stale.yml | 4 +- .github/workflows/test.yml | 5 +- .gitignore | 1 + .husky/commit-msg | 7 +- .npmignore | 12 - Dockerfile | 17 - adonis-typings/container.ts | 16 - adonis-typings/events.ts | 27 - adonis-typings/redis.ts | 258 -------- bin/japaTypes.ts | 7 - bin/test.ts | 19 +- config.ts | 37 -- docker-compose.yml | 21 +- example/index.ts | 2 - factories/main.ts | 10 + factories/redis_manager.ts | 21 + adonis-typings/index.ts => index.ts | 6 +- instructions.md | 10 - package.json | 305 ++++----- providers/RedisProvider.ts | 87 --- providers/redis_provider.ts | 58 ++ src/Bindings/Repl.ts | 32 - src/RedisManager/index.ts | 271 -------- .../index.ts => abstract_connection.ts} | 113 ++-- src/define_config.ts | 44 ++ src/{ioMethods.ts => io_methods.ts} | 4 +- src/{pubsubMethods.ts => pubsub_methods.ts} | 2 +- .../index.ts => redis_cluster_connection.ts} | 43 +- .../index.ts => redis_connection.ts} | 39 +- src/redis_manager.ts | 280 ++++++++ src/repl_bindings.ts | 30 + src/types/extended.ts | 35 + src/types/main.ts | 151 +++++ test/contracts.ts | 6 - test/define_config.spec.ts | 38 ++ test/redis-connection.spec.ts | 600 ------------------ test/redis-provider.spec.ts | 159 ----- test/redis.spec.ts | 311 --------- ...ec.ts => redis_cluster_connection.spec.ts} | 87 +-- test/redis_connection.spec.ts | 190 ++++++ test/redis_manager.spec.ts | 277 ++++++++ test/redis_provider.spec.ts | 56 ++ tsconfig.json | 11 +- 50 files changed, 1641 insertions(+), 2441 deletions(-) create mode 100644 .env.example delete mode 100644 .github/COMMIT_CONVENTION.md delete mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/labels.json delete mode 100644 .npmignore delete mode 100644 Dockerfile delete mode 100644 adonis-typings/container.ts delete mode 100644 adonis-typings/events.ts delete mode 100644 adonis-typings/redis.ts delete mode 100644 bin/japaTypes.ts delete mode 100644 config.ts delete mode 100644 example/index.ts create mode 100644 factories/main.ts create mode 100644 factories/redis_manager.ts rename adonis-typings/index.ts => index.ts (50%) delete mode 100644 instructions.md delete mode 100644 providers/RedisProvider.ts create mode 100644 providers/redis_provider.ts delete mode 100644 src/Bindings/Repl.ts delete mode 100644 src/RedisManager/index.ts rename src/{AbstractConnection/index.ts => abstract_connection.ts} (77%) create mode 100644 src/define_config.ts rename src/{ioMethods.ts => io_methods.ts} (98%) rename src/{pubsubMethods.ts => pubsub_methods.ts} (88%) rename src/{RedisClusterConnection/index.ts => redis_cluster_connection.ts} (50%) rename src/{RedisConnection/index.ts => redis_connection.ts} (53%) create mode 100644 src/redis_manager.ts create mode 100644 src/repl_bindings.ts create mode 100644 src/types/extended.ts create mode 100644 src/types/main.ts delete mode 100644 test/contracts.ts create mode 100644 test/define_config.spec.ts delete mode 100644 test/redis-connection.spec.ts delete mode 100644 test/redis-provider.spec.ts delete mode 100644 test/redis.spec.ts rename test/{redis-cluster-factory.spec.ts => redis_cluster_connection.spec.ts} (74%) create mode 100644 test/redis_connection.spec.ts create mode 100644 test/redis_manager.spec.ts create mode 100644 test/redis_provider.spec.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6fbbc64 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +REDIS_HOST= +REDIS_PORT= +REDIS_CLUSTER_PORTS= diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md deleted file mode 100644 index f6ea85e..0000000 --- a/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,69 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). - -Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. - -``` js -/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ -``` - -## Commit Message Format -A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: - -> The **scope** is optional - -``` -feat(router): add support for prefix - -Prefix makes it easier to append a path to a group of routes -``` - -1. `feat` is type. -2. `router` is scope and is optional -3. `add support for prefix` is the subject -4. The **body** is followed by a blank line. -5. The optional **footer** can be added after the body, followed by a blank line. - -## Types -Only one type can be used at a time and only following types are allowed. - -- feat -- fix -- docs -- style -- refactor -- perf -- test -- workflow -- ci -- chore -- types -- build - -If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. - -### Revert -If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. - -## Scope -The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. - -## Subject -The subject contains succinct description of the change: - -- use the imperative, present tense: "change" not "changed" nor "changes". -- don't capitalize first letter -- no dot (.) at the end - -## Body - -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -## Footer - -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f0c5446..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing - -AdonisJS is a community driven project. You are free to contribute in any of the following ways. - -- [Coding style](coding-style) -- [Fix bugs by creating PR's](fix-bugs-by-creating-prs) -- [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) -- [Report security issues](report-security-issues) -- [Be a part of the community](be-a-part-of-community) - -## Coding style - -Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. - -## Fix bugs by creating PR's - -We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. - -Go through the following points, before creating a new PR. - -1. Create an issue discussing the bug or short-coming in the framework. -2. Once approved, go ahead and fork the REPO. -3. Make sure to start from the `develop`, since this is the upto date branch. -4. Make sure to keep commits small and relevant. -5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. -6. Once done with all the changes, create a PR against the `develop` branch. - -## Share an RFC for new features or big changes - -Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. - -### What is an RFC? - -RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). - -In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. - -The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. - -## Report security issues - -All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. - -## Be a part of community - -We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e65000c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report identified bugs ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -- Lots of raised issues are directly not bugs but instead are design decisions taken by us. -- Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. -- Ensure the issue isn't already reported. -- Ensure you are reporting the bug in the correct repo. - -*Delete the above section and the instructions in the sections below before submitting* - -## Package version - - -## Node.js and npm version - - -## Sample Code (to reproduce the issue) - - -## BONUS (a sample repo to reproduce the issue) - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index abd44a5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Propose changes for adding a new feature ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -## Consider an RFC - -Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if - -- Feature introduces a breaking change -- Demands lots of time and changes in the current code base. - -*Delete the above section and the instructions in the sections below before submitting* - -## Why this feature is required (specific use-cases will be appreciated)? - - -## Have you tried any other work arounds? - - -## Are you willing to work on it with little guidance? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 5c8d18f..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. - -## Types of changes - -What types of changes does your code introduce? - -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/redis/blob/master/.github/CONTRIBUTING.md) doc -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] I have added necessary documentation (if appropriate) - -## Further comments - -If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..ba001c6 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,170 @@ +[ + { + "name": "Priority: Critical", + "color": "ea0056", + "description": "The issue needs urgent attention", + "aliases": [] + }, + { + "name": "Priority: High", + "color": "5666ed", + "description": "Look into this issue before picking up any new work", + "aliases": [] + }, + { + "name": "Priority: Medium", + "color": "f4ff61", + "description": "Try to fix the issue for the next patch/minor release", + "aliases": [] + }, + { + "name": "Priority: Low", + "color": "87dfd6", + "description": "Something worth considering, but not a top priority for the team", + "aliases": [] + }, + { + "name": "Semver: Alpha", + "color": "008480", + "description": "Will make it's way to the next alpha version of the package", + "aliases": [] + }, + { + "name": "Semver: Major", + "color": "ea0056", + "description": "Has breaking changes", + "aliases": [] + }, + { + "name": "Semver: Minor", + "color": "fbe555", + "description": "Mainly new features and improvements", + "aliases": [] + }, + { + "name": "Semver: Next", + "color": "5666ed", + "description": "Will make it's way to the bleeding edge version of the package", + "aliases": [] + }, + { + "name": "Semver: Patch", + "color": "87dfd6", + "description": "A bug fix", + "aliases": [] + }, + { + "name": "Status: Abandoned", + "color": "ffffff", + "description": "Dropped and not into consideration", + "aliases": ["wontfix"] + }, + { + "name": "Status: Accepted", + "color": "e5fbf2", + "description": "The proposal or the feature has been accepted for the future versions", + "aliases": [] + }, + { + "name": "Status: Blocked", + "color": "ea0056", + "description": "The work on the issue or the PR is blocked. Check comments for reasoning", + "aliases": [] + }, + { + "name": "Status: Completed", + "color": "008672", + "description": "The work has been completed, but not released yet", + "aliases": [] + }, + { + "name": "Status: In Progress", + "color": "73dbc4", + "description": "Still banging the keyboard", + "aliases": ["in progress"] + }, + { + "name": "Status: On Hold", + "color": "f4ff61", + "description": "The work was started earlier, but is on hold now. Check comments for reasoning", + "aliases": ["On Hold"] + }, + { + "name": "Status: Review Needed", + "color": "fbe555", + "description": "Review from the core team is required before moving forward", + "aliases": [] + }, + { + "name": "Status: Awaiting More Information", + "color": "89f8ce", + "description": "Waiting on the issue reporter or PR author to provide more information", + "aliases": [] + }, + { + "name": "Status: Need Contributors", + "color": "7057ff", + "description": "Looking for contributors to help us move forward with this issue or PR", + "aliases": [] + }, + { + "name": "Type: Bug", + "color": "ea0056", + "description": "The issue has indentified a bug", + "aliases": ["bug"] + }, + { + "name": "Type: Security", + "color": "ea0056", + "description": "Spotted security vulnerability and is a top priority for the core team", + "aliases": [] + }, + { + "name": "Type: Duplicate", + "color": "00837e", + "description": "Already answered or fixed previously", + "aliases": ["duplicate"] + }, + { + "name": "Type: Enhancement", + "color": "89f8ce", + "description": "Improving an existing feature", + "aliases": ["enhancement"] + }, + { + "name": "Type: Feature Request", + "color": "483add", + "description": "Request to add a new feature to the package", + "aliases": [] + }, + { + "name": "Type: Invalid", + "color": "dbdbdb", + "description": "Doesn't really belong here. Maybe use discussion threads?", + "aliases": ["invalid"] + }, + { + "name": "Type: Question", + "color": "eceafc", + "description": "Needs clarification", + "aliases": ["help wanted", "question"] + }, + { + "name": "Type: Documentation Change", + "color": "7057ff", + "description": "Documentation needs some improvements", + "aliases": ["documentation"] + }, + { + "name": "Type: Dependencies Update", + "color": "00837e", + "description": "Bump dependencies", + "aliases": ["dependencies"] + }, + { + "name": "Good First Issue", + "color": "008480", + "description": "Want to contribute? Just filter by this label", + "aliases": ["good first issue"] + } +] diff --git a/.github/stale.yml b/.github/stale.yml index 7a6a571..f767674 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,10 +6,10 @@ daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "Type: Security" + - 'Type: Security' # Label to use when marking an issue as stale -staleLabel: "Status: Abandoned" +staleLabel: 'Status: Abandoned' # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b778ca..de4467a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,15 +8,18 @@ jobs: strategy: matrix: node-version: - - 14.15.4 - 17.x + - 20.x steps: - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - name: Install run: npm install + - name: Run tests run: npm test diff --git a/.gitignore b/.gitignore index 8cdf144..dc0a88c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ package-lock.json yarn.lock shrinkwrap.yaml test/__app +.env diff --git a/.husky/commit-msg b/.husky/commit-msg index 4654c12..988eb59 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,3 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 225554a..0000000 --- a/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -coverage -node_modules -.DS_Store -npm-debug.log -test -.travis.yml -.editorconfig -benchmarks -.idea -bin -out -.nyc_output diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 38c5e38..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM node:14-alpine as build-deps - -RUN apk update && apk upgrade && \ - apk add --update git && \ - apk add --update openssh && \ - apk add --update bash && \ - apk add --update wget - -ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait /wait -RUN chmod +x /wait - -WORKDIR /usr/src/app - -COPY package*.json ./ -RUN npm install - -COPY . . diff --git a/adonis-typings/container.ts b/adonis-typings/container.ts deleted file mode 100644 index 87323c1..0000000 --- a/adonis-typings/container.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Application' { - import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' - - export interface ContainerBindings { - 'Adonis/Addons/Redis': RedisManagerContract - } -} diff --git a/adonis-typings/events.ts b/adonis-typings/events.ts deleted file mode 100644 index 9c7ee3d..0000000 --- a/adonis-typings/events.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Event' { - import { Redis } from 'ioredis' - import { RedisClusterConnectionContract, RedisConnectionContract } from '@ioc:Adonis/Addons/Redis' - - interface EventsList { - 'redis:ready': { connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:connect': { connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:error': { - error: any - connection: RedisClusterConnectionContract | RedisConnectionContract - } - 'redis:end': { connection: RedisClusterConnectionContract | RedisConnectionContract } - - 'node:added': { connection: RedisClusterConnectionContract; node: Redis } - 'node:removed': { connection: RedisClusterConnectionContract; node: Redis } - 'node:error': { error: any; connection: RedisClusterConnectionContract; address: string } - } -} diff --git a/adonis-typings/redis.ts b/adonis-typings/redis.ts deleted file mode 100644 index 35c4eb1..0000000 --- a/adonis-typings/redis.ts +++ /dev/null @@ -1,258 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Addons/Redis' { - import { EventEmitter } from 'events' - import { HealthReportEntry } from '@ioc:Adonis/Core/HealthCheck' - import { Redis as IoRedis, RedisOptions, ClusterOptions, Cluster, NodeRole } from 'ioredis' - - /* - |-------------------------------------------------------------------------- - | Helpers - |-------------------------------------------------------------------------- - */ - /** - * Returns factory for a given connection by inspecting it's config. - */ - type GetConnectionFactoryType = - RedisConnectionsList[T] extends RedisClusterConfig - ? RedisClusterConnectionContract - : RedisConnectionContract - - /* - |-------------------------------------------------------------------------- - | PubSub - |-------------------------------------------------------------------------- - */ - /** - * Pubsub subscriber - */ - export type PubSubChannelHandler = (data: T) => Promise | void - export type PubSubPatternHandler = ( - channel: string, - data: T - ) => Promise | void - - /** - * Redis pub/sub methods - */ - export interface RedisPubSubContract { - publish( - channel: string, - message: string, - callback: (error: Error | null, count: number) => void - ): void - publish(channel: string, message: string): Promise - subscribe(channel: string, handler: PubSubChannelHandler | string): void - psubscribe(pattern: string, handler: PubSubPatternHandler | string): void - unsubscribe(channel: string): void - punsubscribe(pattern: string): void - } - - /** - * Shape of the report node for the redis connection report - */ - export type HealthReportNode = { - connection: string - status: string - used_memory: string | null - error: any - } - - /** - * List of commands on the IORedis. We omit their internal events and pub/sub - * handlers, since we our own. - */ - export type IORedisCommands = Omit< - IoRedis, - | 'Promise' - | 'status' - | 'connect' - | 'disconnect' - | 'duplicate' - | 'subscribe' - | 'unsubscribe' - | 'psubscribe' - | 'punsubscribe' - | 'quit' - | 'publish' - | keyof EventEmitter - > - - /* - |-------------------------------------------------------------------------- - | Redis Connections - |-------------------------------------------------------------------------- - */ - /** - * Standard Redis Connection - */ - export interface RedisConnectionContract - extends IORedisCommands, - RedisPubSubContract, - EventEmitter { - status: string - connectionName: string - subscriberStatus?: string - ioConnection: IoRedis - ioSubscriberConnection?: IoRedis - - connect(callback?: () => void): Promise - disconnect(): Promise - getReport(checkForMemory?: boolean): Promise - quit(): Promise - runCommand(command: string, ...args: any[]): Promise | any - } - - /** - * Redis cluster factory interface - */ - export interface RedisClusterConnectionContract - extends IORedisCommands, - RedisPubSubContract, - EventEmitter { - status: string - connectionName: string - subscriberStatus?: string - ioConnection: Cluster - ioSubscriberConnection?: Cluster - - getReport(checkForMemory?: boolean): Promise - connect(callback?: () => void): Promise - nodes(role?: NodeRole): IoRedis[] - disconnect(): Promise - quit(): Promise - runCommand(command: string, ...args: any[]): Promise | any - } - - type Connection = RedisClusterConnectionContract | RedisConnectionContract - - /** - * Redis manager exposes the API to intertact with a redis server. One can make - * use of multiple redis connections by defining them inside `config/redis` - * file. - * - * ```ts - * Redis.connection() // default connection - * Redis.connection('primary') // named connection - * ``` - */ - export interface RedisBaseManagerContract { - /** - * A boolean to know whether health checks have been enabled on one - * or more redis connections or not. - */ - healthChecksEnabled: boolean - - /** - * Number of active redis connection. - */ - activeConnectionsCount: number - activeConnections: { [key: string]: Connection } - - /** - * Fetch a named connection from the defined config inside config/redis file - */ - connection( - name: ConnectionName - ): GetConnectionFactoryType - - /** - * Untyped connection - */ - connection(name: string): RedisConnectionContract | RedisClusterConnectionContract - - /** - * Returns the default connection client - */ - connection(): RedisConnectionContract | RedisClusterConnectionContract - - /** - * Returns the healthcheck report - */ - report(): Promise - - /** - * Quit a named connection. - */ - quit(name?: ConnectionName): Promise - quit(name?: string): Promise - - /** - * Forcefully disconnect a named connection. - */ - disconnect( - name?: ConnectionName - ): Promise - disconnect(name?: string): Promise - - /** - * Quit all redis connections - */ - quitAll(): Promise - - /** - * Disconnect all redis connections - */ - disconnectAll(): Promise - - /** - * Run a given custom command - */ - runCommand(command: string, ...args: any[]): Promise | any - } - - /* - |-------------------------------------------------------------------------- - | Config - |-------------------------------------------------------------------------- - */ - /** - * Shape of standard Redis connection config - */ - export type RedisConnectionConfig = Omit & { - healthCheck?: boolean - port?: string | number - } - - /** - * Shape of cluster config - */ - export type RedisClusterConfig = { - clusters: { host: string; port: number | string }[] - clusterOptions?: ClusterOptions - healthCheck?: boolean - } - - /** - * A list of typed connections defined in the user land using - * the contracts file - */ - export interface RedisConnectionsList {} - - /** - * Define the config properties on this interface and they will appear - * everywhere. - */ - export interface RedisConfig { - connection: keyof RedisConnectionsList - connections: { [P in keyof RedisConnectionsList]: RedisConnectionsList[P] } - } - - /** - * Redis manager proxies all IO methods on the connection - */ - export interface RedisManagerContract - extends RedisBaseManagerContract, - IORedisCommands, - RedisPubSubContract {} - - const Redis: RedisManagerContract - export default Redis -} diff --git a/bin/japaTypes.ts b/bin/japaTypes.ts deleted file mode 100644 index d42cac6..0000000 --- a/bin/japaTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Assert } from '@japa/assert' - -declare module '@japa/runner' { - interface TestContext { - assert: Assert - } -} diff --git a/bin/test.ts b/bin/test.ts index 43dd92b..71d7fc4 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,7 +1,8 @@ +import 'dotenv/config' +import { processCLIArgs, configure, run } from '@japa/runner' import { assert } from '@japa/assert' -import { specReporter } from '@japa/spec-reporter' -import { runFailedTests } from '@japa/run-failed-tests' -import { processCliArgs, configure, run } from '@japa/runner' +import { fileSystem } from '@japa/file-system' +import { expectTypeOf } from '@japa/expect-type' /* |-------------------------------------------------------------------------- @@ -16,15 +17,11 @@ import { processCliArgs, configure, run } from '@japa/runner' | | Please consult japa.dev/runner-config for the config docs. */ +processCLIArgs(process.argv.slice(2)) configure({ - ...processCliArgs(process.argv.slice(2)), - ...{ - files: ['test/**/*.spec.ts'], - plugins: [assert(), runFailedTests()], - reporters: [specReporter()], - importer: (filePath: string) => import(filePath), - forceExit: true, - }, + files: ['test/**/*.spec.ts'], + plugins: [assert(), fileSystem(), expectTypeOf()], + forceExit: true, }) /* diff --git a/config.ts b/config.ts deleted file mode 100644 index 4719347..0000000 --- a/config.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RedisConnectionConfig, RedisClusterConfig } from '@ioc:Adonis/Addons/Redis' - -/** - * Expected shape of the config accepted by the "redisConfig" - * method - */ -type RedisConfig = { - connections: { - [name: string]: RedisConnectionConfig | RedisClusterConfig - } -} - -/** - * Define config for redis - */ -export function redisConfig( - config: T -): T { - return config -} - -/** - * Pull connections from the config defined inside the "config/redis.ts" - * file - */ -export type InferConnectionsFromConfig = { - [K in keyof T['connections']]: T['connections'][K] -} diff --git a/docker-compose.yml b/docker-compose.yml index 830faa8..963804c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,14 @@ version: '3.4' services: - tests: - image: adonis-redis - network_mode: host - build: - context: . - environment: - REDIS_HOST: 0.0.0.0 - REDIS_PORT: 7007 - REDIS_CLUSTER_PORTS: '7000,7001,7002' - WAIT_HOSTS: 0.0.0.0:7000, 0.0.0.0:7001, 0.0.0.0:7002, 0.0.0.0:7003, 0.0.0.0:7004, 0.0.0.0:7005, 0.0.0.0:7006, 0.0.0.0:7007 - depends_on: - - redis - command: sh -c "/wait && npm run test:docker" redis: - network_mode: host image: grokzen/redis-cluster + ports: + - "7000:7000" + - "7001:7001" + - "7002:7002" + - "7003:7003" + - "7004:7004" + - "7005:7005" environment: REDIS_CLUSTER_IP: 0.0.0.0 IP: 0.0.0.0 diff --git a/example/index.ts b/example/index.ts deleted file mode 100644 index 491e526..0000000 --- a/example/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import Redis from '@ioc:Adonis/Addons/Redis' -Redis.connection().get('foo') diff --git a/factories/main.ts b/factories/main.ts new file mode 100644 index 0000000..5b3fc70 --- /dev/null +++ b/factories/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { RedisManagerFactory } from './redis_manager.js' diff --git a/factories/redis_manager.ts b/factories/redis_manager.ts new file mode 100644 index 0000000..cdd436b --- /dev/null +++ b/factories/redis_manager.ts @@ -0,0 +1,21 @@ +import { EmitterFactory } from '@adonisjs/core/factories/events' +import { Application } from '@adonisjs/core/app' +import RedisManager from '../src/redis_manager.js' +import { RedisClusterConfig, RedisConnectionConfig } from '../src/types/main.js' + +export class RedisManagerFactory< + ConnectionsList extends Record, +> { + #config: { + connection: keyof ConnectionsList + connections: ConnectionsList + } + + constructor(config: { connection: keyof ConnectionsList; connections: ConnectionsList }) { + this.#config = config + } + + create(app: Application) { + return new RedisManager(app, this.#config, new EmitterFactory().create(app)) + } +} diff --git a/adonis-typings/index.ts b/index.ts similarity index 50% rename from adonis-typings/index.ts rename to index.ts index d25bcb0..3b91c2c 100644 --- a/adonis-typings/index.ts +++ b/index.ts @@ -1,12 +1,10 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// -/// -/// +import './src/types/extended.js' diff --git a/instructions.md b/instructions.md deleted file mode 100644 index c458ccc..0000000 --- a/instructions.md +++ /dev/null @@ -1,10 +0,0 @@ -The package has been configured successfully. The redis configuration stored inside `config/redis.ts` file relies on the following environment variables and hence we recommend validating them. - -Open the `env.ts` file and paste the following code inside the `Env.rules` object. - -```ts -REDIS_CONNECTION: Env.schema.enum(['local'] as const), -REDIS_HOST: Env.schema.string({ format: 'host' }), -REDIS_PORT: Env.schema.number(), -REDIS_PASSWORD: Env.schema.string.optional(), -``` diff --git a/package.json b/package.json index 8eecdbe..a7d2e35 100644 --- a/package.json +++ b/package.json @@ -1,187 +1,122 @@ { - "name": "@adonisjs/redis", - "version": "7.3.2", - "description": "AdonisJS addon for Redis", - "main": "build/providers/RedisProvider.js", - "exports": { - ".": { - "types": "./build/adonis-typings/index.d.ts", - "require": "./build/providers/RedisProvider.js" - }, - "./config": "./build/config.js", - "./build/config": "./build/config.js", - "./*": "./*" - }, - "files": [ - "build/adonis-typings", - "build/providers", - "build/src", - "build/config.js", - "build/config.d.ts", - "build/templates", - "build/instructions.md" - ], - "types": "build/adonis-typings/index.d.ts", - "scripts": { - "mrm": "mrm --preset=@adonisjs/mrm-preset", - "pretest": "npm run lint", - "test": "docker-compose build && docker-compose run --rm tests", - "clean": "del-cli build", - "test:docker": "FORCE_COLOR=true node -r @adonisjs/require-ts/build/register ./bin/test.ts", - "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", - "compile": "npm run lint && npm run clean && tsc", - "build": "npm run compile && npm run copyfiles", - "commit": "git-cz", - "release": "np --message=\"chore(release): %s\"", - "version": "npm run build", - "prepublishOnly": "npm run build", - "lint": "eslint . --ext=.ts", - "format": "prettier --write .", - "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/redis" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/adonisjs/redis.git" - }, - "keywords": [ - "redis", - "ioredis" - ], - "author": "virk,adonisjs", - "license": "MIT", - "bugs": { - "url": "https://github.com/adonisjs/redis/issues" - }, - "homepage": "https://github.com/adonisjs/redis#readme", - "publishConfig": { - "access": "public", - "tag": "latest" - }, - "devDependencies": { - "@adonisjs/core": "^5.8.6", - "@adonisjs/mrm-preset": "^5.0.3", - "@adonisjs/repl": "^3.1.11", - "@adonisjs/require-ts": "^2.0.12", - "@japa/assert": "^1.3.5", - "@japa/run-failed-tests": "^1.0.8", - "@japa/runner": "^2.1.1", - "@japa/spec-reporter": "^1.2.0", - "@poppinss/dev-utils": "^2.0.3", - "@types/node": "^18.7.15", - "commitizen": "^4.2.5", - "copyfiles": "^2.4.1", - "cz-conventional-changelog": "^3.3.0", - "del-cli": "^5.0.0", - "eslint": "^8.23.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-adonis": "^2.1.0", - "eslint-plugin-prettier": "^4.2.1", - "github-label-sync": "^2.2.0", - "husky": "^8.0.1", - "mrm": "^4.1.0", - "np": "^7.6.2", - "prettier": "^2.7.1", - "typescript": "^4.8.2" - }, - "peerDependencies": { - "@adonisjs/core": "^5.1.0" - }, - "dependencies": { - "@poppinss/utils": "^5.0.0", - "@types/ioredis": "^4.28.10", - "ioredis": "^5.2.3" - }, - "nyc": { - "exclude": [ - "test" - ], - "extension": [ - ".ts" - ] - }, - "husky": { - "hooks": { - "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" - } - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "np": { - "contents": ".", - "anyBranch": false, - "yolo": true - }, - "adonisjs": { - "instructionsMd": "./build/instructions.md", - "templates": { - "config": [ - { - "src": "config.txt", - "dest": "redis" - } - ], - "contracts": [ - { - "src": "contract.txt", - "dest": "redis" - } - ] - }, - "types": "@adonisjs/redis", - "providers": [ - "@adonisjs/redis" - ], - "env": { - "REDIS_CONNECTION": "local", - "REDIS_HOST": "127.0.0.1", - "REDIS_PORT": "6379", - "REDIS_PASSWORD": "" - } - }, - "mrmConfig": { - "core": true, - "license": "MIT", - "services": [ - "github-actions" - ], - "minNodeVersion": "14.15.4", - "probotApps": [ - "stale", - "lock" - ], - "runGhActionsOnWindows": false - }, - "eslintConfig": { - "extends": [ - "plugin:adonis/typescriptPackage", - "prettier" - ], - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ] - } - }, - "eslintIgnore": [ - "build" - ], - "prettier": { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "useTabs": false, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100 - } + "name": "@adonisjs/redis", + "description": "AdonisJS addon for Redis", + "version": "7.3.2", + "main": "build/index.js", + "type": "module", + "files": [ + "index.ts", + "configure.ts", + "src", + "services", + "providers", + "factories", + "build/configure.js", + "build/configure.d.ts", + "build/configure.d.ts.map", + "build/index.js", + "build/index.d.ts", + "build/index.d.ts.map", + "build/src", + "build/services", + "build/providers", + "build/factories", + "build/stubs", + "build/index.d.ts", + "build/index.d.ts.map", + "build/index.js" + ], + "exports": { + ".": "./build/index.js", + "./services/main": "./build/services/main.js", + "./factories": "./build/factories/main.js", + "./types": "./build/src/types/main.js" + }, + "scripts": { + "pretest": "npm run lint", + "test": "docker-compose build && docker-compose run --rm tests", + "quick:test": "node --experimental-import-meta-resolve --enable-source-maps --loader=ts-node/esm ./bin/test.ts", + "clean": "del-cli build", + "test:docker": "FORCE_COLOR=true node --loader=ts-node/esm ./bin/test.ts", + "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", + "compile": "npm run lint && npm run clean && tsc", + "build": "npm run compile && npm run copyfiles", + "release": "np --message=\"chore(release): %s\"", + "version": "npm run build", + "prepublishOnly": "npm run build", + "lint": "eslint . --ext=.ts", + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/logger" + }, + "devDependencies": { + "@adonisjs/core": "^6.1.5-8", + "@adonisjs/eslint-config": "^1.1.2", + "@adonisjs/prettier-config": "^1.1.2", + "@adonisjs/tsconfig": "^1.1.2", + "@japa/assert": "2.0.0-1", + "@japa/expect-type": "2.0.0-0", + "@japa/file-system": "2.0.0-1", + "@japa/runner": "3.0.0-6", + "@poppinss/dev-utils": "^2.0.3", + "@swc/core": "^1.3.64", + "@types/node": "^20.3.1", + "copyfiles": "^2.4.1", + "del-cli": "^5.0.0", + "dotenv": "^16.3.0", + "eslint": "^8.43.0", + "github-label-sync": "^2.3.1", + "husky": "^8.0.1", + "np": "^7.6.2", + "prettier": "^2.8.8", + "ts-node": "^10.9.1", + "typescript": "^5.1.3" + }, + "dependencies": { + "@poppinss/utils": "6.5.0-3", + "ioredis": "^5.3.2" + }, + "peerDependencies": { + "@adonisjs/core": "^6.1.5-8" + }, + "author": "virk,adonisjs", + "license": "MIT", + "homepage": "https://github.com/adonisjs/redis#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/adonisjs/redis.git" + }, + "bugs": { + "url": "https://github.com/adonisjs/redis/issues" + }, + "keywords": [ + "redis", + "ioredis" + ], + "eslintConfig": { + "extends": "@adonisjs/eslint-config/typescript-package" + }, + "prettier": "@adonisjs/prettier-config", + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "publishConfig": { + "access": "public", + "tag": "next" + }, + "np": { + "message": "chore(release): %s", + "tag": "next", + "branch": "main", + "anyBranch": false + }, + "c8": { + "reporter": [ + "text", + "html" + ], + "exclude": [ + "tests/**" + ] + } } diff --git a/providers/RedisProvider.ts b/providers/RedisProvider.ts deleted file mode 100644 index 7d653c4..0000000 --- a/providers/RedisProvider.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -/** - * Provider to bind redis to the container - */ -export default class RedisProvider { - constructor(protected app: ApplicationContract) {} - public static needsApplication = true - - /** - * Register redis health check - */ - protected registerHealthCheck() { - /** - * Do not register healthcheck when not running in web - * or test mode - */ - if (!['web', 'test'].includes(this.app.environment)) { - return - } - - this.app.container.withBindings( - ['Adonis/Core/HealthCheck', 'Adonis/Addons/Redis'], - (HealthCheck, Redis) => { - if (Redis.healthChecksEnabled) { - HealthCheck.addChecker('redis', 'Adonis/Addons/Redis') - } - } - ) - } - - /** - * Define repl bindings - */ - protected defineReplBindings() { - /** - * Do not register repl bindings when not running in "repl" - * environment - */ - if (this.app.environment !== 'repl') { - return - } - - this.app.container.withBindings(['Adonis/Addons/Repl'], (Repl) => { - const { defineReplBindings } = require('../src/Bindings/Repl') - defineReplBindings(this.app, Repl) - }) - } - - /** - * Register the redis binding - */ - public register() { - this.app.container.singleton('Adonis/Addons/Redis', () => { - const config = this.app.container.resolveBinding('Adonis/Core/Config').get('redis', {}) - const emitter = this.app.container.resolveBinding('Adonis/Core/Event') - const { RedisManager } = require('../src/RedisManager') - - return new RedisManager(this.app, config, emitter) - }) - } - - /** - * Registering the health check checker with HealthCheck service - */ - public boot() { - this.registerHealthCheck() - this.defineReplBindings() - } - - /** - * Gracefully shutdown connections when app goes down - */ - public async shutdown() { - const Redis = this.app.container.resolveBinding('Adonis/Addons/Redis') - await Redis.quitAll() - } -} diff --git a/providers/redis_provider.ts b/providers/redis_provider.ts new file mode 100644 index 0000000..ec12b9e --- /dev/null +++ b/providers/redis_provider.ts @@ -0,0 +1,58 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ApplicationService } from '@adonisjs/core/types' +import { defineReplBindings } from '../src/repl_bindings.js' + +/** + * Provider to bind redis to the container + */ +export default class RedisProvider { + constructor(protected app: ApplicationService) {} + + /** + * Define repl bindings + */ + async #defineReplBindings() { + if (this.app.getEnvironment() !== 'repl') { + return + } + + defineReplBindings(this.app, await this.app.container.make('repl')) + } + + /** + * Register the redis binding + */ + register() { + this.app.container.singleton('redis', async () => { + const { default: RedisManager } = await import('../src/redis_manager.js') + + const emitter = await this.app.container.make('emitter') + const config = this.app.config.get('redis', {}) + + return new RedisManager(this.app, config, emitter) + }) + } + + /** + * Registering the health check checker with HealthCheck service + */ + boot() { + this.#defineReplBindings() + } + + /** + * Gracefully shutdown connections when app goes down + */ + async shutdown() { + const redis = await this.app.container.make('redis') + await redis.quitAll() + } +} diff --git a/src/Bindings/Repl.ts b/src/Bindings/Repl.ts deleted file mode 100644 index f1358ed..0000000 --- a/src/Bindings/Repl.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ReplContract } from '@ioc:Adonis/Addons/Repl' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -/** - * Defune repl bindings. The method must be invoked when application environment - * is set to repl. - */ -export function defineReplBindings(application: ApplicationContract, Repl: ReplContract) { - Repl.addMethod( - 'loadRedis', - (repl) => { - repl.server.context.Redis = application.container.use('Adonis/Addons/Redis') - repl.notify( - `Loaded Redis module. You can access it using the "${repl.colors.underline( - 'Redis' - )}" variable` - ) - }, - { - description: 'Load redis provider and save reference to the "Redis" variable', - } - ) -} diff --git a/src/RedisManager/index.ts b/src/RedisManager/index.ts deleted file mode 100644 index 5c65812..0000000 --- a/src/RedisManager/index.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { EmitterContract } from '@ioc:Adonis/Core/Event' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { Exception, ManagerConfigValidator } from '@poppinss/utils' - -import { - RedisConfig, - HealthReportNode, - RedisBaseManagerContract, - RedisConnectionContract, - RedisClusterConnectionContract, -} from '@ioc:Adonis/Addons/Redis' - -import { ioMethods } from '../ioMethods' -import { pubsubMethods } from '../pubsubMethods' -import { RedisConnection } from '../RedisConnection' -import { RedisClusterConnection } from '../RedisClusterConnection' - -/** - * Redis manager exposes the API to interact with a redis server. - */ -export class RedisManager implements RedisBaseManagerContract { - /** - * An array of connections with health checks enabled, which means, we always - * create a connection for them, even when they are not used. - */ - private healthCheckConnections: string[] = [] - - /** - * A copy of live connections. We avoid re-creating a new connection - * everytime and re-use connections. - */ - public activeConnections: { - [key: string]: RedisClusterConnectionContract | RedisConnectionContract - } = {} - - /** - * A boolean to know whether health checks have been enabled on one - * or more redis connections or not. - */ - public get healthChecksEnabled() { - return this.healthCheckConnections.length > 0 - } - - /** - * Returns the length of active connections - */ - public get activeConnectionsCount() { - return Object.keys(this.activeConnections).length - } - - constructor( - private application: ApplicationContract, - private config: RedisConfig, - private emitter: EmitterContract - ) { - this.validateConfig() - this.healthCheckConnections = Object.keys(this.config.connections).filter( - (connection) => this.config.connections[connection].healthCheck - ) - } - - /** - * Validate config at runtime - */ - private validateConfig() { - const validator = new ManagerConfigValidator(this.config, 'redis', 'config/redis') - validator.validateDefault('connection') - validator.validateList('connections', 'connection') - } - - /** - * Returns default connnection name - */ - private getDefaultConnection(): string { - return this.config.connection - } - - /** - * Returns an existing connection using it's name or the - * default connection, - */ - private getExistingConnection(name?: string) { - name = name || this.getDefaultConnection() - return this.activeConnections[name] - } - - /** - * Returns config for a given connection - */ - private getConnectionConfig(name: string) { - return this.config.connections[name] - } - - /** - * Returns redis factory for a given named connection - */ - public connection(name?: string): any { - /** - * Using default connection name when actual name is missing - */ - name = name || this.getDefaultConnection() - - /** - * Return cached connection - */ - if (this.activeConnections[name]) { - return this.activeConnections[name] - } - - const config = this.getConnectionConfig(name) - - /** - * Raise error if config for the given name is missing - */ - if (!config) { - throw new Exception(`Define config for "${name}" connection inside "config/redis" file`) - } - - /** - * Create connection and store inside the connection pools - * object, so that we can re-use it later - */ - const connection = (this.activeConnections[name] = config.clusters - ? (new RedisClusterConnection( - name, - config, - this.application - ) as unknown as RedisClusterConnectionContract) - : (new RedisConnection(name, config, this.application) as unknown as RedisConnectionContract)) - - /** - * Forward events to the application event emitter - */ - connection.on('ready', ($connection) => - this.emitter.emit('redis:ready', { connection: $connection }) - ) - connection.on('connect', ($connection) => - this.emitter.emit('redis:connect', { connection: $connection }) - ) - connection.on('error', (error, $connection) => - this.emitter.emit('redis:error', { error, connection: $connection }) - ) - connection.on('node:added', ($connection, node) => - this.emitter.emit('redis:node:added', { node, connection: $connection }) - ) - connection.on('node:removed', (node, $connection) => - this.emitter.emit('redis:node:removed', { node, connection: $connection }) - ) - connection.on('node:error', (error, address, $connection) => - this.emitter.emit('redis:node:error', { error, address, connection: $connection }) - ) - - /** - * Stop tracking the connection after it's removed - */ - connection.on('end', ($connection) => { - delete this.activeConnections[$connection.connectionName] - this.emitter.emit('redis:end', { connection: $connection }) - }) - - /** - * Return connection - */ - return connection - } - - /** - * Quit a named connection or the default connection when no - * name is defined. - */ - public async quit(name?: string): Promise { - const connection = this.getExistingConnection(name) - if (!connection) { - return - } - - return connection.quit() - } - - /** - * Disconnect a named connection or the default connection when no - * name is defined. - */ - public async disconnect(name?: string): Promise { - const connection = this.getExistingConnection(name) - if (!connection) { - return - } - - return connection.disconnect() - } - - /** - * Quit all connections - */ - public async quitAll(): Promise { - await Promise.all(Object.keys(this.activeConnections).map((name) => this.quit(name))) - } - - /** - * Disconnect all connections - */ - public async disconnectAll(): Promise { - await Promise.all(Object.keys(this.activeConnections).map((name) => this.disconnect(name))) - } - - /** - * Returns the report for all connections marked for `healthChecks` - */ - public async report() { - const reports = (await Promise.all( - this.healthCheckConnections.map((connection) => { - return this.connection(connection).getReport(true) - }) - )) as HealthReportNode[] - - const healthy = !reports.find((report) => !!report.error) - return { - displayName: 'Redis', - health: { - healthy, - message: healthy - ? 'All connections are healthy' - : 'One or more redis connections are not healthy', - }, - meta: reports, - } - } - - /** - * Define a custom command using LUA script. You can run the - * registered command using the "runCommand" method. - */ - public defineCommand(...args: Parameters): this { - this.connection().defineCommand(...args) - return this - } - - /** - * Run a pre registered command - */ - public runCommand(command: string, ...args: any[]): any { - return this.connection().runCommand(command, ...args) - } -} - -/** - * Since types in AdonisJS are derived from interfaces, we take the leverage - * of dynamically adding redis methods to the class prototype. - */ -pubsubMethods.forEach((method) => { - RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { - return this.connection()[method](...args) - } -}) -ioMethods.forEach((method) => { - RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { - return this.connection()[method](...args) - } -}) diff --git a/src/AbstractConnection/index.ts b/src/abstract_connection.ts similarity index 77% rename from src/AbstractConnection/index.ts rename to src/abstract_connection.ts index 928056e..9b7e227 100644 --- a/src/AbstractConnection/index.ts +++ b/src/abstract_connection.ts @@ -1,25 +1,18 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - -import { EventEmitter } from 'events' +import { EventEmitter } from 'node:events' import { Redis, Cluster } from 'ioredis' import { Exception } from '@poppinss/utils' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { ContainerBindings, IocResolverContract } from '@ioc:Adonis/Core/Application' -import { - HealthReportNode, - PubSubChannelHandler, - PubSubPatternHandler, -} from '@ioc:Adonis/Addons/Redis' +import { ApplicationService } from '@adonisjs/core/types' +import { PubSubChannelHandler, PubSubPatternHandler, HealthReportNode } from './types/main.js' /** * Helper to sleep @@ -34,28 +27,23 @@ export abstract class AbstractConnection extends Even /** * Reference to the main ioRedis connection */ - public ioConnection: T + ioConnection!: T /** * Reference to the main ioRedis subscriber connection */ - public ioSubscriberConnection?: T + ioSubscriberConnection?: T /** * Number of times `getReport` was deferred, at max we defer it for 3 times */ - private deferredReportAttempts = 0 + #deferredReportAttempts = 0 /** * The last error emitted by the `error` event. We set it to `null` after * the `ready` event */ - private lastError?: any - - /** - * IoCResolver to resolve bindings - */ - private resolver: IocResolverContract + #lastError?: any /** * A list of active subscription and pattern subscription @@ -63,20 +51,10 @@ export abstract class AbstractConnection extends Even protected subscriptions: Map = new Map() protected psubscriptions: Map = new Map() - /** - * Returns an anonymous function by parsing the IoC container - * binding. - */ - private resolveIoCBinding(handler: string): PubSubChannelHandler | PubSubPatternHandler { - return (...args: any[]) => { - return this.resolver.call(handler, undefined, args) - } - } - /** * Returns the memory usage for a given connection */ - private async getUsedMemory() { + async #getUsedMemory() { const memory = await (this.ioConnection as Redis).info('memory') const memorySegment = memory .split(/\r|\r\n/) @@ -87,7 +65,7 @@ export abstract class AbstractConnection extends Even /** * Returns status of the main connection */ - public get status(): string { + get status(): string { return (this.ioConnection as Redis).status } @@ -96,7 +74,7 @@ export abstract class AbstractConnection extends Even * undefined when there is no subscriber * connection */ - public get subscriberStatus(): string | undefined { + get subscriberStatus(): string | undefined { if (!this.ioSubscriberConnection) { return } @@ -109,9 +87,8 @@ export abstract class AbstractConnection extends Even */ protected abstract makeSubscriberConnection(): void - constructor(public connectionName: string, application: ApplicationContract) { + constructor(public connectionName: string, application: ApplicationService) { super() - this.resolver = application.container.getResolver(undefined, 'redisListeners', 'App/Listeners') } /** @@ -127,12 +104,12 @@ export abstract class AbstractConnection extends Even * We must set the error to null when server is ready for accept * command */ - this.lastError = null + this.#lastError = null this.emit('ready', this) }) this.ioConnection.on('error', (error: any) => { - this.lastError = error + this.#lastError = error this.emit('error', error, this) }) @@ -200,11 +177,11 @@ export abstract class AbstractConnection extends Even this.ioSubscriberConnection!.on('connect', () => this.emit('subscriber:connect', this)) this.ioSubscriberConnection!.on('ready', () => this.emit('subscriber:ready', this)) this.ioSubscriberConnection!.on('error', (error: any) => - this.emit('subscriber:error', error, this) + this.emit('subscriber:error', error, this), ) this.ioSubscriberConnection!.on('close', () => this.emit('subscriber:close', this)) this.ioSubscriberConnection!.on('reconnecting', () => - this.emit('subscriber:reconnecting', this) + this.emit('subscriber:reconnecting', this), ) /** @@ -226,7 +203,7 @@ export abstract class AbstractConnection extends Even /** * Gracefully end the redis connection */ - public async quit() { + async quit() { await this.ioConnection.quit() if (this.ioSubscriberConnection) { await this.ioSubscriberConnection.quit() @@ -236,7 +213,7 @@ export abstract class AbstractConnection extends Even /** * Forcefully end the redis connection */ - public async disconnect() { + async disconnect() { await this.ioConnection.disconnect() if (this.ioSubscriberConnection) { await this.ioSubscriberConnection.disconnect() @@ -247,7 +224,7 @@ export abstract class AbstractConnection extends Even * Subscribe to a given channel to receive Redis pub/sub events. A * new subscriber connection will be created/managed automatically. */ - public subscribe(channel: string, handler: PubSubChannelHandler | string): void { + subscribe(channel: string, handler: PubSubChannelHandler): void { /** * Make the subscriber connection. The method results in a noop when * subscriber connection already exists. @@ -258,11 +235,10 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.subscriptions.has(channel)) { - throw new Exception( - `"${channel}" channel already has an active subscription`, - 500, - 'E_MULTIPLE_REDIS_SUBSCRIPTIONS' - ) + throw new Exception(`"${channel}" channel already has an active subscription`, { + code: 'E_MULTIPLE_REDIS_SUBSCRIPTIONS', + status: 500, + }) } /** @@ -274,9 +250,6 @@ export abstract class AbstractConnection extends Even connection .subscribe(channel) .then((count) => { - if (typeof handler === 'string') { - handler = this.resolveIoCBinding(handler) as PubSubChannelHandler - } this.emit('subscription:ready', count, this) this.subscriptions.set(channel, handler) }) @@ -288,7 +261,7 @@ export abstract class AbstractConnection extends Even /** * Unsubscribe from a channel */ - public unsubscribe(channel: string) { + unsubscribe(channel: string) { this.subscriptions.delete(channel) return (this.ioSubscriberConnection as Redis).unsubscribe(channel) } @@ -296,7 +269,7 @@ export abstract class AbstractConnection extends Even /** * Make redis subscription for a pattern */ - public psubscribe(pattern: string, handler: PubSubPatternHandler | string): void { + psubscribe(pattern: string, handler: PubSubPatternHandler): void { /** * Make the subscriber connection. The method results in a noop when * subscriber connection already exists. @@ -307,11 +280,10 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.psubscriptions.has(pattern)) { - throw new Exception( - `${pattern} pattern already has an active subscription`, - 500, - 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS' - ) + throw new Exception(`${pattern} pattern already has an active subscription`, { + status: 500, + code: 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS', + }) } /** @@ -324,10 +296,6 @@ export abstract class AbstractConnection extends Even connection .psubscribe(pattern) .then((count) => { - if (typeof handler === 'string') { - handler = this.resolveIoCBinding(handler) as PubSubPatternHandler - } - this.emit('psubscription:ready', count, this) this.psubscriptions.set(pattern, handler) }) @@ -339,7 +307,7 @@ export abstract class AbstractConnection extends Even /** * Unsubscribe from a given pattern */ - public punsubscribe(pattern: string) { + punsubscribe(pattern: string) { this.psubscriptions.delete(pattern) return (this.ioSubscriberConnection as any).punsubscribe(pattern) } @@ -347,7 +315,7 @@ export abstract class AbstractConnection extends Even /** * Returns report for the connection */ - public async getReport(checkForMemory?: boolean): Promise { + async getReport(checkForMemory?: boolean): Promise { const connection = this.ioConnection as Redis /** @@ -355,9 +323,13 @@ export abstract class AbstractConnection extends Even * the report. Which means, if we are unable to connect to redis within * 3 seconds, we consider the connection unstable. */ - if (connection.status === 'connecting' && this.deferredReportAttempts < 3 && !this.lastError) { + if ( + connection.status === 'connecting' && + this.#deferredReportAttempts < 3 && + !this.#lastError + ) { await sleep() - this.deferredReportAttempts++ + this.#deferredReportAttempts++ return this.getReport(checkForMemory) } @@ -370,7 +342,7 @@ export abstract class AbstractConnection extends Even connection: this.connectionName, status: connection.status, used_memory: null, - error: this.lastError, + error: this.#lastError, } } @@ -383,7 +355,7 @@ export abstract class AbstractConnection extends Even /** * Collect memory when checkForMemory = true */ - const memory = checkForMemory ? await this.getUsedMemory() : 'unknown' + const memory = checkForMemory ? await this.#getUsedMemory() : 'unknown' return { connection: this.connectionName, @@ -404,7 +376,7 @@ export abstract class AbstractConnection extends Even /** * Publish the pub/sub message */ - public publish(channel: string, message: string, callback?: any) { + publish(channel: string, message: string, callback?: any) { return callback ? this.ioConnection.publish(channel, message, callback) : this.ioConnection.publish(channel, message) @@ -414,7 +386,7 @@ export abstract class AbstractConnection extends Even * Define a custom command using LUA script. You can run the * registered command using the "runCommand" method. */ - public defineCommand(...args: Parameters): this { + defineCommand(...args: Parameters): this { this.ioConnection.defineCommand(...args) return this } @@ -422,7 +394,8 @@ export abstract class AbstractConnection extends Even /** * Run a pre registered command */ - public runCommand(command: string, ...args: any[]): any { + runCommand(command: string, ...args: any[]): any { + // @ts-ignore return this.ioConnection[command](...args) } } diff --git a/src/define_config.ts b/src/define_config.ts new file mode 100644 index 0000000..5ccc094 --- /dev/null +++ b/src/define_config.ts @@ -0,0 +1,44 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { RedisConnectionConfig, RedisClusterConfig } from './types/main.js' +import { InvalidArgumentsException } from '@poppinss/utils' + +/** + * Expected shape of the config accepted by the "defineConfig" + * method + */ +type RedisConfig = { + connections: { + [name: string]: RedisConnectionConfig | RedisClusterConfig + } +} + +/** + * Define config for redis + */ +export function defineConfig( + config: T, +): T { + if (!config) { + throw new InvalidArgumentsException('Invalid config. It must be a valid object') + } + + if (!config.connections) { + throw new InvalidArgumentsException('Invalid config. Missing property "connections" inside it') + } + + if (!config.connection || !config.connections[config.connection as any]) { + throw new InvalidArgumentsException( + 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object', + ) + } + + return config +} diff --git a/src/ioMethods.ts b/src/io_methods.ts similarity index 98% rename from src/ioMethods.ts rename to src/io_methods.ts index fe4d187..0692efb 100644 --- a/src/ioMethods.ts +++ b/src/io_methods.ts @@ -1,7 +1,7 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -170,4 +170,4 @@ export const ioMethods = [ 'xreadgroup', 'xrevrange', 'xtrim', -] +] as const diff --git a/src/pubsubMethods.ts b/src/pubsub_methods.ts similarity index 88% rename from src/pubsubMethods.ts rename to src/pubsub_methods.ts index 15c516c..bad9f15 100644 --- a/src/pubsubMethods.ts +++ b/src/pubsub_methods.ts @@ -1,7 +1,7 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/RedisClusterConnection/index.ts b/src/redis_cluster_connection.ts similarity index 50% rename from src/RedisClusterConnection/index.ts rename to src/redis_cluster_connection.ts index 24b6870..19aae4d 100644 --- a/src/RedisClusterConnection/index.ts +++ b/src/redis_cluster_connection.ts @@ -1,34 +1,32 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import Redis, { Cluster, NodeRole } from 'ioredis' -import { RedisClusterConfig } from '@ioc:Adonis/Addons/Redis' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { ioMethods } from '../ioMethods' -import { AbstractConnection } from '../AbstractConnection' +import { ioMethods } from './io_methods.js' +import { AbstractConnection } from './abstract_connection.js' +import { ApplicationService } from '@adonisjs/core/types' +import { RedisClusterConfig, RedisClusterConnectionFactory } from './types/main.js' /** * Redis cluster connection exposes the API to run Redis commands using `ioredis` as the * underlying client. The class abstracts the need of creating and managing multiple * pub/sub connections by hand, since it handles that internally by itself. */ -export class RedisClusterConnection extends AbstractConnection { - constructor( - connectionName: string, - private config: RedisClusterConfig, - application: ApplicationContract - ) { +export class RawRedisClusterConnection extends AbstractConnection { + #config: RedisClusterConfig + + constructor(connectionName: string, config: RedisClusterConfig, application: ApplicationService) { super(connectionName, application) - this.ioConnection = new Redis.Cluster(this.config.clusters as any[], this.config.clusterOptions) + + this.#config = config + this.ioConnection = new Redis.Cluster(this.#config.clusters as any[]) this.proxyConnectionEvents() } @@ -38,21 +36,32 @@ export class RedisClusterConnection extends AbstractConnection { */ protected makeSubscriberConnection() { this.ioSubscriberConnection = new Redis.Cluster( - this.config.clusters as [], - this.config.clusterOptions + this.#config.clusters as [], + this.#config.clusterOptions, ) } /** * Returns cluster nodes */ - public nodes(role?: NodeRole) { + nodes(role?: NodeRole) { return this.ioConnection.nodes(role) } } +/** + * Here we attach pubsub and ioRedis methods to the class. + * + * But we also need to inform typescript about the existence of + * these methods. So we are exporting the class with a + * casted type that has these methods. + */ +const RedisClusterConnection = RawRedisClusterConnection as unknown as RedisClusterConnectionFactory + ioMethods.forEach((method) => { RedisClusterConnection.prototype[method] = function redisConnectionProxyFn(...args: any[]) { return this.ioConnection[method](...args) } }) + +export default RedisClusterConnection diff --git a/src/RedisConnection/index.ts b/src/redis_connection.ts similarity index 53% rename from src/RedisConnection/index.ts rename to src/redis_connection.ts index 479b38d..317609e 100644 --- a/src/RedisConnection/index.ts +++ b/src/redis_connection.ts @@ -1,20 +1,18 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// +import { Redis, RedisOptions } from 'ioredis' -import Redis, { RedisOptions } from 'ioredis' -import { RedisConnectionConfig } from '@ioc:Adonis/Addons/Redis' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -import { ioMethods } from '../ioMethods' -import { AbstractConnection } from '../AbstractConnection' +import { ioMethods } from './io_methods.js' +import { AbstractConnection } from './abstract_connection.js' +import { RedisConnectionConfig, RedisConnectionFactory } from './types/main.js' +import { ApplicationService } from '@adonisjs/core/types' /** * Redis connection exposes the API to run Redis commands using `ioredis` as the @@ -22,25 +20,25 @@ import { AbstractConnection } from '../AbstractConnection' * multiple pub/sub connections by hand, since it handles that internally * by itself. */ -export class RedisConnection extends AbstractConnection { - private config: RedisOptions +export class RawRedisConnection extends AbstractConnection { + #config: RedisOptions constructor( connectionName: string, config: RedisConnectionConfig, - application: ApplicationContract + application: ApplicationService, ) { super(connectionName, application) - this.config = this.normalizeConfig(config) + this.#config = this.#normalizeConfig(config) - this.ioConnection = new Redis(this.config) + this.ioConnection = new Redis(this.#config) this.proxyConnectionEvents() } /** * Normalizes config option to be compatible with IORedis */ - private normalizeConfig(config: RedisConnectionConfig): RedisOptions { + #normalizeConfig(config: RedisConnectionConfig): RedisOptions { if (typeof config.port === 'string') { config.port = Number(config.port) } @@ -52,16 +50,23 @@ export class RedisConnection extends AbstractConnection { * invoke this method when first subscription is created. */ protected makeSubscriberConnection() { - this.ioSubscriberConnection = new Redis(this.config) + this.ioSubscriberConnection = new Redis(this.#config) } } /** - * Since types in AdonisJS are derived from interfaces, we take the leverage - * of dynamically adding redis methods to the class prototype. + * Here we attach pubsub and ioRedis methods to the class. + * + * But we also need to inform typescript about the existence of + * these methods. So we are exporting the class with a + * casted type that has these methods. */ +const RedisConnection = RawRedisConnection as unknown as RedisConnectionFactory + ioMethods.forEach((method) => { RedisConnection.prototype[method] = function redisConnectionProxyFn(...args: any[]) { return this.ioConnection[method](...args) } }) + +export default RedisConnection diff --git a/src/redis_manager.ts b/src/redis_manager.ts new file mode 100644 index 0000000..1166c9a --- /dev/null +++ b/src/redis_manager.ts @@ -0,0 +1,280 @@ +import { ApplicationService, EmitterService } from '@adonisjs/core/types' +import RedisConnection from './redis_connection.js' +import { pubsubMethods } from './pubsub_methods.js' +import { ioMethods } from './io_methods.js' +import { + Connection, + GetConnectionType, + RedisConnectionAugmented, + RedisConnectionsList, + RedisManagerFactory, +} from './types/main.js' +import RedisClusterConnection from './redis_cluster_connection.js' + +export class RawRedisManager { + /** + * User provided config + */ + #config: { + connection: keyof ConnectionList + connections: ConnectionList + } + + /** + * Reference to the application + */ + #app: ApplicationService + + /** + * Reference to the emitter + */ + #emitter: EmitterService + + /** + * An array of connections with health checks enabled, which means, we always + * create a connection for them, even when they are not used. + */ + #healthCheckConnections: string[] = [] + + /** + * A copy of live connections. We avoid re-creating a new connection + * everytime and re-use connections. + */ + activeConnections: { + [K in keyof ConnectionList]?: GetConnectionType + } = {} + + /** + * A boolean to know whether health checks have been enabled on one + * or more redis connections or not. + */ + get healthChecksEnabled() { + return this.#healthCheckConnections.length > 0 + } + + /** + * Returns the length of active connections + */ + get activeConnectionsCount() { + return Object.keys(this.activeConnections).length + } + + constructor( + app: ApplicationService, + config: { + connection: keyof ConnectionList + connections: ConnectionList + }, + emitter: EmitterService, + ) { + this.#app = app + this.#config = config + this.#emitter = emitter + this.#healthCheckConnections = Object.keys(this.#config.connections).filter( + (connection) => this.#config.connections[connection].healthCheck, + ) + } + + /** + * Returns the default connection name + */ + #getDefaultConnection(): keyof ConnectionList { + return this.#config.connection + } + + /** + * Returns an existing connection using it's name or the + * default connection, + */ + #getExistingConnection(name?: keyof ConnectionList) { + name = name || this.#getDefaultConnection() + return this.activeConnections[name] + } + + /** + * Returns config for a given connection + */ + #getConnectionConfig(name: ConnectionName) { + return this.#config.connections[name] + } + + /** + * Forward events to the application event emitter + * for a given connection + */ + #forwardConnectionEvents(connection: Connection) { + connection.on('ready', () => { + this.#emitter.emit('redis:ready', { connection }) + }) + connection.on('ready', ($connection) => + this.#emitter.emit('redis:ready', { connection: $connection }), + ) + connection.on('connect', ($connection) => + this.#emitter.emit('redis:connect', { connection: $connection }), + ) + connection.on('error', (error, $connection) => + this.#emitter.emit('redis:error', { error, connection: $connection }), + ) + connection.on('node:added', ($connection, node) => + this.#emitter.emit('redis:node:added', { node, connection: $connection }), + ) + connection.on('node:removed', (node, $connection) => + this.#emitter.emit('redis:node:removed', { node, connection: $connection }), + ) + connection.on('node:error', (error, address, $connection) => + this.#emitter.emit('redis:node:error', { error, address, connection: $connection }), + ) + + /** + * Stop tracking the connection after it's removed + */ + connection.on('end', ($connection) => { + delete this.activeConnections[$connection.connectionName] + this.#emitter.emit('redis:end', { connection: $connection }) + }) + } + + /** + * Returns redis factory for a given named connection + */ + connection( + connectionName?: ConnectionName, + ): GetConnectionType { + const name = connectionName || this.#getDefaultConnection() + + /** + * Return existing connection if already exists + */ + if (this.activeConnections[name]) { + return this.activeConnections[name] as any + } + + /** + * Get config for the named connection + */ + const config = this.#getConnectionConfig(name) + if (!config) { + throw new Error(`Redis connection "${name.toString()}" is not defined`) + } + + /** + * Instantiate the connection based upon the config + */ + const connection = + 'clusters' in config + ? new RedisClusterConnection(name as string, config, this.#app) + : new RedisConnection(name as string, config, this.#app) + + /** + * Cache the connection so that we can re-use it later + */ + this.activeConnections[name] = connection as GetConnectionType + + /** + * Forward ioredis events to the application event emitter + */ + this.#forwardConnectionEvents(connection) + + return connection as GetConnectionType + } + + /** + * Quit a named connection or the default connection when no + * name is defined. + */ + async quit(name?: ConnectionName) { + const connection = this.#getExistingConnection(name) + if (!connection) { + return + } + + return connection.quit() + } + + /** + * Disconnect a named connection or the default connection when no + * name is defined. + */ + async disconnect(name?: ConnectionName) { + const connection = this.#getExistingConnection(name) + if (!connection) { + return + } + + return connection.disconnect() + } + + /** + * Quit all connections + */ + async quitAll(): Promise { + await Promise.all(Object.keys(this.activeConnections).map((name) => this.quit(name))) + } + + /** + * Disconnect all connections + */ + async disconnectAll(): Promise { + await Promise.all(Object.keys(this.activeConnections).map((name) => this.disconnect(name))) + } + + /** + * Returns the report for all connections marked for `healthChecks` + */ + async report() { + const reports = await Promise.all( + this.#healthCheckConnections.map((connection) => this.connection(connection).getReport(true)), + ) + + const healthy = !reports.find((report) => !!report.error) + return { + displayName: 'Redis', + health: { + healthy, + message: healthy + ? 'All connections are healthy' + : 'One or more redis connections are not healthy', + }, + meta: reports, + } + } + + /** + * Define a custom command using LUA script. You can run the + * registered command using the "runCommand" method. + */ + defineCommand(...args: Parameters): this { + this.connection().defineCommand(...args) + return this + } + + /** + * Run a pre registered command + */ + runCommand(command: string, ...args: any[]): any { + return this.connection().runCommand(command, ...args) + } +} + +/** + * Here we attach pubsub and ioRedis methods to the class. + * + * But we also need to inform typescript about the existence of + * these methods. So we are exporting the class with a + * casted type that has these methods. + */ +const RedisManager = RawRedisManager as unknown as RedisManagerFactory + +pubsubMethods.forEach((method) => { + RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { + return this.connection()[method](...args) + } +}) + +ioMethods.forEach((method) => { + RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { + return this.connection()[method](...args) + } +}) + +export default RedisManager diff --git a/src/repl_bindings.ts b/src/repl_bindings.ts new file mode 100644 index 0000000..522ff36 --- /dev/null +++ b/src/repl_bindings.ts @@ -0,0 +1,30 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { Repl } from '@adonisjs/core/repl' +import type { ApplicationService } from '@adonisjs/core/types' + +/** + * Define repl bindings. The method must be invoked when application environment + * is set to repl. + */ +export function defineReplBindings(app: ApplicationService, repl: Repl) { + repl.addMethod( + 'loadRedis', + async () => { + repl.server!.context.redis = await app.container.make('redis') + repl.notify( + `Loaded "redis" service. You can access it using the "${repl.colors.underline( + 'redis', + )}" variable`, + ) + }, + { description: 'Load "redis" service in the REPL context' }, + ) +} diff --git a/src/types/extended.ts b/src/types/extended.ts new file mode 100644 index 0000000..42303be --- /dev/null +++ b/src/types/extended.ts @@ -0,0 +1,35 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { + RedisClusterConnectionAugmented, + RedisConnectionAugmented, + RedisManagerAugmented, +} from './main.js' +import { Redis } from 'ioredis' + +declare module '@adonisjs/core/types' { + export interface ContainerBindings { + redis: RedisManagerAugmented + } + + export interface EventsList { + 'redis:ready': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + 'redis:connect': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + 'redis:error': { + error: any + connection: RedisClusterConnectionAugmented | RedisConnectionAugmented + } + 'redis:end': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + + 'redis:node:added': { connection: RedisClusterConnectionAugmented; node: Redis } + 'redis:node:removed': { connection: RedisClusterConnectionAugmented; node: Redis } + 'redis:node:error': { error: any; connection: RedisClusterConnectionAugmented; address: string } + } +} diff --git a/src/types/main.ts b/src/types/main.ts new file mode 100644 index 0000000..aeb67c8 --- /dev/null +++ b/src/types/main.ts @@ -0,0 +1,151 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { EventEmitter } from 'node:events' +import { Redis as IoRedis, RedisOptions, ClusterOptions, Cluster } from 'ioredis' +import { RawRedisClusterConnection } from '../redis_cluster_connection.js' +import { ApplicationService } from '@adonisjs/core/types' +import { AbstractConnection } from '../abstract_connection.js' +import { RawRedisConnection } from '../redis_connection.js' +import { Emitter } from '@adonisjs/core/events' +import { RawRedisManager } from '../redis_manager.js' + +/* + |-------------------------------------------------------------------------- + | Helpers + |-------------------------------------------------------------------------- + */ +/** + * Returns factory for a given connection by inspecting it's config. + */ +// type GetConnectionFactoryType = +// RedisConnectionsList[T] extends RedisClusterConfig +// ? RedisClusterConnectionContract +// : RedisConnectionContract + +/** + * Pubsub subscriber + */ +export type PubSubChannelHandler = (data: T) => Promise | void +export type PubSubPatternHandler = ( + channel: string, + data: T, +) => Promise | void + +/** + * Redis pub/sub methods + */ +export interface RedisPubSubContract { + publish( + channel: string, + message: string, + callback: (error: Error | null, count: number) => void, + ): void + publish(channel: string, message: string): Promise + subscribe(channel: string, handler: PubSubChannelHandler | string): void + psubscribe(pattern: string, handler: PubSubPatternHandler | string): void + unsubscribe(channel: string): void + punsubscribe(pattern: string): void +} + +/** + * Shape of the report node for the redis connection report + */ +export type HealthReportNode = { + connection: string + status: string + used_memory: string | null + error: any +} + +/** + * List of commands on the IORedis. We omit their internal events and pub/sub + * handlers, since we our own. + */ +export type IORedisCommands = Omit< + IoRedis, + | 'Promise' + | 'status' + | 'connect' + | 'disconnect' + | 'duplicate' + | 'subscribe' + | 'unsubscribe' + | 'psubscribe' + | 'punsubscribe' + | 'quit' + | 'publish' + | keyof EventEmitter +> + +export type Connection = RedisClusterConnectionAugmented | RedisConnectionAugmented +export type RedisConnectionsList = Record + +export type GetConnectionType< + ConnectionsList extends RedisConnectionsList, + T extends keyof ConnectionsList, +> = ConnectionsList[T] extends RedisClusterConfig + ? RedisClusterConnectionAugmented + : RedisConnectionAugmented + +/** + * Shape of standard Redis connection config + */ +export type RedisConnectionConfig = Omit & { + healthCheck?: boolean + port?: string | number +} + +/** + * Shape of cluster config + */ +export type RedisClusterConfig = { + clusters: { host: string; port: number | string }[] + clusterOptions?: ClusterOptions + healthCheck?: boolean +} + +/** + * Since we are dynamically addind methods to the RedisClusterConnection + * RedisConnection and RedisManager classes prototypes, we need to tell + * typescript about it. So the below types represents theses classes with + * all the methods added + */ +export type RedisClusterConnectionFactory = { + new ( + connectionName: string, + config: RedisClusterConfig, + application: ApplicationService, + ): RawRedisClusterConnection & AbstractConnection & IORedisCommands & RedisPubSubContract +} + +export type RedisConnectionFactory = { + new ( + connectionName: string, + config: RedisConnectionConfig, + application: ApplicationService, + ): RawRedisConnection & AbstractConnection & IORedisCommands & RedisPubSubContract +} + +export type RedisManagerFactory = { + new >( + application: ApplicationService, + config: { + connection: keyof ConnectionList + connections: ConnectionList + }, + emitter: Emitter, + ): RawRedisManager & IORedisCommands & RedisPubSubContract +} + +type ConstructorReturnType = T extends { new (...args: any[]): infer U } ? U : never + +export type RedisClusterConnectionAugmented = ConstructorReturnType +export type RedisConnectionAugmented = ConstructorReturnType +export type RedisManagerAugmented = ConstructorReturnType diff --git a/test/contracts.ts b/test/contracts.ts deleted file mode 100644 index e992505..0000000 --- a/test/contracts.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module '@ioc:Adonis/Addons/Redis' { - export interface RedisConnectionsList { - primary: RedisConnectionConfig - cluster: RedisClusterConfig - } -} diff --git a/test/define_config.spec.ts b/test/define_config.spec.ts new file mode 100644 index 0000000..18d681d --- /dev/null +++ b/test/define_config.spec.ts @@ -0,0 +1,38 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { defineConfig } from '../src/define_config.js' + +test.group('Define Config', () => { + test('should throw if no config passed', ({ assert }) => { + // @ts-ignore + assert.throws(defineConfig, 'Invalid config. It must be a valid object') + }) + + test('should throw if no connections', ({ assert }) => { + assert.throws( + // @ts-ignore + () => defineConfig({ connection: 'hey' }), + 'Invalid config. Missing property "connections" inside it', + ) + }) + + test('should throw if connection is not defined inside connections', ({ assert }) => { + assert.throws( + () => + defineConfig({ + // @ts-ignore + connection: 'hey', + connections: {}, + }), + 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object', + ) + }) +}) diff --git a/test/redis-connection.spec.ts b/test/redis-connection.spec.ts deleted file mode 100644 index 1f49991..0000000 --- a/test/redis-connection.spec.ts +++ /dev/null @@ -1,600 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { test } from '@japa/runner' -import { Application } from '@adonisjs/core/build/standalone' -import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis' - -import { RedisConnection } from '../src/RedisConnection' - -test.group('Redis factory', () => { - test('emit ready when connected to redis server', ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('error', async (error) => { - console.log(error) - }) - - factory.on('ready', async () => { - assert.isTrue(true) - await factory.quit() - done() - }) - }).waitForDone() - - test('execute redis commands', async ({ assert }) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - await factory.set('greeting', 'hello world') - - const greeting = await factory.get('greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) - - test('clean event listeners on quit', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.quit() - }) - }).waitForDone() - - test('clean event listeners on disconnect', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.quit() - }) - }).waitForDone() - - test('get event for connection errors', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { port: 4444 }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', async (error) => { - assert.equal(error.code, 'ECONNREFUSED') - assert.equal(error.port, 4444) - await factory.quit() - }) - }).waitForDone() - - test('get report for connected connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - const report = await factory.getReport(true) - - assert.equal(report.status, 'ready') - assert.isNull(report.error) - assert.isDefined(report.used_memory) - - await factory.quit() - }) - }).waitForDone() - - test('get report for errored connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: 4444, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', async () => { - const report = await factory.getReport(true) - - assert.notEqual(report.status, 'ready') - assert.equal(report.error.code, 'ECONNREFUSED') - assert.equal(report.used_memory, null) - - await factory.quit() - }) - }).waitForDone() - - test('execute redis commands using lua scripts', async ({ assert }) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.defineCommand('defineValue', { - numberOfKeys: 1, - lua: `redis.call('set', KEYS[1], ARGV[1])`, - }) - - factory.defineCommand('readValue', { - numberOfKeys: 1, - lua: `return redis.call('get', KEYS[1])`, - }) - - await factory.runCommand('defineValue', 'greeting', 'hello world') - const greeting = await factory.runCommand('readValue', 'greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) -}) - -test.group('Redis factory - Subscribe', () => { - test('emit subscriber events when subscriber connection is created', async ({}, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscriber:ready', async () => { - await factory.quit() - done() - }) - - factory.subscribe('news', () => {}) - }).waitForDone() - - test('emit subscription event when subscription is created', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscription:ready', async (count) => { - assert.equal(count, 1) - await factory.quit() - done() - }) - - factory.subscribe('news', () => {}) - }).waitForDone() - - test('make multiple subscriptions to different channels', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - let invokedCounts = 0 - - factory.on('subscription:ready', async (count) => { - invokedCounts++ - - if (invokedCounts === 2) { - assert.equal(count, 2) - await factory.quit() - done() - } else { - assert.equal(count, 1) - } - }) - - factory.subscribe('news', () => {}) - factory.subscribe('sports', () => {}) - }).waitForDone() - - test('publish messages', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.subscribe('news', async (message) => { - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('subscription:ready', () => { - factory.publish('news', 'breaking news at 9') - }) - }).waitForDone() - - test('publish messages to multiple channels', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscription:ready', (count) => { - if (count === 1) { - factory.publish('news', 'breaking news at 9') - } - }) - - factory.subscribe('news', (message) => { - assert.equal(message, 'breaking news at 9') - factory.publish('sports', 'india won the cup') - }) - - factory.subscribe('sports', async (message) => { - assert.equal(message, 'india won the cup') - await factory.quit() - done() - }) - }).waitForDone() - - test('unsubscribe from a channel', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('subscription:ready', () => { - factory.publish('news', 'breaking news at 9') - }) - - factory.subscribe('news', (message) => { - assert.equal(message, 'breaking news at 9') - factory.unsubscribe('news') - - factory.publish('news', 'breaking news at 9', (_error, count) => { - assert.equal(count, 0) - done() - }) - }) - }).waitForDone() - - test('consume messages not stringified using message builder', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.subscribe('news', async (message) => { - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('subscription:ready', () => { - factory.ioConnection.publish('news', 'breaking news at 9') - }) - }).waitForDone() - - test('consume messages self stringified with message sub property', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.subscribe('news', async (message) => { - assert.equal(message, JSON.stringify({ message: 'breaking news at 9' })) - await factory.quit() - done() - }) - - factory.on('subscription:ready', () => { - factory.ioConnection.publish('news', JSON.stringify({ message: 'breaking news at 9' })) - }) - }).waitForDone() -}) - -test.group('Redis factory - PSubscribe', () => { - test('emit subscriber events when subscriber connection is created', async ({}, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.on('subscriber:ready', async () => { - await factory.quit() - done() - }) - - factory.psubscribe('news:*', () => {}) - }).waitForDone() - - test('emit subscription event when subscription is created', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.on('psubscription:ready', async (count) => { - assert.equal(count, 1) - await factory.quit() - done() - }) - - factory.psubscribe('news:*', () => {}) - }).waitForDone() - - test('make multiple subscriptions to different patterns', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - let invokedCounts = 0 - - factory.on('psubscription:ready', async (count) => { - invokedCounts++ - - if (invokedCounts === 2) { - assert.equal(count, 2) - await factory.quit() - done() - } else { - assert.equal(count, 1) - } - }) - - factory.psubscribe('news:*', () => {}) - factory.psubscribe('sports:*', () => {}) - }).waitForDone() - - test('publish messages', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.psubscribe('news:*', async (channel, message) => { - assert.equal(channel, 'news:prime') - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('psubscription:ready', () => { - factory.publish('news:prime', 'breaking news at 9') - }) - }).waitForDone() - - test('publish messages to multiple channels', async ({ assert }, done) => { - assert.plan(2) - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('psubscription:ready', (count) => { - if (count === 1) { - factory.publish('news:prime', 'breaking news at 9') - } - }) - - factory.psubscribe('news:*', async (channel, message) => { - if (channel === 'news:prime') { - assert.equal(message, 'breaking news at 9') - factory.publish('news:breakfast', 'celebrating marathon') - } - - if (channel === 'news:breakfast') { - assert.equal(message, 'celebrating marathon') - await factory.quit() - done() - } - }) - }).waitForDone() - - test('unsubscribe from a pattern', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - - factory.on('psubscription:ready', () => { - factory.publish('news:prime', JSON.stringify({ title: 'breaking news at 9' })) - }) - - factory.psubscribe('news:*', (channel, message) => { - assert.equal(channel, 'news:prime') - assert.deepEqual(message, JSON.stringify({ title: 'breaking news at 9' })) - factory.punsubscribe('news:*') - - factory.publish('news:prime', 'breaking news at 9', (_error, count) => { - assert.equal(count, 0) - done() - }) - }) - }).waitForDone() - - test('bind IoC container binding as subscriber', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - app - ) as unknown as RedisConnectionContract - - class RedisListeners { - public async onNews(channel: string, message: string) { - assert.equal(channel, 'news:prime') - assert.equal(message, JSON.stringify({ title: 'breaking news at 9' })) - await factory.quit() - done() - } - } - - app.container.bind('App/Listeners/RedisListeners', () => { - return new RedisListeners() - }) - - factory.psubscribe('news:*', 'RedisListeners.onNews') - - factory.on('psubscription:ready', () => { - factory.publish('news:prime', JSON.stringify({ title: 'breaking news at 9' })) - }) - }).waitForDone() - - test('consume messages not stringified using message builder', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.psubscribe('news:*', async (channel, message) => { - assert.equal(channel, 'news:prime') - assert.equal(message, 'breaking news at 9') - await factory.quit() - done() - }) - - factory.on('psubscription:ready', () => { - factory.ioConnection.publish('news:prime', 'breaking news at 9') - }) - }).waitForDone() - - test('consume messages self stringified', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisConnectionContract - factory.psubscribe('news:*', async (channel, message) => { - assert.equal(channel, 'news:prime') - assert.equal(message, JSON.stringify({ message: 'breaking news at 9' })) - await factory.quit() - done() - }) - - factory.on('psubscription:ready', () => { - factory.ioConnection.publish('news:prime', JSON.stringify({ message: 'breaking news at 9' })) - }) - }).waitForDone() -}) diff --git a/test/redis-provider.spec.ts b/test/redis-provider.spec.ts deleted file mode 100644 index bf661ac..0000000 --- a/test/redis-provider.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * @adonisjs/events - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { RedisManager } from '../src/RedisManager' - -const fs = new Filesystem(join(__dirname, 'app')) - -async function setup(environment: 'web' | 'repl', redisConfig: any) { - await fs.add('.env', '') - await fs.add( - 'config/app.ts', - ` - export const appKey = 'averylong32charsrandomsecretkey', - export const http = { - cookie: {}, - trustProxy: () => true, - } - ` - ) - - await fs.add( - 'config/redis.ts', - ` - const redisConfig = ${JSON.stringify(redisConfig, null, 2)} - export default redisConfig - ` - ) - - const app = new Application(fs.basePath, environment, { - providers: ['@adonisjs/core', '@adonisjs/repl', '../../providers/RedisProvider'], - }) - - await app.setup() - await app.registerProviders() - await app.bootProviders() - - return app -} - -test.group('Redis Provider', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('register redis provider', async ({ assert }) => { - const app = await setup('web', { - connection: 'local', - connections: { - local: {}, - }, - }) - - assert.instanceOf(app.container.use('Adonis/Addons/Redis'), RedisManager) - assert.deepEqual(app.container.use('Adonis/Addons/Redis')['application'], app) - assert.deepEqual( - app.container.use('Adonis/Addons/Redis'), - app.container.use('Adonis/Addons/Redis') - ) - }) - - test('raise error when config is missing', async ({ assert }) => { - assert.plan(1) - - try { - await setup('web', {}) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. Missing value for "connection". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('raise error when primary connection is not defined', async ({ assert }) => { - assert.plan(1) - - try { - await setup('web', {}) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. Missing value for "connection". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('raise error when connections are not defined', async ({ assert }) => { - assert.plan(1) - - try { - await setup('web', { - connection: 'local', - }) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. Missing value for "connections". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('raise error when primary connection is not defined in the connections list', async ({ - assert, - }) => { - assert.plan(1) - - try { - await setup('web', { - connection: 'local', - connections: {}, - }) - } catch (error) { - assert.equal( - error.message, - 'Invalid "redis" config. "local" is not defined inside "connections". Make sure to set it inside the "config/redis" file' - ) - } - }) - - test('define repl bindings', async ({ assert }) => { - const app = await setup('repl', { - connection: 'local', - connections: { - local: {}, - }, - }) - - assert.property(app.container.use('Adonis/Addons/Repl')['customMethods'], 'loadRedis') - assert.isFunction(app.container.use('Adonis/Addons/Repl')['customMethods']['loadRedis'].handler) - }) - - test('define health checks', async ({ assert }) => { - const app = await setup('web', { - connection: 'local', - connections: { - local: { - healthCheck: true, - }, - }, - }) - - assert.property(app.container.use('Adonis/Core/HealthCheck')['healthCheckers'], 'redis') - assert.equal( - app.container.use('Adonis/Core/HealthCheck')['healthCheckers'].redis, - 'Adonis/Addons/Redis' - ) - }) -}) diff --git a/test/redis.spec.ts b/test/redis.spec.ts deleted file mode 100644 index 9c40ca9..0000000 --- a/test/redis.spec.ts +++ /dev/null @@ -1,311 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { test } from '@japa/runner' -import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' -import { Application, Emitter } from '@adonisjs/core/build/standalone' - -import { RedisManager } from '../src/RedisManager' - -const clusterNodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { - return { host: process.env.REDIS_HOST!, port: Number(port) } -}) - -test.group('Redis Manager', () => { - test('run redis commands using default connection', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - await redis.set('greeting', 'hello-world') - const greeting = await redis.get('greeting') - - assert.equal(greeting, 'hello-world') - - await redis.del('greeting') - await redis.quit('primary') - }) - - test('run redis commands using the connection method', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - await redis.connection().set('greeting', 'hello-world') - const greeting = await redis.connection().get('greeting') - assert.equal(greeting, 'hello-world') - - await redis.connection().del('greeting') - await redis.quit('primary') - }) - - test('re-use connection when connection method is called', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - assert.deepEqual(redis.connection(), redis.connection('primary')) - await redis.quit() - }) - - test('connect to redis cluster when cluster array is defined', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'cluster', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - redis.connection('cluster').on('ready', async () => { - assert.isAbove(redis.connection('cluster').nodes().length, 2) - await redis.quit() - done() - }) - }).waitForDone() - - test('on disconnect clear connection from tracked list', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - const connection = redis.connection() - connection.on('end', () => { - assert.equal(redis.activeConnectionsCount, 0) - done() - }) - - connection.on('ready', async () => { - await redis.quit() - }) - }).waitForDone() - - test('get report for connections marked for healthChecks', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - healthCheck: true, - }, - secondary: { - host: process.env.REDIS_HOST, - port: 4444, - }, - }, - } as any, - new Emitter(app) - ) - - const report = await redis.report() - assert.deepEqual(report.health, { healthy: true, message: 'All connections are healthy' }) - assert.lengthOf(report.meta, 1) - assert.isDefined(report.meta[0].used_memory) - assert.equal(report.meta[0].status, 'ready') - await redis.quit() - }) - - test('generate correct report when one of the connections are broken', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - healthCheck: true, - }, - secondary: { - host: process.env.REDIS_HOST, - healthCheck: true, - port: 4444, - }, - }, - } as any, - new Emitter(app) - ) - - const report = await redis.report() - - assert.deepEqual(report.health, { - healthy: false, - message: 'One or more redis connections are not healthy', - }) - assert.lengthOf(report.meta, 2) - await redis.quit() - }) - - test('use pub/sub using the manager instance', async ({ assert }, done) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - redis.connection().on('subscription:ready', () => { - redis.publish('news', 'breaking news at 9') - }) - - redis.subscribe('news', async (message) => { - assert.equal(message, 'breaking news at 9') - await redis.quit() - done() - }) - }).waitForDone() - - test('execute redis commands using lua scripts', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - redis.defineCommand('defineValue', { - numberOfKeys: 1, - lua: `redis.call('set', KEYS[1], ARGV[1])`, - }) - - redis.defineCommand('readValue', { - numberOfKeys: 1, - lua: `return redis.call('get', KEYS[1])`, - }) - - await redis.runCommand('defineValue', 'greeting', 'hello world') - const greeting = await redis.runCommand('readValue', 'greeting') - assert.equal(greeting, 'hello world') - - await redis.del('greeting') - await redis.quit() - }) - - test('get and delete the key', async ({ assert }) => { - const app = new Application(__dirname, 'web', {}) - const redis = new RedisManager( - app, - { - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }, - new Emitter(app) - ) as unknown as RedisManagerContract - - await redis.set('greeting', 'hello-world') - - assert.equal(await redis.getdel('greeting'), 'hello-world') - assert.isNull(await redis.get('greeting')) - - await redis.quit('primary') - }) -}) diff --git a/test/redis-cluster-factory.spec.ts b/test/redis_cluster_connection.spec.ts similarity index 74% rename from test/redis-cluster-factory.spec.ts rename to test/redis_cluster_connection.spec.ts index 955a804..9c2c6d5 100644 --- a/test/redis-cluster-factory.spec.ts +++ b/test/redis_cluster_connection.spec.ts @@ -1,19 +1,16 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - +import { AppFactory } from '@adonisjs/core/factories/app' import { test } from '@japa/runner' -import { Application } from '@adonisjs/core/build/standalone' -import { RedisClusterConnectionContract } from '@ioc:Adonis/Addons/Redis' - -import { RedisClusterConnection } from '../src/RedisClusterConnection' +import { BASE_URL } from './redis_manager.spec.js' +import RedisClusterConnection from '../src/redis_cluster_connection.js' const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { return { host: process.env.REDIS_HOST!, port: Number(port) } @@ -23,11 +20,9 @@ test.group('Redis cluster factory', () => { test('emit ready when connected to redis server', ({ assert }, done) => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('ready', async () => { assert.isTrue(true) @@ -39,11 +34,9 @@ test.group('Redis cluster factory', () => { test('emit node connection event', ({ assert }, done) => { const factory = new RedisClusterConnection( 'main', - { - clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }], - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }] }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('node:added', async () => { assert.isTrue(true) @@ -55,11 +48,9 @@ test.group('Redis cluster factory', () => { test('execute redis commands', async ({ assert }) => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) await factory.set('greeting', 'hello world') const greeting = await factory.get('greeting') @@ -74,11 +65,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -99,8 +88,8 @@ test.group('Redis cluster factory', () => { { clusters: nodes, }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -118,11 +107,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -145,11 +132,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -168,11 +153,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -196,11 +179,9 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', - { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -221,11 +202,9 @@ test.group('Redis cluster factory', () => { test('execute redis commands using lua scripts', async ({ assert }) => { const factory = new RedisClusterConnection( 'main', - { - clusters: nodes, - }, - new Application(__dirname, 'web', {}) - ) as unknown as RedisClusterConnectionContract + { clusters: nodes }, + new AppFactory().create(BASE_URL, () => {}), + ) factory.defineCommand('defineValue', { numberOfKeys: 1, diff --git a/test/redis_connection.spec.ts b/test/redis_connection.spec.ts new file mode 100644 index 0000000..77e6283 --- /dev/null +++ b/test/redis_connection.spec.ts @@ -0,0 +1,190 @@ +import RedisConnection from '../src/redis_connection.js' +import { test } from '@japa/runner' +import { BASE_URL } from './redis_manager.spec.js' +import { AppFactory } from '@adonisjs/core/factories/app' + +test.group('Redis factory', () => { + test('emit ready when connected to redis server', ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('ready', async () => { + assert.isTrue(true) + await factory.quit() + done() + }) + }).waitForDone() + + test('execute redis commands', async ({ assert }) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + await factory.set('greeting', 'hello world') + + const greeting = await factory.get('greeting') + assert.equal(greeting, 'hello world') + + await factory.del('greeting') + await factory.quit() + }) + + test('clean event listeners on quit', async ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('ready', async () => { + await factory.quit() + }) + }).waitForDone() + + test('clean event listeners on disconnect', async ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('ready', async () => { + await factory.quit() + }) + }).waitForDone() + + test('get event for connection errors', async ({ assert }, done) => { + const factory = new RedisConnection( + 'main', + { port: 4444 }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('error', async (error) => { + assert.equal(error.code, 'ECONNREFUSED') + assert.equal(error.port, 4444) + await factory.quit() + }) + }).waitForDone() + + test('get report for connected connection', async ({ assert }, done) => { + assert.plan(5) + + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('ready', async () => { + const report = await factory.getReport(true) + + assert.equal(report.status, 'ready') + assert.isNull(report.error) + assert.isDefined(report.used_memory) + + await factory.quit() + }) + }).waitForDone() + + test('get report for errored connection', async ({ assert }, done) => { + assert.plan(5) + + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: 4444, + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.on('end', () => { + assert.equal(factory.ioConnection.listenerCount('ready'), 0) + assert.equal(factory.ioConnection.listenerCount('end'), 0) + done() + }) + + factory.on('error', async () => { + const report = await factory.getReport(true) + + assert.notEqual(report.status, 'ready') + assert.equal(report.error.code, 'ECONNREFUSED') + assert.equal(report.used_memory, null) + + await factory.quit() + }) + }).waitForDone() + + test('execute redis commands using lua scripts', async ({ assert }) => { + const factory = new RedisConnection( + 'main', + { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + new AppFactory().create(BASE_URL, () => {}), + ) + + factory.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + factory.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await factory.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await factory.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + + await factory.del('greeting') + await factory.quit() + }) +}) diff --git a/test/redis_manager.spec.ts b/test/redis_manager.spec.ts new file mode 100644 index 0000000..eda4a5d --- /dev/null +++ b/test/redis_manager.spec.ts @@ -0,0 +1,277 @@ +import { test } from '@japa/runner' +import { AppFactory } from '@adonisjs/core/factories/app' +import { RedisManagerFactory } from '../factories/redis_manager.js' +import { RedisClusterConnectionAugmented, RedisConnectionAugmented } from '../src/types/main.js' + +const clusterNodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { + return { host: process.env.REDIS_HOST!, port: Number(port) } +}) + +export const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Redis Manager', () => { + test('.connection() types should be inferrred from config', async ({ expectTypeOf }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + expectTypeOf(redis.connection).parameter(0).toEqualTypeOf<'primary' | 'secondary' | undefined>() + }) + + test('run redis commands using default connection', async ({ assert }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + await redis.set('greeting', 'hello-world') + const greeting = await redis.get('greeting') + + assert.equal(greeting, 'hello-world') + + await redis.del('greeting') + await redis.quit('primary') + }) + + test('should trigger a ready event when connection is ready', async ({ assert }, done) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + redis.connection().on('ready', async () => { + assert.isTrue(true) + await redis.quit() + done() + }) + }) + .waitForDone() + .timeout(5000) + + test('run redis commands using the connection method', async ({ assert }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + await redis.connection().set('greeting', 'hello-world') + const greeting = await redis.connection().get('greeting') + assert.equal(greeting, 'hello-world') + + await redis.connection().del('greeting') + await redis.quit('primary') + }) + + test('re-use connection when connection method is called', async ({ assert }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + assert.deepEqual(redis.connection(), redis.connection()) + await redis.quit() + }) + + test('connect to redis cluster when cluster array is defined', async ({ assert }, done) => { + const app = new AppFactory().create(BASE_URL, () => {}) + + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + cluster: { clusters: clusterNodes }, + }, + }).create(app) + + redis.connection('cluster').on('ready', async () => { + assert.isAbove(redis.connection('cluster').nodes().length, 2) + await redis.quit() + done() + }) + }).waitForDone() + + test('on disconnect clear connection from tracked list', async ({ assert }, done) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create(app) + + const connection = redis.connection() + connection.on('end', () => { + assert.equal(redis.activeConnectionsCount, 0) + done() + }) + + connection.on('ready', async () => { + await redis.quit() + }) + }).waitForDone() + + test('get report for connections marked for healthChecks', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + healthCheck: true, + }, + secondary: { + host: process.env.REDIS_HOST, + port: 4444, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + const report = await redis.report() + assert.deepEqual(report.health, { healthy: true, message: 'All connections are healthy' }) + assert.lengthOf(report.meta, 1) + assert.isDefined(report.meta[0].used_memory) + assert.equal(report.meta[0].status, 'ready') + await redis.quit() + }) + + test('generate correct report when one of the connections are broken', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + healthCheck: true, + }, + secondary: { + host: process.env.REDIS_HOST, + healthCheck: true, + port: 4444, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + const report = await redis.report() + + assert.deepEqual(report.health, { + healthy: false, + message: 'One or more redis connections are not healthy', + }) + assert.lengthOf(report.meta, 2) + await redis.quit() + }) + + test('use pub/sub using the manager instance', async ({ assert }, done) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + redis.connection().on('subscription:ready', () => { + redis.publish('news', 'breaking news at 9') + }) + + redis.subscribe('news', async (message) => { + assert.equal(message, 'breaking news at 9') + await redis.quit() + done() + }) + }).waitForDone() + + test('execute redis commands using lua scripts', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + redis.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + redis.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await redis.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await redis.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + + await redis.del('greeting') + await redis.quit() + }) + + test('get and delete the key', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + await redis.set('greeting', 'hello-world') + + assert.equal(await redis.getdel('greeting'), 'hello-world') + assert.isNull(await redis.get('greeting')) + + await redis.quit('primary') + }) + + test('should have connection types inferred from config', async ({ expectTypeOf }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + expectTypeOf(redis.connection('cluster')).toEqualTypeOf() + expectTypeOf(redis.connection('primary')).toEqualTypeOf() + }) +}) diff --git a/test/redis_provider.spec.ts b/test/redis_provider.spec.ts new file mode 100644 index 0000000..dd853fa --- /dev/null +++ b/test/redis_provider.spec.ts @@ -0,0 +1,56 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { IgnitorFactory } from '@adonisjs/core/factories' +import { test } from '@japa/runner' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Redis Provider', () => { + test('register redis provider', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['./providers/redis_provider.js'], + }, + }) + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => import(new URL(filePath, new URL('../', import.meta.url)).href), + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + assert.isTrue(app.container.hasBinding('redis')) + }) + + test('define repl bindings', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: ['../providers/redis_provider.js'], + }, + }) + .withCoreProviders() + .create(BASE_URL, { + importer: (filePath) => import(filePath), + }) + + const app = ignitor.createApp('repl') + await app.init() + await app.boot() + + const repl = await app.container.make('repl') + assert.property(repl.getMethods(), 'loadRedis') + assert.isFunction(repl.getMethods().loadRedis.handler) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index db7deb4..ad0cc44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,7 @@ { - "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", + "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { - "esModuleInterop": true, - "skipLibCheck": true, - "types": [ - "@adonisjs/core", - "@adonisjs/repl", - "@types/node" - ] + "rootDir": "./", + "outDir": "./build" } } From 20b3064fb0800c8d067405403086693c355a1684 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:12:30 +0200 Subject: [PATCH 03/71] chore: remove unused dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index a7d2e35..4d5c373 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "@japa/expect-type": "2.0.0-0", "@japa/file-system": "2.0.0-1", "@japa/runner": "3.0.0-6", - "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.64", "@types/node": "^20.3.1", "copyfiles": "^2.4.1", From 0dc04d2ad85416b2159e7492525eb416590d8fe9 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:13:55 +0200 Subject: [PATCH 04/71] chore: update dependencies --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 4d5c373..59cf777 100644 --- a/package.json +++ b/package.json @@ -49,25 +49,25 @@ }, "devDependencies": { "@adonisjs/core": "^6.1.5-8", - "@adonisjs/eslint-config": "^1.1.2", - "@adonisjs/prettier-config": "^1.1.2", - "@adonisjs/tsconfig": "^1.1.2", + "@adonisjs/eslint-config": "^1.1.8", + "@adonisjs/prettier-config": "^1.1.8", + "@adonisjs/tsconfig": "^1.1.8", "@japa/assert": "2.0.0-1", "@japa/expect-type": "2.0.0-0", "@japa/file-system": "2.0.0-1", "@japa/runner": "3.0.0-6", "@swc/core": "^1.3.64", - "@types/node": "^20.3.1", + "@types/node": "^20.4.2", "copyfiles": "^2.4.1", "del-cli": "^5.0.0", - "dotenv": "^16.3.0", - "eslint": "^8.43.0", + "dotenv": "^16.3.1", + "eslint": "^8.45.0", "github-label-sync": "^2.3.1", - "husky": "^8.0.1", - "np": "^7.6.2", - "prettier": "^2.8.8", + "husky": "^8.0.3", + "np": "^8.0.4", + "prettier": "^3.0.0", "ts-node": "^10.9.1", - "typescript": "^5.1.3" + "typescript": "^5.1.6" }, "dependencies": { "@poppinss/utils": "6.5.0-3", @@ -91,7 +91,7 @@ "ioredis" ], "eslintConfig": { - "extends": "@adonisjs/eslint-config/typescript-package" + "extends": "@adonisjs/eslint-config/package" }, "prettier": "@adonisjs/prettier-config", "commitlint": { From 9276f828a8eefe21e8cc2c2258a35b16ca6f070d Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:14:50 +0200 Subject: [PATCH 05/71] style: lint files --- src/abstract_connection.ts | 9 ++++++--- src/define_config.ts | 4 ++-- src/redis_cluster_connection.ts | 2 +- src/redis_connection.ts | 2 +- src/redis_manager.ts | 20 ++++++++++---------- src/repl_bindings.ts | 6 +++--- src/types/main.ts | 10 +++++----- test/define_config.spec.ts | 4 ++-- test/redis_cluster_connection.spec.ts | 20 ++++++++++---------- test/redis_connection.spec.ts | 16 ++++++++-------- 10 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/abstract_connection.ts b/src/abstract_connection.ts index 9b7e227..695cf26 100644 --- a/src/abstract_connection.ts +++ b/src/abstract_connection.ts @@ -87,7 +87,10 @@ export abstract class AbstractConnection extends Even */ protected abstract makeSubscriberConnection(): void - constructor(public connectionName: string, application: ApplicationService) { + constructor( + public connectionName: string, + application: ApplicationService + ) { super() } @@ -177,11 +180,11 @@ export abstract class AbstractConnection extends Even this.ioSubscriberConnection!.on('connect', () => this.emit('subscriber:connect', this)) this.ioSubscriberConnection!.on('ready', () => this.emit('subscriber:ready', this)) this.ioSubscriberConnection!.on('error', (error: any) => - this.emit('subscriber:error', error, this), + this.emit('subscriber:error', error, this) ) this.ioSubscriberConnection!.on('close', () => this.emit('subscriber:close', this)) this.ioSubscriberConnection!.on('reconnecting', () => - this.emit('subscriber:reconnecting', this), + this.emit('subscriber:reconnecting', this) ) /** diff --git a/src/define_config.ts b/src/define_config.ts index 5ccc094..84bf469 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -24,7 +24,7 @@ type RedisConfig = { * Define config for redis */ export function defineConfig( - config: T, + config: T ): T { if (!config) { throw new InvalidArgumentsException('Invalid config. It must be a valid object') @@ -36,7 +36,7 @@ export function defineConfig { protected makeSubscriberConnection() { this.ioSubscriberConnection = new Redis.Cluster( this.#config.clusters as [], - this.#config.clusterOptions, + this.#config.clusterOptions ) } diff --git a/src/redis_connection.ts b/src/redis_connection.ts index 317609e..929ad98 100644 --- a/src/redis_connection.ts +++ b/src/redis_connection.ts @@ -26,7 +26,7 @@ export class RawRedisConnection extends AbstractConnection { constructor( connectionName: string, config: RedisConnectionConfig, - application: ApplicationService, + application: ApplicationService ) { super(connectionName, application) this.#config = this.#normalizeConfig(config) diff --git a/src/redis_manager.ts b/src/redis_manager.ts index 1166c9a..c3b0b95 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -65,13 +65,13 @@ export class RawRedisManager { connection: keyof ConnectionList connections: ConnectionList }, - emitter: EmitterService, + emitter: EmitterService ) { this.#app = app this.#config = config this.#emitter = emitter this.#healthCheckConnections = Object.keys(this.#config.connections).filter( - (connection) => this.#config.connections[connection].healthCheck, + (connection) => this.#config.connections[connection].healthCheck ) } @@ -107,22 +107,22 @@ export class RawRedisManager { this.#emitter.emit('redis:ready', { connection }) }) connection.on('ready', ($connection) => - this.#emitter.emit('redis:ready', { connection: $connection }), + this.#emitter.emit('redis:ready', { connection: $connection }) ) connection.on('connect', ($connection) => - this.#emitter.emit('redis:connect', { connection: $connection }), + this.#emitter.emit('redis:connect', { connection: $connection }) ) connection.on('error', (error, $connection) => - this.#emitter.emit('redis:error', { error, connection: $connection }), + this.#emitter.emit('redis:error', { error, connection: $connection }) ) connection.on('node:added', ($connection, node) => - this.#emitter.emit('redis:node:added', { node, connection: $connection }), + this.#emitter.emit('redis:node:added', { node, connection: $connection }) ) connection.on('node:removed', (node, $connection) => - this.#emitter.emit('redis:node:removed', { node, connection: $connection }), + this.#emitter.emit('redis:node:removed', { node, connection: $connection }) ) connection.on('node:error', (error, address, $connection) => - this.#emitter.emit('redis:node:error', { error, address, connection: $connection }), + this.#emitter.emit('redis:node:error', { error, address, connection: $connection }) ) /** @@ -138,7 +138,7 @@ export class RawRedisManager { * Returns redis factory for a given named connection */ connection( - connectionName?: ConnectionName, + connectionName?: ConnectionName ): GetConnectionType { const name = connectionName || this.#getDefaultConnection() @@ -223,7 +223,7 @@ export class RawRedisManager { */ async report() { const reports = await Promise.all( - this.#healthCheckConnections.map((connection) => this.connection(connection).getReport(true)), + this.#healthCheckConnections.map((connection) => this.connection(connection).getReport(true)) ) const healthy = !reports.find((report) => !!report.error) diff --git a/src/repl_bindings.ts b/src/repl_bindings.ts index 522ff36..7aadd15 100644 --- a/src/repl_bindings.ts +++ b/src/repl_bindings.ts @@ -21,10 +21,10 @@ export function defineReplBindings(app: ApplicationService, repl: Repl) { repl.server!.context.redis = await app.container.make('redis') repl.notify( `Loaded "redis" service. You can access it using the "${repl.colors.underline( - 'redis', - )}" variable`, + 'redis' + )}" variable` ) }, - { description: 'Load "redis" service in the REPL context' }, + { description: 'Load "redis" service in the REPL context' } ) } diff --git a/src/types/main.ts b/src/types/main.ts index aeb67c8..968f2c3 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -35,7 +35,7 @@ import { RawRedisManager } from '../redis_manager.js' export type PubSubChannelHandler = (data: T) => Promise | void export type PubSubPatternHandler = ( channel: string, - data: T, + data: T ) => Promise | void /** @@ -45,7 +45,7 @@ export interface RedisPubSubContract { publish( channel: string, message: string, - callback: (error: Error | null, count: number) => void, + callback: (error: Error | null, count: number) => void ): void publish(channel: string, message: string): Promise subscribe(channel: string, handler: PubSubChannelHandler | string): void @@ -121,7 +121,7 @@ export type RedisClusterConnectionFactory = { new ( connectionName: string, config: RedisClusterConfig, - application: ApplicationService, + application: ApplicationService ): RawRedisClusterConnection & AbstractConnection & IORedisCommands & RedisPubSubContract } @@ -129,7 +129,7 @@ export type RedisConnectionFactory = { new ( connectionName: string, config: RedisConnectionConfig, - application: ApplicationService, + application: ApplicationService ): RawRedisConnection & AbstractConnection & IORedisCommands & RedisPubSubContract } @@ -140,7 +140,7 @@ export type RedisManagerFactory = { connection: keyof ConnectionList connections: ConnectionList }, - emitter: Emitter, + emitter: Emitter ): RawRedisManager & IORedisCommands & RedisPubSubContract } diff --git a/test/define_config.spec.ts b/test/define_config.spec.ts index 18d681d..f3f75d9 100644 --- a/test/define_config.spec.ts +++ b/test/define_config.spec.ts @@ -20,7 +20,7 @@ test.group('Define Config', () => { assert.throws( // @ts-ignore () => defineConfig({ connection: 'hey' }), - 'Invalid config. Missing property "connections" inside it', + 'Invalid config. Missing property "connections" inside it' ) }) @@ -32,7 +32,7 @@ test.group('Define Config', () => { connection: 'hey', connections: {}, }), - 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object', + 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object' ) }) }) diff --git a/test/redis_cluster_connection.spec.ts b/test/redis_cluster_connection.spec.ts index 9c2c6d5..c0da4bf 100644 --- a/test/redis_cluster_connection.spec.ts +++ b/test/redis_cluster_connection.spec.ts @@ -21,7 +21,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('ready', async () => { @@ -35,7 +35,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }] }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('node:added', async () => { @@ -49,7 +49,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) await factory.set('greeting', 'hello world') @@ -66,7 +66,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -88,7 +88,7 @@ test.group('Redis cluster factory', () => { { clusters: nodes, }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -108,7 +108,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -133,7 +133,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -154,7 +154,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -180,7 +180,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -203,7 +203,7 @@ test.group('Redis cluster factory', () => { const factory = new RedisClusterConnection( 'main', { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.defineCommand('defineValue', { diff --git a/test/redis_connection.spec.ts b/test/redis_connection.spec.ts index 77e6283..5f72bdb 100644 --- a/test/redis_connection.spec.ts +++ b/test/redis_connection.spec.ts @@ -11,7 +11,7 @@ test.group('Redis factory', () => { host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('ready', async () => { @@ -28,7 +28,7 @@ test.group('Redis factory', () => { host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) await factory.set('greeting', 'hello world') @@ -47,7 +47,7 @@ test.group('Redis factory', () => { host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -68,7 +68,7 @@ test.group('Redis factory', () => { host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -86,7 +86,7 @@ test.group('Redis factory', () => { const factory = new RedisConnection( 'main', { port: 4444 }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -111,7 +111,7 @@ test.group('Redis factory', () => { host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -140,7 +140,7 @@ test.group('Redis factory', () => { host: process.env.REDIS_HOST, port: 4444, }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.on('end', () => { @@ -167,7 +167,7 @@ test.group('Redis factory', () => { host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), }, - new AppFactory().create(BASE_URL, () => {}), + new AppFactory().create(BASE_URL, () => {}) ) factory.defineCommand('defineValue', { From 5f85d09e2998e3a279ccb1d33da1c8d5be429fd4 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:17:12 +0200 Subject: [PATCH 06/71] chore: cleanup pkg json --- package.json | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 59cf777..c5ed717 100644 --- a/package.json +++ b/package.json @@ -5,25 +5,16 @@ "main": "build/index.js", "type": "module", "files": [ - "index.ts", - "configure.ts", - "src", - "services", - "providers", - "factories", "build/configure.js", "build/configure.d.ts", - "build/configure.d.ts.map", "build/index.js", "build/index.d.ts", - "build/index.d.ts.map", "build/src", "build/services", "build/providers", "build/factories", "build/stubs", "build/index.d.ts", - "build/index.d.ts.map", "build/index.js" ], "exports": { @@ -34,18 +25,18 @@ }, "scripts": { "pretest": "npm run lint", - "test": "docker-compose build && docker-compose run --rm tests", - "quick:test": "node --experimental-import-meta-resolve --enable-source-maps --loader=ts-node/esm ./bin/test.ts", + "test": "c8 npm run quick:test", + "quick:test": "node --enable-source-maps --loader=ts-node/esm ./bin/test.ts", "clean": "del-cli build", "test:docker": "FORCE_COLOR=true node --loader=ts-node/esm ./bin/test.ts", - "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", - "compile": "npm run lint && npm run clean && tsc", - "build": "npm run compile && npm run copyfiles", + "copy:templates": "copyfiles \"stubs/**/**/*.stub\" build", + "compile": "npm run lint && npm run clean && tsc && npm run copy:templates", + "build": "npm run compile", "release": "np --message=\"chore(release): %s\"", "version": "npm run build", "prepublishOnly": "npm run build", "lint": "eslint . --ext=.ts", - "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/logger" + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { "@adonisjs/core": "^6.1.5-8", @@ -56,7 +47,7 @@ "@japa/expect-type": "2.0.0-0", "@japa/file-system": "2.0.0-1", "@japa/runner": "3.0.0-6", - "@swc/core": "^1.3.64", + "@swc/core": "^1.3.69", "@types/node": "^20.4.2", "copyfiles": "^2.4.1", "del-cli": "^5.0.0", From 9280aa628dcfad5c1750b0ea2d059b917e031da3 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:18:47 +0200 Subject: [PATCH 07/71] chore: add headers --- factories/redis_manager.ts | 9 +++++++++ src/redis_manager.ts | 9 +++++++++ src/types/main.ts | 13 ------------- test/redis_connection.spec.ts | 9 +++++++++ test/redis_manager.spec.ts | 9 +++++++++ 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/factories/redis_manager.ts b/factories/redis_manager.ts index cdd436b..a7f4891 100644 --- a/factories/redis_manager.ts +++ b/factories/redis_manager.ts @@ -1,3 +1,12 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import { EmitterFactory } from '@adonisjs/core/factories/events' import { Application } from '@adonisjs/core/app' import RedisManager from '../src/redis_manager.js' diff --git a/src/redis_manager.ts b/src/redis_manager.ts index c3b0b95..b6bbb5f 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -1,3 +1,12 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import { ApplicationService, EmitterService } from '@adonisjs/core/types' import RedisConnection from './redis_connection.js' import { pubsubMethods } from './pubsub_methods.js' diff --git a/src/types/main.ts b/src/types/main.ts index 968f2c3..8d4c609 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -16,19 +16,6 @@ import { RawRedisConnection } from '../redis_connection.js' import { Emitter } from '@adonisjs/core/events' import { RawRedisManager } from '../redis_manager.js' -/* - |-------------------------------------------------------------------------- - | Helpers - |-------------------------------------------------------------------------- - */ -/** - * Returns factory for a given connection by inspecting it's config. - */ -// type GetConnectionFactoryType = -// RedisConnectionsList[T] extends RedisClusterConfig -// ? RedisClusterConnectionContract -// : RedisConnectionContract - /** * Pubsub subscriber */ diff --git a/test/redis_connection.spec.ts b/test/redis_connection.spec.ts index 5f72bdb..0ecadda 100644 --- a/test/redis_connection.spec.ts +++ b/test/redis_connection.spec.ts @@ -1,3 +1,12 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import RedisConnection from '../src/redis_connection.js' import { test } from '@japa/runner' import { BASE_URL } from './redis_manager.spec.js' diff --git a/test/redis_manager.spec.ts b/test/redis_manager.spec.ts index eda4a5d..cef6794 100644 --- a/test/redis_manager.spec.ts +++ b/test/redis_manager.spec.ts @@ -1,3 +1,12 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import { test } from '@japa/runner' import { AppFactory } from '@adonisjs/core/factories/app' import { RedisManagerFactory } from '../factories/redis_manager.js' From 32ae9e6768f2576c372aca9c71b66c70a3060796 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:19:14 +0200 Subject: [PATCH 08/71] chore: add typecheck script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c5ed717..78ab8ee 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "version": "npm run build", "prepublishOnly": "npm run build", "lint": "eslint . --ext=.ts", + "typecheck": "tsc --noEmit", "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { From 504627456caacb98eb42bae4cc494089382c9895 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 22:19:42 +0200 Subject: [PATCH 09/71] chore: remove unused script --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 78ab8ee..b2ac810 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "test": "c8 npm run quick:test", "quick:test": "node --enable-source-maps --loader=ts-node/esm ./bin/test.ts", "clean": "del-cli build", - "test:docker": "FORCE_COLOR=true node --loader=ts-node/esm ./bin/test.ts", "copy:templates": "copyfiles \"stubs/**/**/*.stub\" build", "compile": "npm run lint && npm run clean && tsc && npm run copy:templates", "build": "npm run compile", From 47c33cf874a179967b55696a128d1971268f2827 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 15 Jul 2023 23:52:49 +0200 Subject: [PATCH 10/71] refactor: some cleanup --- factories/redis_manager.ts | 13 +++- index.ts | 2 + package.json | 1 + providers/redis_provider.ts | 2 +- src/abstract_connection.ts | 7 +- src/redis_cluster_connection.ts | 5 +- src/redis_connection.ts | 9 +-- src/redis_manager.ts | 22 ++----- src/types/extended.ts | 22 +++---- src/types/main.ts | 78 +++++++++++++++-------- test/redis_cluster_connection.spec.ts | 72 ++++++--------------- test/redis_connection.spec.ts | 92 +++++++++------------------ test/redis_manager.spec.ts | 6 +- 13 files changed, 137 insertions(+), 194 deletions(-) diff --git a/factories/redis_manager.ts b/factories/redis_manager.ts index a7f4891..56b1485 100644 --- a/factories/redis_manager.ts +++ b/factories/redis_manager.ts @@ -7,11 +7,15 @@ * file that was distributed with this source code. */ +import type { Application } from '@adonisjs/core/app' +import type { RedisClusterConfig, RedisConnectionConfig } from '../src/types/main.js' import { EmitterFactory } from '@adonisjs/core/factories/events' -import { Application } from '@adonisjs/core/app' import RedisManager from '../src/redis_manager.js' -import { RedisClusterConfig, RedisConnectionConfig } from '../src/types/main.js' +/** + * Redis manager factory is used to create an instance of the redis + * manager for testing + */ export class RedisManagerFactory< ConnectionsList extends Record, > { @@ -24,7 +28,10 @@ export class RedisManagerFactory< this.#config = config } + /** + * Create an instance of the redis manager + */ create(app: Application) { - return new RedisManager(app, this.#config, new EmitterFactory().create(app)) + return new RedisManager(this.#config, new EmitterFactory().create(app)) } } diff --git a/index.ts b/index.ts index 3b91c2c..c9f9f12 100644 --- a/index.ts +++ b/index.ts @@ -8,3 +8,5 @@ */ import './src/types/extended.js' + +export { defineConfig } from './src/define_config.js' diff --git a/package.json b/package.json index b2ac810..4532406 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "exports": { ".": "./build/index.js", "./services/main": "./build/services/main.js", + "./providers/redis_provider": "./build/providers/redis_provider.js", "./factories": "./build/factories/main.js", "./types": "./build/src/types/main.js" }, diff --git a/providers/redis_provider.ts b/providers/redis_provider.ts index ec12b9e..a510c60 100644 --- a/providers/redis_provider.ts +++ b/providers/redis_provider.ts @@ -37,7 +37,7 @@ export default class RedisProvider { const emitter = await this.app.container.make('emitter') const config = this.app.config.get('redis', {}) - return new RedisManager(this.app, config, emitter) + return new RedisManager(config, emitter) }) } diff --git a/src/abstract_connection.ts b/src/abstract_connection.ts index 695cf26..2e52058 100644 --- a/src/abstract_connection.ts +++ b/src/abstract_connection.ts @@ -10,8 +10,6 @@ import { EventEmitter } from 'node:events' import { Redis, Cluster } from 'ioredis' import { Exception } from '@poppinss/utils' - -import { ApplicationService } from '@adonisjs/core/types' import { PubSubChannelHandler, PubSubPatternHandler, HealthReportNode } from './types/main.js' /** @@ -87,10 +85,7 @@ export abstract class AbstractConnection extends Even */ protected abstract makeSubscriberConnection(): void - constructor( - public connectionName: string, - application: ApplicationService - ) { + constructor(public connectionName: string) { super() } diff --git a/src/redis_cluster_connection.ts b/src/redis_cluster_connection.ts index b477102..d904a3a 100644 --- a/src/redis_cluster_connection.ts +++ b/src/redis_cluster_connection.ts @@ -11,7 +11,6 @@ import Redis, { Cluster, NodeRole } from 'ioredis' import { ioMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' -import { ApplicationService } from '@adonisjs/core/types' import { RedisClusterConfig, RedisClusterConnectionFactory } from './types/main.js' /** @@ -22,8 +21,8 @@ import { RedisClusterConfig, RedisClusterConnectionFactory } from './types/main. export class RawRedisClusterConnection extends AbstractConnection { #config: RedisClusterConfig - constructor(connectionName: string, config: RedisClusterConfig, application: ApplicationService) { - super(connectionName, application) + constructor(connectionName: string, config: RedisClusterConfig) { + super(connectionName) this.#config = config this.ioConnection = new Redis.Cluster(this.#config.clusters as any[]) diff --git a/src/redis_connection.ts b/src/redis_connection.ts index 929ad98..0c31747 100644 --- a/src/redis_connection.ts +++ b/src/redis_connection.ts @@ -12,7 +12,6 @@ import { Redis, RedisOptions } from 'ioredis' import { ioMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' import { RedisConnectionConfig, RedisConnectionFactory } from './types/main.js' -import { ApplicationService } from '@adonisjs/core/types' /** * Redis connection exposes the API to run Redis commands using `ioredis` as the @@ -23,12 +22,8 @@ import { ApplicationService } from '@adonisjs/core/types' export class RawRedisConnection extends AbstractConnection { #config: RedisOptions - constructor( - connectionName: string, - config: RedisConnectionConfig, - application: ApplicationService - ) { - super(connectionName, application) + constructor(connectionName: string, config: RedisConnectionConfig) { + super(connectionName) this.#config = this.#normalizeConfig(config) this.ioConnection = new Redis(this.#config) diff --git a/src/redis_manager.ts b/src/redis_manager.ts index b6bbb5f..957a573 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -7,14 +7,14 @@ * file that was distributed with this source code. */ -import { ApplicationService, EmitterService } from '@adonisjs/core/types' +import { EmitterService } from '@adonisjs/core/types' import RedisConnection from './redis_connection.js' import { pubsubMethods } from './pubsub_methods.js' import { ioMethods } from './io_methods.js' import { Connection, GetConnectionType, - RedisConnectionAugmented, + RedisConnectionContract, RedisConnectionsList, RedisManagerFactory, } from './types/main.js' @@ -29,11 +29,6 @@ export class RawRedisManager { connections: ConnectionList } - /** - * Reference to the application - */ - #app: ApplicationService - /** * Reference to the emitter */ @@ -69,14 +64,9 @@ export class RawRedisManager { } constructor( - app: ApplicationService, - config: { - connection: keyof ConnectionList - connections: ConnectionList - }, + config: { connection: keyof ConnectionList; connections: ConnectionList }, emitter: EmitterService ) { - this.#app = app this.#config = config this.#emitter = emitter this.#healthCheckConnections = Object.keys(this.#config.connections).filter( @@ -171,8 +161,8 @@ export class RawRedisManager { */ const connection = 'clusters' in config - ? new RedisClusterConnection(name as string, config, this.#app) - : new RedisConnection(name as string, config, this.#app) + ? new RedisClusterConnection(name as string, config) + : new RedisConnection(name as string, config) /** * Cache the connection so that we can re-use it later @@ -252,7 +242,7 @@ export class RawRedisManager { * Define a custom command using LUA script. You can run the * registered command using the "runCommand" method. */ - defineCommand(...args: Parameters): this { + defineCommand(...args: Parameters): this { this.connection().defineCommand(...args) return this } diff --git a/src/types/extended.ts b/src/types/extended.ts index 42303be..82cdfeb 100644 --- a/src/types/extended.ts +++ b/src/types/extended.ts @@ -8,28 +8,28 @@ */ import type { - RedisClusterConnectionAugmented, - RedisConnectionAugmented, - RedisManagerAugmented, + RedisClusterConnectionContract, + RedisConnectionContract, + RedisManagerContract, } from './main.js' import { Redis } from 'ioredis' declare module '@adonisjs/core/types' { export interface ContainerBindings { - redis: RedisManagerAugmented + redis: RedisManagerContract } export interface EventsList { - 'redis:ready': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } - 'redis:connect': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + 'redis:ready': { connection: RedisClusterConnectionContract | RedisConnectionContract } + 'redis:connect': { connection: RedisClusterConnectionContract | RedisConnectionContract } 'redis:error': { error: any - connection: RedisClusterConnectionAugmented | RedisConnectionAugmented + connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:end': { connection: RedisClusterConnectionAugmented | RedisConnectionAugmented } + 'redis:end': { connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:node:added': { connection: RedisClusterConnectionAugmented; node: Redis } - 'redis:node:removed': { connection: RedisClusterConnectionAugmented; node: Redis } - 'redis:node:error': { error: any; connection: RedisClusterConnectionAugmented; address: string } + 'redis:node:added': { connection: RedisClusterConnectionContract; node: Redis } + 'redis:node:removed': { connection: RedisClusterConnectionContract; node: Redis } + 'redis:node:error': { error: any; connection: RedisClusterConnectionContract; address: string } } } diff --git a/src/types/main.ts b/src/types/main.ts index 8d4c609..830551e 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -10,7 +10,6 @@ import { EventEmitter } from 'node:events' import { Redis as IoRedis, RedisOptions, ClusterOptions, Cluster } from 'ioredis' import { RawRedisClusterConnection } from '../redis_cluster_connection.js' -import { ApplicationService } from '@adonisjs/core/types' import { AbstractConnection } from '../abstract_connection.js' import { RawRedisConnection } from '../redis_connection.js' import { Emitter } from '@adonisjs/core/events' @@ -71,15 +70,26 @@ export type IORedisCommands = Omit< | keyof EventEmitter > -export type Connection = RedisClusterConnectionAugmented | RedisConnectionAugmented +/** + * A connection. Can be a cluster or a single connection + */ +export type Connection = RedisClusterConnectionContract | RedisConnectionContract + +/** + * Shape of the connections list + */ export type RedisConnectionsList = Record +/** + * Extract the connection type ( either Cluster or single ) from + * a given RedisConnectionsList + */ export type GetConnectionType< ConnectionsList extends RedisConnectionsList, T extends keyof ConnectionsList, > = ConnectionsList[T] extends RedisClusterConfig - ? RedisClusterConnectionAugmented - : RedisConnectionAugmented + ? RedisClusterConnectionContract + : RedisConnectionContract /** * Shape of standard Redis connection config @@ -104,35 +114,49 @@ export type RedisClusterConfig = { * typescript about it. So the below types represents theses classes with * all the methods added */ + +/** + * Type of RedisClusterConnection with dynamically added methods + */ +export type RedisClusterConnectionContract = RawRedisClusterConnection & + AbstractConnection & + IORedisCommands & + RedisPubSubContract + +/** + * Type of RedisConnection with dynamically added methods + */ +export type RedisConnectionContract = RawRedisConnection & + AbstractConnection & + IORedisCommands & + RedisPubSubContract + +/** + * Type of RedisManager with dynamically added methods + */ +export type RedisManagerContract = + RawRedisManager & IORedisCommands & RedisPubSubContract + +/** + * Factory for creating RedisClusterConnection + */ export type RedisClusterConnectionFactory = { - new ( - connectionName: string, - config: RedisClusterConfig, - application: ApplicationService - ): RawRedisClusterConnection & AbstractConnection & IORedisCommands & RedisPubSubContract + new (connectionName: string, config: RedisClusterConfig): RedisClusterConnectionContract } +/** + * Factory for creating RedisConnection + */ export type RedisConnectionFactory = { - new ( - connectionName: string, - config: RedisConnectionConfig, - application: ApplicationService - ): RawRedisConnection & AbstractConnection & IORedisCommands & RedisPubSubContract + new (connectionName: string, config: RedisConnectionConfig): RedisConnectionContract } +/** + * Factory for creating RedisManager + */ export type RedisManagerFactory = { - new >( - application: ApplicationService, - config: { - connection: keyof ConnectionList - connections: ConnectionList - }, + new ( + config: { connection: keyof ConnectionList; connections: ConnectionList }, emitter: Emitter - ): RawRedisManager & IORedisCommands & RedisPubSubContract + ): RedisManagerContract } - -type ConstructorReturnType = T extends { new (...args: any[]): infer U } ? U : never - -export type RedisClusterConnectionAugmented = ConstructorReturnType -export type RedisConnectionAugmented = ConstructorReturnType -export type RedisManagerAugmented = ConstructorReturnType diff --git a/test/redis_cluster_connection.spec.ts b/test/redis_cluster_connection.spec.ts index c0da4bf..8667e10 100644 --- a/test/redis_cluster_connection.spec.ts +++ b/test/redis_cluster_connection.spec.ts @@ -7,9 +7,7 @@ * file that was distributed with this source code. */ -import { AppFactory } from '@adonisjs/core/factories/app' import { test } from '@japa/runner' -import { BASE_URL } from './redis_manager.spec.js' import RedisClusterConnection from '../src/redis_cluster_connection.js' const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { @@ -18,11 +16,7 @@ const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { test.group('Redis cluster factory', () => { test('emit ready when connected to redis server', ({ assert }, done) => { - const factory = new RedisClusterConnection( - 'main', - { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { clusters: nodes }) factory.on('ready', async () => { assert.isTrue(true) @@ -32,11 +26,9 @@ test.group('Redis cluster factory', () => { }).waitForDone() test('emit node connection event', ({ assert }, done) => { - const factory = new RedisClusterConnection( - 'main', - { clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }] }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { + clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }], + }) factory.on('node:added', async () => { assert.isTrue(true) @@ -46,11 +38,7 @@ test.group('Redis cluster factory', () => { }).waitForDone() test('execute redis commands', async ({ assert }) => { - const factory = new RedisClusterConnection( - 'main', - { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { clusters: nodes }) await factory.set('greeting', 'hello world') const greeting = await factory.get('greeting') @@ -63,11 +51,7 @@ test.group('Redis cluster factory', () => { test('clean event listeners on quit', async ({ assert }, done) => { assert.plan(2) - const factory = new RedisClusterConnection( - 'main', - { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { clusters: nodes }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -83,13 +67,9 @@ test.group('Redis cluster factory', () => { test('clean event listeners on disconnect', async ({ assert }, done) => { assert.plan(2) - const factory = new RedisClusterConnection( - 'main', - { - clusters: nodes, - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { + clusters: nodes, + }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -105,11 +85,9 @@ test.group('Redis cluster factory', () => { test('get event for connection errors', async ({ assert }, done) => { assert.plan(2) - const factory = new RedisClusterConnection( - 'main', - { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { + clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], + }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -130,11 +108,7 @@ test.group('Redis cluster factory', () => { test('access cluster nodes', async ({ assert }, done) => { assert.plan(3) - const factory = new RedisClusterConnection( - 'main', - { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { clusters: nodes }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -151,11 +125,7 @@ test.group('Redis cluster factory', () => { test('get report for connected connection', async ({ assert }, done) => { assert.plan(5) - const factory = new RedisClusterConnection( - 'main', - { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { clusters: nodes }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -177,11 +147,9 @@ test.group('Redis cluster factory', () => { test('get report for errored connection', async ({ assert }, done) => { assert.plan(5) - const factory = new RedisClusterConnection( - 'main', - { clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }] }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { + clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], + }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -200,11 +168,7 @@ test.group('Redis cluster factory', () => { }).waitForDone() test('execute redis commands using lua scripts', async ({ assert }) => { - const factory = new RedisClusterConnection( - 'main', - { clusters: nodes }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisClusterConnection('main', { clusters: nodes }) factory.defineCommand('defineValue', { numberOfKeys: 1, diff --git a/test/redis_connection.spec.ts b/test/redis_connection.spec.ts index 0ecadda..e42cc2b 100644 --- a/test/redis_connection.spec.ts +++ b/test/redis_connection.spec.ts @@ -9,19 +9,13 @@ import RedisConnection from '../src/redis_connection.js' import { test } from '@japa/runner' -import { BASE_URL } from './redis_manager.spec.js' -import { AppFactory } from '@adonisjs/core/factories/app' test.group('Redis factory', () => { test('emit ready when connected to redis server', ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) factory.on('ready', async () => { assert.isTrue(true) @@ -31,14 +25,10 @@ test.group('Redis factory', () => { }).waitForDone() test('execute redis commands', async ({ assert }) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) await factory.set('greeting', 'hello world') @@ -50,14 +40,10 @@ test.group('Redis factory', () => { }) test('clean event listeners on quit', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -71,14 +57,10 @@ test.group('Redis factory', () => { }).waitForDone() test('clean event listeners on disconnect', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -92,11 +74,7 @@ test.group('Redis factory', () => { }).waitForDone() test('get event for connection errors', async ({ assert }, done) => { - const factory = new RedisConnection( - 'main', - { port: 4444 }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { port: 4444 }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -114,14 +92,10 @@ test.group('Redis factory', () => { test('get report for connected connection', async ({ assert }, done) => { assert.plan(5) - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -143,14 +117,10 @@ test.group('Redis factory', () => { test('get report for errored connection', async ({ assert }, done) => { assert.plan(5) - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: 4444, - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: 4444, + }) factory.on('end', () => { assert.equal(factory.ioConnection.listenerCount('ready'), 0) @@ -170,14 +140,10 @@ test.group('Redis factory', () => { }).waitForDone() test('execute redis commands using lua scripts', async ({ assert }) => { - const factory = new RedisConnection( - 'main', - { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - new AppFactory().create(BASE_URL, () => {}) - ) + const factory = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) factory.defineCommand('defineValue', { numberOfKeys: 1, diff --git a/test/redis_manager.spec.ts b/test/redis_manager.spec.ts index cef6794..1466738 100644 --- a/test/redis_manager.spec.ts +++ b/test/redis_manager.spec.ts @@ -10,7 +10,7 @@ import { test } from '@japa/runner' import { AppFactory } from '@adonisjs/core/factories/app' import { RedisManagerFactory } from '../factories/redis_manager.js' -import { RedisClusterConnectionAugmented, RedisConnectionAugmented } from '../src/types/main.js' +import { RedisClusterConnectionContract, RedisConnectionContract } from '../src/types/main.js' const clusterNodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { return { host: process.env.REDIS_HOST!, port: Number(port) } @@ -280,7 +280,7 @@ test.group('Redis Manager', () => { }, }).create(new AppFactory().create(BASE_URL, () => {})) - expectTypeOf(redis.connection('cluster')).toEqualTypeOf() - expectTypeOf(redis.connection('primary')).toEqualTypeOf() + expectTypeOf(redis.connection('cluster')).toEqualTypeOf() + expectTypeOf(redis.connection('primary')).toEqualTypeOf() }) }) From 2b9143fb9d52900ae2c714359bcd34f57b825b56 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 00:11:29 +0200 Subject: [PATCH 11/71] fix: RedisService typing --- src/types/extended.ts | 4 ++-- src/types/main.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/types/extended.ts b/src/types/extended.ts index 82cdfeb..d43b747 100644 --- a/src/types/extended.ts +++ b/src/types/extended.ts @@ -10,13 +10,13 @@ import type { RedisClusterConnectionContract, RedisConnectionContract, - RedisManagerContract, + RedisService, } from './main.js' import { Redis } from 'ioredis' declare module '@adonisjs/core/types' { export interface ContainerBindings { - redis: RedisManagerContract + redis: RedisService } export interface EventsList { diff --git a/src/types/main.ts b/src/types/main.ts index 830551e..b3208bd 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -160,3 +160,18 @@ export type RedisManagerFactory = { emitter: Emitter ): RedisManagerContract } + +/** + * List of connections inferred from user config + */ +export interface RedisConnections {} +export type InferConnections = T['connections'] + +/** + * Redis service is a singleton redis instance registered + * to the container + */ +export interface RedisService + extends RedisManagerContract< + RedisConnections extends RedisConnectionsList ? RedisConnections : never + > {} From 48cb1c258073667fd2d8a7a357835ba7ce089cb1 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 00:12:28 +0200 Subject: [PATCH 12/71] feat: add main service --- services/main.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 services/main.ts diff --git a/services/main.ts b/services/main.ts new file mode 100644 index 0000000..47af1a1 --- /dev/null +++ b/services/main.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from '@adonisjs/core/services/app' +import { RedisService } from '../src/types/main.js' + +let redis: RedisService + +/** + * Returns a singleton instance of the Redis manager from the + * container + */ +await app.booted(async () => { + redis = await app.container.make('redis') +}) + +export { redis as default } From c8dce18639ecbb6de7c92f0f887829b5e9db5695 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 00:29:11 +0200 Subject: [PATCH 13/71] feat: add configure hook --- configure.ts | 42 ++++++++++++++++++++++ index.ts | 2 ++ stubs/config/redis.stub | 30 ++++++++++++++++ stubs/index.ts | 12 +++++++ stubs/types/redis.stub | 8 +++++ test/configure.spec.ts | 80 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 configure.ts create mode 100644 stubs/config/redis.stub create mode 100644 stubs/index.ts create mode 100644 stubs/types/redis.stub create mode 100644 test/configure.spec.ts diff --git a/configure.ts b/configure.ts new file mode 100644 index 0000000..9cbd5c7 --- /dev/null +++ b/configure.ts @@ -0,0 +1,42 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type Configure from '@adonisjs/core/commands/configure' + +/** + * Configures the package + */ +export async function configure(command: Configure) { + /** + * Publish config file + */ + await command.publishStub('config/redis.stub') + + /** + * Publish typings file + */ + await command.publishStub('types/redis.stub') + + /** + * Add environment variables + */ + await command.defineEnvVariables({ + REDIS_CONNECTION: 'local', + REDIS_HOST: '127.0.0.1', + REDIS_PORT: '6379', + REDIS_PASSWORD: '', + }) + + /** + * Add provider to rc file + */ + await command.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/redis/providers/redis_provider') + }) +} diff --git a/index.ts b/index.ts index c9f9f12..fae4259 100644 --- a/index.ts +++ b/index.ts @@ -10,3 +10,5 @@ import './src/types/extended.js' export { defineConfig } from './src/define_config.js' +export { stubsRoot } from './stubs/index.js' +export { configure } from './configure.js' diff --git a/stubs/config/redis.stub b/stubs/config/redis.stub new file mode 100644 index 0000000..a3e782c --- /dev/null +++ b/stubs/config/redis.stub @@ -0,0 +1,30 @@ +--- +to: {{ app.configPath('redis.ts') }} +--- + +import env from '#start/env' +import { defineConfig } from '@adonisjs/redis' + +export default defineConfig({ + connection: env.get('REDIS_CONNECTION'), + + connections: { + /* + |-------------------------------------------------------------------------- + | The default connection + |-------------------------------------------------------------------------- + | + | The main connection you want to use to execute redis commands. The same + | connection will be used by the session provider, if you rely on the + | redis driver. + | + */ + local: { + host: env.get('REDIS_HOST'), + port: env.get('REDIS_PORT'), + password: env.get('REDIS_PASSWORD', ''), + db: 0, + keyPrefix: '', + }, + }, +}) diff --git a/stubs/index.ts b/stubs/index.ts new file mode 100644 index 0000000..908ba8f --- /dev/null +++ b/stubs/index.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { getDirname } from '@poppinss/utils' + +export const stubsRoot = getDirname(import.meta.url) diff --git a/stubs/types/redis.stub b/stubs/types/redis.stub new file mode 100644 index 0000000..8587d87 --- /dev/null +++ b/stubs/types/redis.stub @@ -0,0 +1,8 @@ +--- +to: {{ app.makePath('types/redis.ts') }} +--- +import redis from '#config/redis' + +declare module '@adonisjs/redis/types' { + export interface RedisConnections extends InferConnections {} +} diff --git a/test/configure.spec.ts b/test/configure.spec.ts new file mode 100644 index 0000000..83d6e54 --- /dev/null +++ b/test/configure.spec.ts @@ -0,0 +1,80 @@ +/* + * @adonisjs/mail + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { fileURLToPath } from 'node:url' +import { test } from '@japa/runner' +import { IgnitorFactory } from '@adonisjs/core/factories' +import Configure from '@adonisjs/core/commands/configure' + +export const BASE_URL = new URL('./tmp/', import.meta.url) + +async function setupConfigureCommand() { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + const ace = await app.container.make('ace') + const command = await ace.create(Configure, ['../../index.js']) + + command.ui.switchMode('raw') + + return { command } +} + +test.group('Configure', (group) => { + group.each.setup(({ context }) => { + context.fs.baseUrl = BASE_URL + context.fs.basePath = fileURLToPath(BASE_URL) + }) + + test('publish config and types files', async ({ assert }) => { + const { command } = await setupConfigureCommand() + + await command.exec() + + await assert.fileExists('config/redis.ts') + await assert.fileContains('config/redis.ts', 'export default defineConfig({') + await assert.fileExists('types/redis.ts') + }) + + test('add redis_provider to the rc file', async ({ assert }) => { + const { command } = await setupConfigureCommand() + + await command.exec() + + await assert.fileExists('.adonisrc.json') + await assert.fileContains('.adonisrc.json', '"@adonisjs/redis/providers/redis_provider"') + }) + + test('add env variables for the selected drivers', async ({ assert, fs }) => { + const { command } = await setupConfigureCommand() + + await fs.create('.env', '') + await command.exec() + + await assert.fileContains('.env', 'REDIS_CONNECTION=local') + await assert.fileContains('.env', 'REDIS_HOST=127.0.0.1') + + await assert.fileContains('.env', 'REDIS_PORT=6379') + await assert.fileContains('.env', 'REDIS_PASSWORD=') + }) +}) From 4e636c8f54e929b0bbb0700c3a45257d23c9fd94 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 00:35:07 +0200 Subject: [PATCH 14/71] chore: remove old templates --- templates/config.txt | 46 ------------------------------------------ templates/contract.txt | 13 ------------ 2 files changed, 59 deletions(-) delete mode 100644 templates/config.txt delete mode 100644 templates/contract.txt diff --git a/templates/config.txt b/templates/config.txt deleted file mode 100644 index 9138ae6..0000000 --- a/templates/config.txt +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Config source: https://git.io/JemcF - * - * Feel free to let us know via PR, if you find something broken in this config - * file. - */ - -import Env from '@ioc:Adonis/Core/Env' -import { redisConfig } from '@adonisjs/redis/build/config' - -/* -|-------------------------------------------------------------------------- -| Redis configuration -|-------------------------------------------------------------------------- -| -| Following is the configuration used by the Redis provider to connect to -| the redis server and execute redis commands. -| -| Do make sure to pre-define the connections type inside `contracts/redis.ts` -| file for AdonisJs to recognize connections. -| -| Make sure to check `contracts/redis.ts` file for defining extra connections -*/ -export default redisConfig({ - connection: Env.get('REDIS_CONNECTION'), - - connections: { - /* - |-------------------------------------------------------------------------- - | The default connection - |-------------------------------------------------------------------------- - | - | The main connection you want to use to execute redis commands. The same - | connection will be used by the session provider, if you rely on the - | redis driver. - | - */ - local: { - host: Env.get('REDIS_HOST'), - port: Env.get('REDIS_PORT'), - password: Env.get('REDIS_PASSWORD', ''), - db: 0, - keyPrefix: '', - }, - }, -}) diff --git a/templates/contract.txt b/templates/contract.txt deleted file mode 100644 index c70ccf3..0000000 --- a/templates/contract.txt +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Contract source: https://git.io/JemcN - * - * Feel free to let us know via PR, if you find something broken in this config - * file. - */ - -import { InferConnectionsFromConfig } from '@adonisjs/redis/build/config' -import redisConfig from '../config/redis' - -declare module '@ioc:Adonis/Addons/Redis' { - interface RedisConnectionsList extends InferConnectionsFromConfig {} -} From 2573f64c9aea10800e7853982e54afd86ca86e2a Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 00:37:15 +0200 Subject: [PATCH 15/71] chore: add engines.node --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 4532406..b2fa51e 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", "version": "7.3.2", + "engines": { + "node": ">=18.16.0" + }, "main": "build/index.js", "type": "module", "files": [ From 5a4efcf2d37a37be4358fe3da171d4dff571e5c3 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 00:37:48 +0200 Subject: [PATCH 16/71] chore(release): 8.0.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b2fa51e..91b19e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "7.3.2", + "version": "8.0.0-0", "engines": { "node": ">=18.16.0" }, From 0b139c4e9a11f1f212f6e5369826a29aee5830d4 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 15:12:06 +0200 Subject: [PATCH 17/71] refactor: get io methods names using reflection before, we kept a hard-coded list of method names. a lot was missing from it and it could be out of sync with the ioredis api --- src/io_methods.ts | 207 ++++++++----------------------------- test/redis_manager.spec.ts | 27 +++++ 2 files changed, 71 insertions(+), 163 deletions(-) diff --git a/src/io_methods.ts b/src/io_methods.ts index 0692efb..879a60d 100644 --- a/src/io_methods.ts +++ b/src/io_methods.ts @@ -7,167 +7,48 @@ * file that was distributed with this source code. */ -export const ioMethods = [ - 'call', +import { Redis } from 'ioredis' + +/** + * Returns all method names for a given class + */ +function getAllMethodNames(obj: any) { + let methods = new Set() + while ((obj = Reflect.getPrototypeOf(obj))) { + let keys = Reflect.ownKeys(obj) + keys.forEach((k) => methods.add(k)) + } + return [...methods] as string[] +} + +const ignoredMethods = [ + 'constructor', + 'status', 'connect', - 'send_command', - 'bitcount', - 'bitfield', - 'get', - 'getdel', - 'getBuffer', - 'set', - 'setBuffer', - 'callback', - 'setnx', - 'setex', - 'psetex', - 'append', - 'strlen', - 'del', - 'exists', - 'setbit', - 'getbit', - 'setrange', - 'getrange', - 'substr', - 'incr', - 'decr', - 'mget', - 'rpush', - 'lpush', - 'rpushx', - 'lpushx', - 'linsert', - 'rpop', - 'lpop', - 'brpop', - 'blpop', - 'brpoplpush', - 'llen', - 'lindex', - 'lset', - 'lrange', - 'ltrim', - 'lrem', - 'rpoplpush', - 'sadd', - 'srem', - 'smove', - 'sismember', - 'scard', - 'spop', - 'srandmember', - 'sinter', - 'sinterstore', - 'sunion', - 'sunionstore', - 'sdiff', - 'sdiffstore', - 'smembers', - 'zadd', - 'zincrby', - 'zrem', - 'zremrangebyscore', - 'zremrangebyrank', - 'zinterstore', - 'zrange', - 'zrevrange', - 'zrangebyscore', - 'zrevrangebyscore', - 'zcount', - 'zcard', - 'zscore', - 'zrank', - 'zrevrank', - 'hset', - 'hsetBuffer', - 'hsetnx', - 'hget', - 'hgetBuffer', - 'hmget', - 'hincrby', - 'hincrbyfloat', - 'hdel', - 'hlen', - 'hkeys', - 'hvals', - 'hgetall', - 'hexists', - 'incrby', - 'incrbyfloat', - 'decrby', - 'getset', - 'mset', - 'msetnx', - 'randomkey', - 'select', - 'move', - 'rename', - 'renamenx', - 'expire', - 'pexpire', - 'expireat', - 'pexpireat', - 'keys', - 'dbsize', - 'auth', - 'ping', - 'echo', - 'save', - 'bgsave', - 'bgrewriteaof', - 'shutdown', - 'lastsave', - 'type', - 'multi', - 'exec', - 'discard', - 'sync', - 'flushdb', - 'flushall', - 'sort', - 'info', - 'time', - 'monitor', - 'ttl', - 'persist', - 'slaveof', - 'debug', - 'config', - 'watch', - 'unwatch', - 'cluster', - 'restore', - 'migrate', - 'dump', - 'object', - 'client', - 'eval', - 'evalsha', - 'script', - 'scan', - 'sscan', - 'hscan', - 'zscan', - 'pfmerge', - 'pfadd', - 'pfcount', - 'pipeline', - 'scanStream', - 'hscanStream', - 'zscanStream', - 'xack', - 'xadd', - 'xclaim', - 'xdel', - 'xgroup', - 'xinfo', - 'xlen', - 'xpending', - 'xrange', - 'xread', - 'xreadgroup', - 'xrevrange', - 'xtrim', -] as const + 'disconnect', + 'duplicate', + 'subscribe', + 'unsubscribe', + 'psubscribe', + 'punsubscribe', + 'quit', + 'publish', + '__defineGetter__', + '__defineSetter__', + 'hasOwnProperty', + '__lookupGetter__', + '__lookupSetter__', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toString', + 'valueOf', + '__proto__', + 'toLocaleString', +] + +/** + * List of methods on Redis class + */ +export const ioMethods = getAllMethodNames(Redis.prototype).filter( + (method) => !ignoredMethods.includes(method) +) as string[] diff --git a/test/redis_manager.spec.ts b/test/redis_manager.spec.ts index 1466738..3419db9 100644 --- a/test/redis_manager.spec.ts +++ b/test/redis_manager.spec.ts @@ -283,4 +283,31 @@ test.group('Redis Manager', () => { expectTypeOf(redis.connection('cluster')).toEqualTypeOf() expectTypeOf(redis.connection('primary')).toEqualTypeOf() }) + + test('should have every ioredis methods available', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create(new AppFactory().create(BASE_URL, () => {})) + + /** + * Assert on some obscure methods to make sure + * they are available + */ + assert.isFunction(redis.geohash) + assert.isFunction(redis.spopBuffer) + assert.isFunction(redis.georadiusbymember) + assert.isFunction(redis.xack) + assert.isFunction(redis.xclaim) + assert.isFunction(redis.xgroup) + assert.isFunction(redis.decr) + }) }) From 8ea92c05810033ae9ad0f6bfa335e66411f16e5f Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 15:29:15 +0200 Subject: [PATCH 18/71] ci: start containers before testing --- .github/workflows/test.yml | 59 +++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de4467a..7fe13cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,23 +3,56 @@ on: - push - pull_request jobs: - linux: + test_linux: runs-on: ubuntu-latest strategy: matrix: - node-version: - - 17.x - - 20.x + node-version: [18.16.0, 20.x] + steps: - - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Start docker-compose + run: docker-compose up -d + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test + + test_windows: + runs-on: windows-latest + strategy: + matrix: + node-version: [18.16.0, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Start docker-compose + run: docker-compose up -d + + - name: Install dependencies + run: npm install - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} + - name: Run tests + run: npm test - - name: Install - run: npm install + lint: + uses: adonisjs/.github/.github/workflows/lint.yml@main - - name: Run tests - run: npm test + typecheck: + uses: adonisjs/.github/.github/workflows/typecheck.yml@main From 63efbee38c3b795889f19b97e10549c9a9ea1acb Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 15:31:29 +0200 Subject: [PATCH 19/71] chore: add c8 dev dep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 91b19e6..63de13b 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@japa/runner": "3.0.0-6", "@swc/core": "^1.3.69", "@types/node": "^20.4.2", + "c8": "^8.0.0", "copyfiles": "^2.4.1", "del-cli": "^5.0.0", "dotenv": "^16.3.1", From e7c485c74ddeda1557b235727b24dd134f9c78de Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 15:31:35 +0200 Subject: [PATCH 20/71] ci: run tests only on linux --- .github/workflows/test.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fe13cf..99ca46b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,30 +27,6 @@ jobs: - name: Run tests run: npm test - test_windows: - runs-on: windows-latest - strategy: - matrix: - node-version: [18.16.0, 20.x] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - - name: Start docker-compose - run: docker-compose up -d - - - name: Install dependencies - run: npm install - - - name: Run tests - run: npm test - lint: uses: adonisjs/.github/.github/workflows/lint.yml@main From b58dc7350d59adaadebd6c250c4d8629da72511e Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 16:02:08 +0200 Subject: [PATCH 21/71] ci: fix ci --- .github/workflows/test.yml | 5 +---- Dockerfile | 17 +++++++++++++++++ docker-compose.ci.yml | 22 ++++++++++++++++++++++ package.json | 1 + 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.ci.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99ca46b..4a6d4f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,14 +18,11 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Start docker-compose - run: docker-compose up -d - - name: Install dependencies run: npm install - name: Run tests - run: npm test + run: npm run test:docker lint: uses: adonisjs/.github/.github/workflows/lint.yml@main diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95a18f1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine as build-deps + +RUN apk update && apk upgrade && \ + apk add --update git && \ + apk add --update openssh && \ + apk add --update bash && \ + apk add --update wget + +ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait /wait +RUN chmod +x /wait + +WORKDIR /usr/src/app + +COPY package*.json ./ +RUN npm install + +COPY . . diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 0000000..9138887 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,22 @@ +version: '3.4' +services: + tests: + image: adonis-redis + network_mode: host + build: + context: . + environment: + REDIS_HOST: 0.0.0.0 + REDIS_PORT: 7007 + REDIS_CLUSTER_PORTS: '7000,7001,7002' + WAIT_HOSTS: 0.0.0.0:7000, 0.0.0.0:7001, 0.0.0.0:7002, 0.0.0.0:7003, 0.0.0.0:7004, 0.0.0.0:7005, 0.0.0.0:7006, 0.0.0.0:7007 + depends_on: + - redis + command: sh -c "/wait && FORCE_COLOR=true npm run test" + redis: + network_mode: host + image: grokzen/redis-cluster + environment: + REDIS_CLUSTER_IP: 0.0.0.0 + IP: 0.0.0.0 + STANDALONE: 'true' diff --git a/package.json b/package.json index 63de13b..fa908c8 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "scripts": { "pretest": "npm run lint", "test": "c8 npm run quick:test", + "test:docker": "docker-compose -f docker-compose.ci.yml run --rm tests", "quick:test": "node --enable-source-maps --loader=ts-node/esm ./bin/test.ts", "clean": "del-cli build", "copy:templates": "copyfiles \"stubs/**/**/*.stub\" build", From c479ccf1c82a31cfece618d4d207e320638d1dc8 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 16:08:17 +0200 Subject: [PATCH 22/71] chore: update readme --- README.md | 77 +++++++++++++++++++++++++------------------------------ 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 296b770..5e1017a 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,46 @@ -
- -
+# @adonisjs/redis
-
-

Redis

-

Redis provider for AdonisJS with support for multiple Redis connections, cluster, pub/sub and much more

-
+[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![snyk-image]][snyk-url] -
+## Introduction +Redis provider for AdonisJS with support for multiple Redis connections, cluster, pub/sub and much more -
- -[![gh-workflow-image]][gh-workflow-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url] - -
- - - -
- Built with ❤︎ by Harminder Virk -
- -[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/redis/test?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/redis/actions/workflows/test.yml "Github action" +## Official Documentation +The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/repl) -[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript -[typescript-url]: "typescript" +## Contributing +One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. + +We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. + +### Run tests locally +Easiest way to run tests is to launch the redis cluster using docker-compose and `docker-compose.yml` file. + +```sh +docker-compose up -d +npm run test +``` + +We also have a `docker-compose.ci.yml` file that will dockerize the library and run tests inside the container. This is what we use on Github actions. + +## Code of Conduct +In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). + +## License +AdonisJS Redis is open-sourced software licensed under the [MIT license](LICENSE.md). + +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/redis/test.yml?style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/redis/actions/workflows/test.yml "Github action" [npm-image]: https://img.shields.io/npm/v/@adonisjs/redis/latest.svg?style=for-the-badge&logo=npm [npm-url]: https://www.npmjs.com/package/@adonisjs/redis/v/latest "npm" -[license-image]: https://img.shields.io/npm/l/@adonisjs/redis?color=blueviolet&style=for-the-badge -[license-url]: LICENSE.md "license" +[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript + +[license-url]: LICENSE.md +[license-image]: https://img.shields.io/github/license/adonisjs/redis?style=for-the-badge -[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/redis?label=Synk%20Vulnerabilities&style=for-the-badge -[synk-url]: https://snyk.io/test/github/adonisjs/redis?targetFile=package.json "synk" +[snyk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/redis?label=Snyk%20Vulnerabilities&style=for-the-badge +[snyk-url]: https://snyk.io/test/github/adonisjs/redis?targetFile=package.json "snyk" From 8c258d621b14818a5863485fcd8ea1dd09213996 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 16:09:31 +0200 Subject: [PATCH 23/71] chore(release): 8.0.0-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa908c8..1c72d50 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-0", + "version": "8.0.0-1", "engines": { "node": ">=18.16.0" }, From 4732cc8378c039a1c3b7c238bf3f8819470d1465 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 15:22:02 +0200 Subject: [PATCH 24/71] refactor: update provider export --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c72d50..a6f059f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "exports": { ".": "./build/index.js", "./services/main": "./build/services/main.js", - "./providers/redis_provider": "./build/providers/redis_provider.js", + "./redis_provider": "./build/providers/redis_provider.js", "./factories": "./build/factories/main.js", "./types": "./build/src/types/main.js" }, From b3aaf38a08714312316018566eabceded78bc5c1 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 15:23:41 +0200 Subject: [PATCH 25/71] chore(release): 8.0.0-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6f059f..638371d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-1", + "version": "8.0.0-2", "engines": { "node": ">=18.16.0" }, From 0268257627d19086d78d771ae0717f6fbdb3f45a Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 15:27:01 +0200 Subject: [PATCH 26/71] fix: update configure.ts with new provider export --- configure.ts | 2 +- test/configure.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ts b/configure.ts index 9cbd5c7..8abef65 100644 --- a/configure.ts +++ b/configure.ts @@ -37,6 +37,6 @@ export async function configure(command: Configure) { * Add provider to rc file */ await command.updateRcFile((rcFile) => { - rcFile.addProvider('@adonisjs/redis/providers/redis_provider') + rcFile.addProvider('@adonisjs/redis/redis_provider') }) } diff --git a/test/configure.spec.ts b/test/configure.spec.ts index 83d6e54..6ba2779 100644 --- a/test/configure.spec.ts +++ b/test/configure.spec.ts @@ -62,7 +62,7 @@ test.group('Configure', (group) => { await command.exec() await assert.fileExists('.adonisrc.json') - await assert.fileContains('.adonisrc.json', '"@adonisjs/redis/providers/redis_provider"') + await assert.fileContains('.adonisrc.json', '"@adonisjs/redis/redis_provider"') }) test('add env variables for the selected drivers', async ({ assert, fs }) => { From 43b53677639a17dd47b6b02e247e297aabe11458 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 15:30:53 +0200 Subject: [PATCH 27/71] chore(release): 8.0.0-3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 638371d..251a2ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-2", + "version": "8.0.0-3", "engines": { "node": ">=18.16.0" }, From 97837d7ac914bf4c88ce9263ab0d0a83cc6a53f2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 27 Jul 2023 14:03:51 +0530 Subject: [PATCH 28/71] refactor: cleaning things up --- .husky/commit-msg | 2 +- bin/test.ts | 4 +- docker-compose.yml | 10 +- factories/redis_manager.ts | 10 +- package.json | 22 +- providers/redis_provider.ts | 22 +- src/{ => connections}/abstract_connection.ts | 189 +++++----- src/{ => connections}/io_methods.ts | 9 +- src/{ => connections}/pubsub_methods.ts | 2 +- .../redis_cluster_connection.ts | 32 +- src/{ => connections}/redis_connection.ts | 19 +- src/define_config.ts | 8 +- src/redis_manager.ts | 207 ++--------- src/types/extended.ts | 21 +- src/types/main.ts | 128 ++----- test/redis_cluster_connection.spec.ts | 190 ---------- test/redis_connection.spec.ts | 165 --------- test/redis_manager.spec.ts | 313 ----------------- {test => tests}/configure.spec.ts | 0 {test => tests}/define_config.spec.ts | 0 tests/redis_cluster_connection.spec.ts | 168 +++++++++ tests/redis_connection.spec.ts | 332 ++++++++++++++++++ tests/redis_manager.spec.ts | 230 ++++++++++++ {test => tests}/redis_provider.spec.ts | 45 ++- 24 files changed, 1006 insertions(+), 1122 deletions(-) rename src/{ => connections}/abstract_connection.ts (73%) rename src/{ => connections}/io_methods.ts (91%) rename src/{ => connections}/pubsub_methods.ts (81%) rename src/{ => connections}/redis_cluster_connection.ts (58%) rename src/{ => connections}/redis_connection.ts (71%) delete mode 100644 test/redis_cluster_connection.spec.ts delete mode 100644 test/redis_connection.spec.ts delete mode 100644 test/redis_manager.spec.ts rename {test => tests}/configure.spec.ts (100%) rename {test => tests}/define_config.spec.ts (100%) create mode 100644 tests/redis_cluster_connection.spec.ts create mode 100644 tests/redis_connection.spec.ts create mode 100644 tests/redis_manager.spec.ts rename {test => tests}/redis_provider.spec.ts (52%) diff --git a/.husky/commit-msg b/.husky/commit-msg index 988eb59..4002db7 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npx --no -- commitlint --edit +npx --no -- commitlint --edit diff --git a/bin/test.ts b/bin/test.ts index 71d7fc4..5ec5412 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,8 +1,8 @@ import 'dotenv/config' -import { processCLIArgs, configure, run } from '@japa/runner' import { assert } from '@japa/assert' import { fileSystem } from '@japa/file-system' import { expectTypeOf } from '@japa/expect-type' +import { processCLIArgs, configure, run } from '@japa/runner' /* |-------------------------------------------------------------------------- @@ -19,7 +19,7 @@ import { expectTypeOf } from '@japa/expect-type' */ processCLIArgs(process.argv.slice(2)) configure({ - files: ['test/**/*.spec.ts'], + files: ['tests/**/*.spec.ts'], plugins: [assert(), fileSystem(), expectTypeOf()], forceExit: true, }) diff --git a/docker-compose.yml b/docker-compose.yml index 963804c..8e8722f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,10 @@ version: '3.4' services: redis: - image: grokzen/redis-cluster + platform: linux/x86_64 + image: grokzen/redis-cluster:6.2.10 ports: - - "7000:7000" - - "7001:7001" - - "7002:7002" - - "7003:7003" - - "7004:7004" - - "7005:7005" + - "7000-7005:7000-7005" environment: REDIS_CLUSTER_IP: 0.0.0.0 IP: 0.0.0.0 diff --git a/factories/redis_manager.ts b/factories/redis_manager.ts index 56b1485..7b5d34d 100644 --- a/factories/redis_manager.ts +++ b/factories/redis_manager.ts @@ -7,17 +7,15 @@ * file that was distributed with this source code. */ -import type { Application } from '@adonisjs/core/app' -import type { RedisClusterConfig, RedisConnectionConfig } from '../src/types/main.js' -import { EmitterFactory } from '@adonisjs/core/factories/events' import RedisManager from '../src/redis_manager.js' +import type { RedisClusterConnectionConfig, RedisConnectionConfig } from '../src/types/main.js' /** * Redis manager factory is used to create an instance of the redis * manager for testing */ export class RedisManagerFactory< - ConnectionsList extends Record, + ConnectionsList extends Record, > { #config: { connection: keyof ConnectionsList @@ -31,7 +29,7 @@ export class RedisManagerFactory< /** * Create an instance of the redis manager */ - create(app: Application) { - return new RedisManager(this.#config, new EmitterFactory().create(app)) + create() { + return new RedisManager(this.#config) } } diff --git a/package.json b/package.json index 251a2ae..daa5a99 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,8 @@ "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", "version": "8.0.0-3", - "engines": { - "node": ">=18.16.0" - }, - "main": "build/index.js", "type": "module", + "main": "build/index.js", "files": [ "build/configure.js", "build/configure.d.ts", @@ -20,6 +17,9 @@ "build/index.d.ts", "build/index.js" ], + "engines": { + "node": ">=18.16.0" + }, "exports": { ".": "./build/index.js", "./services/main": "./build/services/main.js", @@ -31,12 +31,12 @@ "pretest": "npm run lint", "test": "c8 npm run quick:test", "test:docker": "docker-compose -f docker-compose.ci.yml run --rm tests", - "quick:test": "node --enable-source-maps --loader=ts-node/esm ./bin/test.ts", + "quick:test": "node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "clean": "del-cli build", "copy:templates": "copyfiles \"stubs/**/**/*.stub\" build", "compile": "npm run lint && npm run clean && tsc && npm run copy:templates", "build": "npm run compile", - "release": "np --message=\"chore(release): %s\"", + "release": "np", "version": "npm run build", "prepublishOnly": "npm run build", "lint": "eslint . --ext=.ts", @@ -44,10 +44,12 @@ "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { - "@adonisjs/core": "^6.1.5-8", + "@adonisjs/core": "^6.1.5-12", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", + "@commitlint/cli": "^17.6.7", + "@commitlint/config-conventional": "^17.6.7", "@japa/assert": "2.0.0-1", "@japa/expect-type": "2.0.0-0", "@japa/file-system": "2.0.0-1", @@ -62,6 +64,7 @@ "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", + "p-event": "^6.0.0", "prettier": "^3.0.0", "ts-node": "^10.9.1", "typescript": "^5.1.6" @@ -71,7 +74,7 @@ "ioredis": "^5.3.2" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-8" + "@adonisjs/core": "^6.1.5-12" }, "author": "virk,adonisjs", "license": "MIT", @@ -112,7 +115,8 @@ "html" ], "exclude": [ - "tests/**" + "tests/**", + "src/repl_bindings.ts" ] } } diff --git a/providers/redis_provider.ts b/providers/redis_provider.ts index a510c60..19c18f4 100644 --- a/providers/redis_provider.ts +++ b/providers/redis_provider.ts @@ -7,11 +7,11 @@ * file that was distributed with this source code. */ -import { ApplicationService } from '@adonisjs/core/types' -import { defineReplBindings } from '../src/repl_bindings.js' +import type { ApplicationService } from '@adonisjs/core/types' /** - * Provider to bind redis to the container + * Registering the Redis manager as a singleton to the container + * and defining REPL bindings */ export default class RedisProvider { constructor(protected app: ApplicationService) {} @@ -19,33 +19,33 @@ export default class RedisProvider { /** * Define repl bindings */ - async #defineReplBindings() { + protected async defineReplBindings() { if (this.app.getEnvironment() !== 'repl') { return } + const { defineReplBindings } = await import('../src/repl_bindings.js') defineReplBindings(this.app, await this.app.container.make('repl')) } /** - * Register the redis binding + * Register the Redis manager as a singleton with the + * container */ register() { this.app.container.singleton('redis', async () => { const { default: RedisManager } = await import('../src/redis_manager.js') - const emitter = await this.app.container.make('emitter') const config = this.app.config.get('redis', {}) - - return new RedisManager(config, emitter) + return new RedisManager(config) }) } /** - * Registering the health check checker with HealthCheck service + * Defining repl bindings on boot */ - boot() { - this.#defineReplBindings() + async boot() { + await this.defineReplBindings() } /** diff --git a/src/abstract_connection.ts b/src/connections/abstract_connection.ts similarity index 73% rename from src/abstract_connection.ts rename to src/connections/abstract_connection.ts index 2e52058..d27a9e2 100644 --- a/src/abstract_connection.ts +++ b/src/connections/abstract_connection.ts @@ -8,29 +8,31 @@ */ import { EventEmitter } from 'node:events' -import { Redis, Cluster } from 'ioredis' import { Exception } from '@poppinss/utils' -import { PubSubChannelHandler, PubSubPatternHandler, HealthReportNode } from './types/main.js' - -/** - * Helper to sleep - */ -const sleep = () => new Promise((resolve) => setTimeout(resolve, 1000)) +import type { Redis, Cluster } from 'ioredis' +import { setTimeout } from 'node:timers/promises' +import type { HealthReportNode, PubSubChannelHandler, PubSubPatternHandler } from '../types/main.js' /** * Abstract factory implements the shared functionality required by Redis cluster - * and normal Redis connections. + * and the normal Redis connections. */ export abstract class AbstractConnection extends EventEmitter { /** * Reference to the main ioRedis connection */ - ioConnection!: T + declare ioConnection: T /** * Reference to the main ioRedis subscriber connection */ - ioSubscriberConnection?: T + declare ioSubscriberConnection?: T + + /** + * A list of active subscriptions and pattern subscription + */ + protected subscriptions: Map = new Map() + protected psubscriptions: Map = new Map() /** * Number of times `getReport` was deferred, at max we defer it for 3 times @@ -43,12 +45,6 @@ export abstract class AbstractConnection extends Even */ #lastError?: any - /** - * A list of active subscription and pattern subscription - */ - protected subscriptions: Map = new Map() - protected psubscriptions: Map = new Map() - /** * Returns the memory usage for a given connection */ @@ -63,8 +59,8 @@ export abstract class AbstractConnection extends Even /** * Returns status of the main connection */ - get status(): string { - return (this.ioConnection as Redis).status + get status() { + return this.ioConnection.status } /** @@ -72,12 +68,8 @@ export abstract class AbstractConnection extends Even * undefined when there is no subscriber * connection */ - get subscriberStatus(): string | undefined { - if (!this.ioSubscriberConnection) { - return - } - - return (this.ioSubscriberConnection as Redis).status + get subscriberStatus() { + return this.ioSubscriberConnection?.status } /** @@ -90,17 +82,16 @@ export abstract class AbstractConnection extends Even } /** - * The events proxying is required, since ioredis itself doesn't cleanup - * listeners after closing the redis connection and since closing a - * connection is an async operation, we have to wait for the `end` - * event on the actual connection and then remove listeners. + * Monitoring the redis connection via event emitter to cleanup + * things properly and also notify subscribers of this class */ - protected proxyConnectionEvents() { + protected monitorConnection() { this.ioConnection.on('connect', () => this.emit('connect', this)) + this.ioConnection.on('wait', () => this.emit('wait', this)) this.ioConnection.on('ready', () => { /** * We must set the error to null when server is ready for accept - * command + * commands */ this.#lastError = null this.emit('ready', this) @@ -129,13 +120,64 @@ export abstract class AbstractConnection extends Even this.ioConnection.on('end', async () => { this.ioConnection.removeAllListeners() this.emit('end', this) - this.removeAllListeners() + this.removeAllListeners('connect') + this.removeAllListeners('wait') + this.removeAllListeners('ready') + this.removeAllListeners('error') + this.removeAllListeners('close') + this.removeAllListeners('reconnecting') + this.removeAllListeners('node:added') + this.removeAllListeners('node:removed') + this.removeAllListeners('node:error') + this.removeAllListeners('end') + }) + } + + /** + * Monitoring the subscriber connection via event emitter to + * cleanup things properly and also notify subscribers of + * this class. + */ + protected monitorSubscriberConnection() { + this.ioSubscriberConnection!.on('connect', () => this.emit('subscriber:connect', this)) + this.ioSubscriberConnection!.on('wait', () => this.emit('subscriber:wait', this)) + this.ioSubscriberConnection!.on('ready', () => this.emit('subscriber:ready', this)) + this.ioSubscriberConnection!.on('error', (error: any) => { + this.emit('subscriber:error', error, this) + }) + this.ioSubscriberConnection!.on('close', () => this.emit('subscriber:close', this)) + this.ioSubscriberConnection!.on('reconnecting', () => + this.emit('subscriber:reconnecting', this) + ) + + /** + * On subscriber connection end, we must clear registered + * subscriptions and client event listeners. + */ + this.ioSubscriberConnection!.on('end', async () => { + this.ioSubscriberConnection!.removeAllListeners() + this.emit('subscriber:end', this) + + /** + * Cleanup subscriptions + */ + this.subscriptions.clear() + this.psubscriptions.clear() + + this.ioSubscriberConnection = undefined + this.removeAllListeners('subscriber:connect') + this.removeAllListeners('subscriber:wait') + this.removeAllListeners('subscriber:ready') + this.removeAllListeners('subscriber:error') + this.removeAllListeners('subscriber:close') + this.removeAllListeners('subscriber:reconnecting') + this.removeAllListeners('subscriber:end') }) } /** - * Making the subscriber connection and proxying it's events. The method - * results in a noop, in case of an existing subscriber connection. + * Setting up the subscriber connection. The method results + * in a noop when a connection already exists. */ protected setupSubscriberConnection() { if (this.ioSubscriberConnection) { @@ -143,7 +185,7 @@ export abstract class AbstractConnection extends Even } /** - * Ask parent class to setup the subscriber connection + * Ask child class to setup the subscriber connection */ this.makeSubscriberConnection() @@ -166,36 +208,6 @@ export abstract class AbstractConnection extends Even handler(channel, message) } }) - - /** - * Proxying subscriber events, so that we can prefix them with `subscriber:`. - * Also make sure not to clear the events of this class on subscriber - * disconnect - */ - this.ioSubscriberConnection!.on('connect', () => this.emit('subscriber:connect', this)) - this.ioSubscriberConnection!.on('ready', () => this.emit('subscriber:ready', this)) - this.ioSubscriberConnection!.on('error', (error: any) => - this.emit('subscriber:error', error, this) - ) - this.ioSubscriberConnection!.on('close', () => this.emit('subscriber:close', this)) - this.ioSubscriberConnection!.on('reconnecting', () => - this.emit('subscriber:reconnecting', this) - ) - - /** - * On subscriber connection end, we must clear registered - * subscriptions and client event listeners. - */ - this.ioSubscriberConnection!.on('end', async () => { - this.ioConnection.removeAllListeners() - this.emit('subscriber:end', this) - - /** - * Cleanup subscriptions map - */ - this.subscriptions.clear() - this.psubscriptions.clear() - }) } /** @@ -233,10 +245,13 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.subscriptions.has(channel)) { - throw new Exception(`"${channel}" channel already has an active subscription`, { - code: 'E_MULTIPLE_REDIS_SUBSCRIPTIONS', - status: 500, - }) + throw new Exception( + `Cannot subscribe to "${channel}" channel. Channel already has an active subscription`, + { + code: 'E_MULTIPLE_REDIS_SUBSCRIPTIONS', + status: 500, + } + ) } /** @@ -244,9 +259,7 @@ export abstract class AbstractConnection extends Even * on the given channel, hence we should make one subscription and also set * the subscription handler. */ - const connection = this.ioSubscriberConnection as Redis - connection - .subscribe(channel) + this.ioSubscriberConnection!.subscribe(channel) .then((count) => { this.emit('subscription:ready', count, this) this.subscriptions.set(channel, handler) @@ -261,7 +274,7 @@ export abstract class AbstractConnection extends Even */ unsubscribe(channel: string) { this.subscriptions.delete(channel) - return (this.ioSubscriberConnection as Redis).unsubscribe(channel) + return this.ioSubscriberConnection!.unsubscribe(channel) } /** @@ -278,10 +291,13 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.psubscriptions.has(pattern)) { - throw new Exception(`${pattern} pattern already has an active subscription`, { - status: 500, - code: 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS', - }) + throw new Exception( + `Cannot subscribe to "${pattern}" pattern. Pattern already has an active subscription`, + { + status: 500, + code: 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS', + } + ) } /** @@ -289,10 +305,7 @@ export abstract class AbstractConnection extends Even * on the given channel, hence we should make one subscription and also set * the subscription handler. */ - const connection = this.ioSubscriberConnection as Redis - - connection - .psubscribe(pattern) + this.ioSubscriberConnection!.psubscribe(pattern) .then((count) => { this.emit('psubscription:ready', count, this) this.psubscriptions.set(pattern, handler) @@ -307,7 +320,7 @@ export abstract class AbstractConnection extends Even */ punsubscribe(pattern: string) { this.psubscriptions.delete(pattern) - return (this.ioSubscriberConnection as any).punsubscribe(pattern) + return this.ioSubscriberConnection!.punsubscribe(pattern) } /** @@ -326,7 +339,7 @@ export abstract class AbstractConnection extends Even this.#deferredReportAttempts < 3 && !this.#lastError ) { - await sleep() + await setTimeout(1000) this.#deferredReportAttempts++ return this.getReport(checkForMemory) } @@ -374,7 +387,17 @@ export abstract class AbstractConnection extends Even /** * Publish the pub/sub message */ - publish(channel: string, message: string, callback?: any) { + publish( + channel: string, + message: string, + callback: (error: Error | null | undefined, count: number | undefined) => void + ): void + publish(channel: string, message: string): Promise + publish( + channel: string, + message: string, + callback?: (error: Error | null | undefined, count: number | undefined) => void + ) { return callback ? this.ioConnection.publish(channel, message, callback) : this.ioConnection.publish(channel, message) diff --git a/src/io_methods.ts b/src/connections/io_methods.ts similarity index 91% rename from src/io_methods.ts rename to src/connections/io_methods.ts index 879a60d..288ab81 100644 --- a/src/io_methods.ts +++ b/src/connections/io_methods.ts @@ -8,6 +8,7 @@ */ import { Redis } from 'ioredis' +import { pubSubMethods } from './pubsub_methods.js' /** * Returns all method names for a given class @@ -27,12 +28,7 @@ const ignoredMethods = [ 'connect', 'disconnect', 'duplicate', - 'subscribe', - 'unsubscribe', - 'psubscribe', - 'punsubscribe', 'quit', - 'publish', '__defineGetter__', '__defineSetter__', 'hasOwnProperty', @@ -43,8 +39,9 @@ const ignoredMethods = [ 'toString', 'valueOf', '__proto__', + 'defineCommand', 'toLocaleString', -] +].concat(pubSubMethods) /** * List of methods on Redis class diff --git a/src/pubsub_methods.ts b/src/connections/pubsub_methods.ts similarity index 81% rename from src/pubsub_methods.ts rename to src/connections/pubsub_methods.ts index bad9f15..0c1f687 100644 --- a/src/pubsub_methods.ts +++ b/src/connections/pubsub_methods.ts @@ -10,4 +10,4 @@ /** * An array of methods that exists on the connection class */ -export const pubsubMethods = ['subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe', 'publish'] +export const pubSubMethods = ['subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe', 'publish'] diff --git a/src/redis_cluster_connection.ts b/src/connections/redis_cluster_connection.ts similarity index 58% rename from src/redis_cluster_connection.ts rename to src/connections/redis_cluster_connection.ts index d904a3a..9eee565 100644 --- a/src/redis_cluster_connection.ts +++ b/src/connections/redis_cluster_connection.ts @@ -7,26 +7,29 @@ * file that was distributed with this source code. */ -import Redis, { Cluster, NodeRole } from 'ioredis' +import Redis, { type Cluster, type NodeRole } from 'ioredis' import { ioMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' -import { RedisClusterConfig, RedisClusterConnectionFactory } from './types/main.js' +import type { IORedisCommands, RedisClusterConnectionConfig } from '../types/main.js' /** * Redis cluster connection exposes the API to run Redis commands using `ioredis` as the * underlying client. The class abstracts the need of creating and managing multiple * pub/sub connections by hand, since it handles that internally by itself. */ -export class RawRedisClusterConnection extends AbstractConnection { - #config: RedisClusterConfig +export class RedisClusterConnection extends AbstractConnection { + #config: RedisClusterConnectionConfig - constructor(connectionName: string, config: RedisClusterConfig) { + constructor(connectionName: string, config: RedisClusterConnectionConfig) { super(connectionName) this.#config = config - this.ioConnection = new Redis.Cluster(this.#config.clusters as any[]) - this.proxyConnectionEvents() + this.ioConnection = new Redis.Cluster( + this.#config.clusters as any[], + this.#config.clusterOptions + ) + this.monitorConnection() } /** @@ -38,6 +41,7 @@ export class RawRedisClusterConnection extends AbstractConnection { this.#config.clusters as [], this.#config.clusterOptions ) + this.monitorSubscriberConnection() } /** @@ -49,16 +53,14 @@ export class RawRedisClusterConnection extends AbstractConnection { } /** - * Here we attach pubsub and ioRedis methods to the class. - * - * But we also need to inform typescript about the existence of - * these methods. So we are exporting the class with a - * casted type that has these methods. + * Adding IORedis methods dynamically on the RedisClusterConnection + * class and also extending its TypeScript types */ -const RedisClusterConnection = RawRedisClusterConnection as unknown as RedisClusterConnectionFactory - +export interface RedisClusterConnection extends IORedisCommands {} ioMethods.forEach((method) => { - RedisClusterConnection.prototype[method] = function redisConnectionProxyFn(...args: any[]) { + ;(RedisClusterConnection.prototype as any)[method] = function redisConnectionProxyFn( + ...args: any[] + ) { return this.ioConnection[method](...args) } }) diff --git a/src/redis_connection.ts b/src/connections/redis_connection.ts similarity index 71% rename from src/redis_connection.ts rename to src/connections/redis_connection.ts index 0c31747..9455651 100644 --- a/src/redis_connection.ts +++ b/src/connections/redis_connection.ts @@ -11,7 +11,7 @@ import { Redis, RedisOptions } from 'ioredis' import { ioMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' -import { RedisConnectionConfig, RedisConnectionFactory } from './types/main.js' +import { IORedisCommands, RedisConnectionConfig } from '../types/main.js' /** * Redis connection exposes the API to run Redis commands using `ioredis` as the @@ -19,7 +19,7 @@ import { RedisConnectionConfig, RedisConnectionFactory } from './types/main.js' * multiple pub/sub connections by hand, since it handles that internally * by itself. */ -export class RawRedisConnection extends AbstractConnection { +export class RedisConnection extends AbstractConnection { #config: RedisOptions constructor(connectionName: string, config: RedisConnectionConfig) { @@ -27,7 +27,7 @@ export class RawRedisConnection extends AbstractConnection { this.#config = this.#normalizeConfig(config) this.ioConnection = new Redis(this.#config) - this.proxyConnectionEvents() + this.monitorConnection() } /** @@ -46,20 +46,17 @@ export class RawRedisConnection extends AbstractConnection { */ protected makeSubscriberConnection() { this.ioSubscriberConnection = new Redis(this.#config) + this.monitorSubscriberConnection() } } /** - * Here we attach pubsub and ioRedis methods to the class. - * - * But we also need to inform typescript about the existence of - * these methods. So we are exporting the class with a - * casted type that has these methods. + * Adding IORedis methods dynamically on the RedisConnection + * class and also extending its TypeScript types */ -const RedisConnection = RawRedisConnection as unknown as RedisConnectionFactory - +export interface RedisConnection extends IORedisCommands {} ioMethods.forEach((method) => { - RedisConnection.prototype[method] = function redisConnectionProxyFn(...args: any[]) { + ;(RedisConnection.prototype as any)[method] = function redisConnectionProxyFn(...args: any[]) { return this.ioConnection[method](...args) } }) diff --git a/src/define_config.ts b/src/define_config.ts index 84bf469..b51dbdc 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -7,17 +7,15 @@ * file that was distributed with this source code. */ -import type { RedisConnectionConfig, RedisClusterConfig } from './types/main.js' import { InvalidArgumentsException } from '@poppinss/utils' +import type { RedisConnectionsList } from './types/main.js' /** * Expected shape of the config accepted by the "defineConfig" * method */ type RedisConfig = { - connections: { - [name: string]: RedisConnectionConfig | RedisClusterConfig - } + connections: RedisConnectionsList } /** @@ -34,7 +32,7 @@ export function defineConfig { +/** + * Redis Manager exposes the API to manage multiple redis connections + * based upon user defined config. + * + * All connections are long-lived until they are closed explictly + */ +export default class RedisManager { /** * User provided config */ #config: { - connection: keyof ConnectionList - connections: ConnectionList + connection: keyof ConnectionsList + connections: ConnectionsList } - /** - * Reference to the emitter - */ - #emitter: EmitterService - - /** - * An array of connections with health checks enabled, which means, we always - * create a connection for them, even when they are not used. - */ - #healthCheckConnections: string[] = [] - /** * A copy of live connections. We avoid re-creating a new connection * everytime and re-use connections. */ activeConnections: { - [K in keyof ConnectionList]?: GetConnectionType + [K in keyof ConnectionsList]?: GetConnectionType } = {} - /** - * A boolean to know whether health checks have been enabled on one - * or more redis connections or not. - */ - get healthChecksEnabled() { - return this.#healthCheckConnections.length > 0 - } - /** * Returns the length of active connections */ @@ -63,95 +41,29 @@ export class RawRedisManager { return Object.keys(this.activeConnections).length } - constructor( - config: { connection: keyof ConnectionList; connections: ConnectionList }, - emitter: EmitterService - ) { + constructor(config: { connection: keyof ConnectionsList; connections: ConnectionsList }) { this.#config = config - this.#emitter = emitter - this.#healthCheckConnections = Object.keys(this.#config.connections).filter( - (connection) => this.#config.connections[connection].healthCheck - ) - } - - /** - * Returns the default connection name - */ - #getDefaultConnection(): keyof ConnectionList { - return this.#config.connection - } - - /** - * Returns an existing connection using it's name or the - * default connection, - */ - #getExistingConnection(name?: keyof ConnectionList) { - name = name || this.#getDefaultConnection() - return this.activeConnections[name] - } - - /** - * Returns config for a given connection - */ - #getConnectionConfig(name: ConnectionName) { - return this.#config.connections[name] - } - - /** - * Forward events to the application event emitter - * for a given connection - */ - #forwardConnectionEvents(connection: Connection) { - connection.on('ready', () => { - this.#emitter.emit('redis:ready', { connection }) - }) - connection.on('ready', ($connection) => - this.#emitter.emit('redis:ready', { connection: $connection }) - ) - connection.on('connect', ($connection) => - this.#emitter.emit('redis:connect', { connection: $connection }) - ) - connection.on('error', (error, $connection) => - this.#emitter.emit('redis:error', { error, connection: $connection }) - ) - connection.on('node:added', ($connection, node) => - this.#emitter.emit('redis:node:added', { node, connection: $connection }) - ) - connection.on('node:removed', (node, $connection) => - this.#emitter.emit('redis:node:removed', { node, connection: $connection }) - ) - connection.on('node:error', (error, address, $connection) => - this.#emitter.emit('redis:node:error', { error, address, connection: $connection }) - ) - - /** - * Stop tracking the connection after it's removed - */ - connection.on('end', ($connection) => { - delete this.activeConnections[$connection.connectionName] - this.#emitter.emit('redis:end', { connection: $connection }) - }) } /** * Returns redis factory for a given named connection */ - connection( + connection( connectionName?: ConnectionName - ): GetConnectionType { - const name = connectionName || this.#getDefaultConnection() + ): GetConnectionType { + const name = connectionName || this.#config.connection /** * Return existing connection if already exists */ if (this.activeConnections[name]) { - return this.activeConnections[name] as any + return this.activeConnections[name] as GetConnectionType } /** * Get config for the named connection */ - const config = this.#getConnectionConfig(name) + const config = this.#config.connections[name] if (!config) { throw new Error(`Redis connection "${name.toString()}" is not defined`) } @@ -165,24 +77,25 @@ export class RawRedisManager { : new RedisConnection(name as string, config) /** - * Cache the connection so that we can re-use it later + * Remove connection from the list of tracked connections */ - this.activeConnections[name] = connection as GetConnectionType + connection.on('end', ($connection) => { + delete this.activeConnections[$connection.connectionName] + }) /** - * Forward ioredis events to the application event emitter + * Cache the connection so that we can re-use it later */ - this.#forwardConnectionEvents(connection) - - return connection as GetConnectionType + this.activeConnections[name] = connection as GetConnectionType + return connection as GetConnectionType } /** * Quit a named connection or the default connection when no * name is defined. */ - async quit(name?: ConnectionName) { - const connection = this.#getExistingConnection(name) + async quit(name?: ConnectionName) { + const connection = this.activeConnections[name || this.#config.connection] if (!connection) { return } @@ -194,8 +107,8 @@ export class RawRedisManager { * Disconnect a named connection or the default connection when no * name is defined. */ - async disconnect(name?: ConnectionName) { - const connection = this.#getExistingConnection(name) + async disconnect(name?: ConnectionName) { + const connection = this.activeConnections[name || this.#config.connection] if (!connection) { return } @@ -216,64 +129,4 @@ export class RawRedisManager { async disconnectAll(): Promise { await Promise.all(Object.keys(this.activeConnections).map((name) => this.disconnect(name))) } - - /** - * Returns the report for all connections marked for `healthChecks` - */ - async report() { - const reports = await Promise.all( - this.#healthCheckConnections.map((connection) => this.connection(connection).getReport(true)) - ) - - const healthy = !reports.find((report) => !!report.error) - return { - displayName: 'Redis', - health: { - healthy, - message: healthy - ? 'All connections are healthy' - : 'One or more redis connections are not healthy', - }, - meta: reports, - } - } - - /** - * Define a custom command using LUA script. You can run the - * registered command using the "runCommand" method. - */ - defineCommand(...args: Parameters): this { - this.connection().defineCommand(...args) - return this - } - - /** - * Run a pre registered command - */ - runCommand(command: string, ...args: any[]): any { - return this.connection().runCommand(command, ...args) - } } - -/** - * Here we attach pubsub and ioRedis methods to the class. - * - * But we also need to inform typescript about the existence of - * these methods. So we are exporting the class with a - * casted type that has these methods. - */ -const RedisManager = RawRedisManager as unknown as RedisManagerFactory - -pubsubMethods.forEach((method) => { - RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { - return this.connection()[method](...args) - } -}) - -ioMethods.forEach((method) => { - RedisManager.prototype[method] = function redisManagerProxyFn(...args: any[]) { - return this.connection()[method](...args) - } -}) - -export default RedisManager diff --git a/src/types/extended.ts b/src/types/extended.ts index d43b747..e4aca4f 100644 --- a/src/types/extended.ts +++ b/src/types/extended.ts @@ -7,29 +7,10 @@ * file that was distributed with this source code. */ -import type { - RedisClusterConnectionContract, - RedisConnectionContract, - RedisService, -} from './main.js' -import { Redis } from 'ioredis' +import type { RedisService } from './main.js' declare module '@adonisjs/core/types' { export interface ContainerBindings { redis: RedisService } - - export interface EventsList { - 'redis:ready': { connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:connect': { connection: RedisClusterConnectionContract | RedisConnectionContract } - 'redis:error': { - error: any - connection: RedisClusterConnectionContract | RedisConnectionContract - } - 'redis:end': { connection: RedisClusterConnectionContract | RedisConnectionContract } - - 'redis:node:added': { connection: RedisClusterConnectionContract; node: Redis } - 'redis:node:removed': { connection: RedisClusterConnectionContract; node: Redis } - 'redis:node:error': { error: any; connection: RedisClusterConnectionContract; address: string } - } } diff --git a/src/types/main.ts b/src/types/main.ts index b3208bd..8bef6be 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -7,16 +7,15 @@ * file that was distributed with this source code. */ -import { EventEmitter } from 'node:events' -import { Redis as IoRedis, RedisOptions, ClusterOptions, Cluster } from 'ioredis' -import { RawRedisClusterConnection } from '../redis_cluster_connection.js' -import { AbstractConnection } from '../abstract_connection.js' -import { RawRedisConnection } from '../redis_connection.js' -import { Emitter } from '@adonisjs/core/events' -import { RawRedisManager } from '../redis_manager.js' +import type { EventEmitter } from 'node:events' +import type { Redis as IoRedis, RedisOptions, ClusterOptions } from 'ioredis' + +import type RedisManager from '../redis_manager.js' +import type RedisConnection from '../connections/redis_connection.js' +import type RedisClusterConnection from '../connections/redis_cluster_connection.js' /** - * Pubsub subscriber + * PubSub subscriber */ export type PubSubChannelHandler = (data: T) => Promise | void export type PubSubPatternHandler = ( @@ -24,22 +23,6 @@ export type PubSubPatternHandler = ( data: T ) => Promise | void -/** - * Redis pub/sub methods - */ -export interface RedisPubSubContract { - publish( - channel: string, - message: string, - callback: (error: Error | null, count: number) => void - ): void - publish(channel: string, message: string): Promise - subscribe(channel: string, handler: PubSubChannelHandler | string): void - psubscribe(pattern: string, handler: PubSubPatternHandler | string): void - unsubscribe(channel: string): void - punsubscribe(pattern: string): void -} - /** * Shape of the report node for the redis connection report */ @@ -52,7 +35,7 @@ export type HealthReportNode = { /** * List of commands on the IORedis. We omit their internal events and pub/sub - * handlers, since we our own. + * handlers, since we have our own. */ export type IORedisCommands = Omit< IoRedis, @@ -67,99 +50,50 @@ export type IORedisCommands = Omit< | 'punsubscribe' | 'quit' | 'publish' + | 'defineCommand' | keyof EventEmitter > /** - * A connection. Can be a cluster or a single connection - */ -export type Connection = RedisClusterConnectionContract | RedisConnectionContract - -/** - * Shape of the connections list - */ -export type RedisConnectionsList = Record - -/** - * Extract the connection type ( either Cluster or single ) from - * a given RedisConnectionsList - */ -export type GetConnectionType< - ConnectionsList extends RedisConnectionsList, - T extends keyof ConnectionsList, -> = ConnectionsList[T] extends RedisClusterConfig - ? RedisClusterConnectionContract - : RedisConnectionContract - -/** - * Shape of standard Redis connection config + * Configuration accepted by the redis connection. It is same + * as ioredis, except the number can be a string as well */ export type RedisConnectionConfig = Omit & { - healthCheck?: boolean port?: string | number } /** - * Shape of cluster config + * Configuration accepted by the RedisClusterConnectionConfig. */ -export type RedisClusterConfig = { +export type RedisClusterConnectionConfig = { clusters: { host: string; port: number | string }[] clusterOptions?: ClusterOptions healthCheck?: boolean } /** - * Since we are dynamically addind methods to the RedisClusterConnection - * RedisConnection and RedisManager classes prototypes, we need to tell - * typescript about it. So the below types represents theses classes with - * all the methods added + * A connection can be a cluster or a single connection */ +export type Connection = RedisClusterConnection | RedisConnection /** - * Type of RedisClusterConnection with dynamically added methods + * A list of multiple connections defined inside the user + * config file */ -export type RedisClusterConnectionContract = RawRedisClusterConnection & - AbstractConnection & - IORedisCommands & - RedisPubSubContract - -/** - * Type of RedisConnection with dynamically added methods - */ -export type RedisConnectionContract = RawRedisConnection & - AbstractConnection & - IORedisCommands & - RedisPubSubContract - -/** - * Type of RedisManager with dynamically added methods - */ -export type RedisManagerContract = - RawRedisManager & IORedisCommands & RedisPubSubContract - -/** - * Factory for creating RedisClusterConnection - */ -export type RedisClusterConnectionFactory = { - new (connectionName: string, config: RedisClusterConfig): RedisClusterConnectionContract -} - -/** - * Factory for creating RedisConnection - */ -export type RedisConnectionFactory = { - new (connectionName: string, config: RedisConnectionConfig): RedisConnectionContract -} +export type RedisConnectionsList = Record< + string, + RedisConnectionConfig | RedisClusterConnectionConfig +> /** - * Factory for creating RedisManager + * Returns the connection class to be used based upon the config */ -export type RedisManagerFactory = { - new ( - config: { connection: keyof ConnectionList; connections: ConnectionList }, - emitter: Emitter - ): RedisManagerContract -} +export type GetConnectionType< + ConnectionsList extends RedisConnectionsList, + T extends keyof ConnectionsList, +> = ConnectionsList[T] extends RedisClusterConnectionConfig + ? RedisClusterConnection + : RedisConnection /** * List of connections inferred from user config @@ -169,9 +103,7 @@ export type InferConnections = /** * Redis service is a singleton redis instance registered - * to the container + * with the container based upon user defined config */ export interface RedisService - extends RedisManagerContract< - RedisConnections extends RedisConnectionsList ? RedisConnections : never - > {} + extends RedisManager {} diff --git a/test/redis_cluster_connection.spec.ts b/test/redis_cluster_connection.spec.ts deleted file mode 100644 index 8667e10..0000000 --- a/test/redis_cluster_connection.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import RedisClusterConnection from '../src/redis_cluster_connection.js' - -const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { - return { host: process.env.REDIS_HOST!, port: Number(port) } -}) - -test.group('Redis cluster factory', () => { - test('emit ready when connected to redis server', ({ assert }, done) => { - const factory = new RedisClusterConnection('main', { clusters: nodes }) - - factory.on('ready', async () => { - assert.isTrue(true) - await factory.quit() - done() - }) - }).waitForDone() - - test('emit node connection event', ({ assert }, done) => { - const factory = new RedisClusterConnection('main', { - clusters: [{ host: process.env.REDIS_HOST!!, port: 7000 }], - }) - - factory.on('node:added', async () => { - assert.isTrue(true) - await factory.quit() - done() - }) - }).waitForDone() - - test('execute redis commands', async ({ assert }) => { - const factory = new RedisClusterConnection('main', { clusters: nodes }) - - await factory.set('greeting', 'hello world') - const greeting = await factory.get('greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) - - test('clean event listeners on quit', async ({ assert }, done) => { - assert.plan(2) - - const factory = new RedisClusterConnection('main', { clusters: nodes }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.quit() - }) - }).waitForDone() - - test('clean event listeners on disconnect', async ({ assert }, done) => { - assert.plan(2) - - const factory = new RedisClusterConnection('main', { - clusters: nodes, - }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.disconnect() - }) - }).waitForDone() - - test('get event for connection errors', async ({ assert }, done) => { - assert.plan(2) - - const factory = new RedisClusterConnection('main', { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', () => {}) - - /** - * `error` event is also emitted - */ - factory.on('node:error', async () => { - await factory.quit() - }) - }).waitForDone() - - test('access cluster nodes', async ({ assert }, done) => { - assert.plan(3) - - const factory = new RedisClusterConnection('main', { clusters: nodes }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - assert.isAbove(factory.nodes().length, 2) // defined in compose file - await factory.quit() - }) - }).waitForDone() - - test('get report for connected connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisClusterConnection('main', { clusters: nodes }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - const report = await factory.getReport(true) - - assert.equal(report.status, 'ready') - assert.isNull(report.error) - assert.isDefined(report.used_memory) - - await factory.quit() - }) - }).waitForDone() - - test('get report for errored connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisClusterConnection('main', { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', async () => { - const report = await factory.getReport(true) - assert.notEqual(report.status, 'ready') - assert.match(report.error.message, /Failed to refresh/) - assert.equal(report.used_memory, null) - - await factory.quit() - }) - }).waitForDone() - - test('execute redis commands using lua scripts', async ({ assert }) => { - const factory = new RedisClusterConnection('main', { clusters: nodes }) - - factory.defineCommand('defineValue', { - numberOfKeys: 1, - lua: `redis.call('set', KEYS[1], ARGV[1])`, - }) - - factory.defineCommand('readValue', { - numberOfKeys: 1, - lua: `return redis.call('get', KEYS[1])`, - }) - - await factory.runCommand('defineValue', 'greeting', 'hello world') - const greeting = await factory.runCommand('readValue', 'greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) -}) diff --git a/test/redis_connection.spec.ts b/test/redis_connection.spec.ts deleted file mode 100644 index e42cc2b..0000000 --- a/test/redis_connection.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import RedisConnection from '../src/redis_connection.js' -import { test } from '@japa/runner' - -test.group('Redis factory', () => { - test('emit ready when connected to redis server', ({ assert }, done) => { - const factory = new RedisConnection('main', { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }) - - factory.on('ready', async () => { - assert.isTrue(true) - await factory.quit() - done() - }) - }).waitForDone() - - test('execute redis commands', async ({ assert }) => { - const factory = new RedisConnection('main', { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }) - - await factory.set('greeting', 'hello world') - - const greeting = await factory.get('greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) - - test('clean event listeners on quit', async ({ assert }, done) => { - const factory = new RedisConnection('main', { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.quit() - }) - }).waitForDone() - - test('clean event listeners on disconnect', async ({ assert }, done) => { - const factory = new RedisConnection('main', { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - await factory.quit() - }) - }).waitForDone() - - test('get event for connection errors', async ({ assert }, done) => { - const factory = new RedisConnection('main', { port: 4444 }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', async (error) => { - assert.equal(error.code, 'ECONNREFUSED') - assert.equal(error.port, 4444) - await factory.quit() - }) - }).waitForDone() - - test('get report for connected connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisConnection('main', { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('ready', async () => { - const report = await factory.getReport(true) - - assert.equal(report.status, 'ready') - assert.isNull(report.error) - assert.isDefined(report.used_memory) - - await factory.quit() - }) - }).waitForDone() - - test('get report for errored connection', async ({ assert }, done) => { - assert.plan(5) - - const factory = new RedisConnection('main', { - host: process.env.REDIS_HOST, - port: 4444, - }) - - factory.on('end', () => { - assert.equal(factory.ioConnection.listenerCount('ready'), 0) - assert.equal(factory.ioConnection.listenerCount('end'), 0) - done() - }) - - factory.on('error', async () => { - const report = await factory.getReport(true) - - assert.notEqual(report.status, 'ready') - assert.equal(report.error.code, 'ECONNREFUSED') - assert.equal(report.used_memory, null) - - await factory.quit() - }) - }).waitForDone() - - test('execute redis commands using lua scripts', async ({ assert }) => { - const factory = new RedisConnection('main', { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }) - - factory.defineCommand('defineValue', { - numberOfKeys: 1, - lua: `redis.call('set', KEYS[1], ARGV[1])`, - }) - - factory.defineCommand('readValue', { - numberOfKeys: 1, - lua: `return redis.call('get', KEYS[1])`, - }) - - await factory.runCommand('defineValue', 'greeting', 'hello world') - const greeting = await factory.runCommand('readValue', 'greeting') - assert.equal(greeting, 'hello world') - - await factory.del('greeting') - await factory.quit() - }) -}) diff --git a/test/redis_manager.spec.ts b/test/redis_manager.spec.ts deleted file mode 100644 index 3419db9..0000000 --- a/test/redis_manager.spec.ts +++ /dev/null @@ -1,313 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { AppFactory } from '@adonisjs/core/factories/app' -import { RedisManagerFactory } from '../factories/redis_manager.js' -import { RedisClusterConnectionContract, RedisConnectionContract } from '../src/types/main.js' - -const clusterNodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { - return { host: process.env.REDIS_HOST!, port: Number(port) } -}) - -export const BASE_URL = new URL('./tmp/', import.meta.url) - -test.group('Redis Manager', () => { - test('.connection() types should be inferrred from config', async ({ expectTypeOf }) => { - const app = new AppFactory().create(BASE_URL, () => {}) - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - }, - }).create(app) - - expectTypeOf(redis.connection).parameter(0).toEqualTypeOf<'primary' | 'secondary' | undefined>() - }) - - test('run redis commands using default connection', async ({ assert }) => { - const app = new AppFactory().create(BASE_URL, () => {}) - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - }, - }).create(app) - - await redis.set('greeting', 'hello-world') - const greeting = await redis.get('greeting') - - assert.equal(greeting, 'hello-world') - - await redis.del('greeting') - await redis.quit('primary') - }) - - test('should trigger a ready event when connection is ready', async ({ assert }, done) => { - const app = new AppFactory().create(BASE_URL, () => {}) - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - }, - }).create(app) - - redis.connection().on('ready', async () => { - assert.isTrue(true) - await redis.quit() - done() - }) - }) - .waitForDone() - .timeout(5000) - - test('run redis commands using the connection method', async ({ assert }) => { - const app = new AppFactory().create(BASE_URL, () => {}) - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - }, - }).create(app) - - await redis.connection().set('greeting', 'hello-world') - const greeting = await redis.connection().get('greeting') - assert.equal(greeting, 'hello-world') - - await redis.connection().del('greeting') - await redis.quit('primary') - }) - - test('re-use connection when connection method is called', async ({ assert }) => { - const app = new AppFactory().create(BASE_URL, () => {}) - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - }, - }).create(app) - - assert.deepEqual(redis.connection(), redis.connection()) - await redis.quit() - }) - - test('connect to redis cluster when cluster array is defined', async ({ assert }, done) => { - const app = new AppFactory().create(BASE_URL, () => {}) - - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - cluster: { clusters: clusterNodes }, - }, - }).create(app) - - redis.connection('cluster').on('ready', async () => { - assert.isAbove(redis.connection('cluster').nodes().length, 2) - await redis.quit() - done() - }) - }).waitForDone() - - test('on disconnect clear connection from tracked list', async ({ assert }, done) => { - const app = new AppFactory().create(BASE_URL, () => {}) - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, - }, - }).create(app) - - const connection = redis.connection() - connection.on('end', () => { - assert.equal(redis.activeConnectionsCount, 0) - done() - }) - - connection.on('ready', async () => { - await redis.quit() - }) - }).waitForDone() - - test('get report for connections marked for healthChecks', async ({ assert }) => { - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - healthCheck: true, - }, - secondary: { - host: process.env.REDIS_HOST, - port: 4444, - }, - }, - }).create(new AppFactory().create(BASE_URL, () => {})) - - const report = await redis.report() - assert.deepEqual(report.health, { healthy: true, message: 'All connections are healthy' }) - assert.lengthOf(report.meta, 1) - assert.isDefined(report.meta[0].used_memory) - assert.equal(report.meta[0].status, 'ready') - await redis.quit() - }) - - test('generate correct report when one of the connections are broken', async ({ assert }) => { - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - healthCheck: true, - }, - secondary: { - host: process.env.REDIS_HOST, - healthCheck: true, - port: 4444, - }, - }, - }).create(new AppFactory().create(BASE_URL, () => {})) - - const report = await redis.report() - - assert.deepEqual(report.health, { - healthy: false, - message: 'One or more redis connections are not healthy', - }) - assert.lengthOf(report.meta, 2) - await redis.quit() - }) - - test('use pub/sub using the manager instance', async ({ assert }, done) => { - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }).create(new AppFactory().create(BASE_URL, () => {})) - - redis.connection().on('subscription:ready', () => { - redis.publish('news', 'breaking news at 9') - }) - - redis.subscribe('news', async (message) => { - assert.equal(message, 'breaking news at 9') - await redis.quit() - done() - }) - }).waitForDone() - - test('execute redis commands using lua scripts', async ({ assert }) => { - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }).create(new AppFactory().create(BASE_URL, () => {})) - - redis.defineCommand('defineValue', { - numberOfKeys: 1, - lua: `redis.call('set', KEYS[1], ARGV[1])`, - }) - - redis.defineCommand('readValue', { - numberOfKeys: 1, - lua: `return redis.call('get', KEYS[1])`, - }) - - await redis.runCommand('defineValue', 'greeting', 'hello world') - const greeting = await redis.runCommand('readValue', 'greeting') - assert.equal(greeting, 'hello world') - - await redis.del('greeting') - await redis.quit() - }) - - test('get and delete the key', async ({ assert }) => { - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }).create(new AppFactory().create(BASE_URL, () => {})) - - await redis.set('greeting', 'hello-world') - - assert.equal(await redis.getdel('greeting'), 'hello-world') - assert.isNull(await redis.get('greeting')) - - await redis.quit('primary') - }) - - test('should have connection types inferred from config', async ({ expectTypeOf }) => { - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }).create(new AppFactory().create(BASE_URL, () => {})) - - expectTypeOf(redis.connection('cluster')).toEqualTypeOf() - expectTypeOf(redis.connection('primary')).toEqualTypeOf() - }) - - test('should have every ioredis methods available', async ({ assert }) => { - const redis = new RedisManagerFactory({ - connection: 'primary', - connections: { - primary: { - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), - }, - cluster: { - clusters: clusterNodes, - }, - }, - }).create(new AppFactory().create(BASE_URL, () => {})) - - /** - * Assert on some obscure methods to make sure - * they are available - */ - assert.isFunction(redis.geohash) - assert.isFunction(redis.spopBuffer) - assert.isFunction(redis.georadiusbymember) - assert.isFunction(redis.xack) - assert.isFunction(redis.xclaim) - assert.isFunction(redis.xgroup) - assert.isFunction(redis.decr) - }) -}) diff --git a/test/configure.spec.ts b/tests/configure.spec.ts similarity index 100% rename from test/configure.spec.ts rename to tests/configure.spec.ts diff --git a/test/define_config.spec.ts b/tests/define_config.spec.ts similarity index 100% rename from test/define_config.spec.ts rename to tests/define_config.spec.ts diff --git a/tests/redis_cluster_connection.spec.ts b/tests/redis_cluster_connection.spec.ts new file mode 100644 index 0000000..a715266 --- /dev/null +++ b/tests/redis_cluster_connection.spec.ts @@ -0,0 +1,168 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { pEvent } from 'p-event' +import { test } from '@japa/runner' +import RedisClusterConnection from '../src/connections/redis_cluster_connection.js' + +const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { + return { host: process.env.REDIS_HOST!, port: Number(port) } +}) + +test.group('Redis cluster factory', () => { + test('emit ready when connected to redis server', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + assert.equal(connection.status, 'ready') + }) + + test('emit connect event before the ready event', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'connect') + await pEvent(connection, 'ready') + assert.equal(connection.status, 'ready') + }) + + test('emit node:added event', async ({ cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'node:added') + }) + + test('execute redis commands', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(async () => { + await connection.del('greeting') + await connection.quit() + }) + + await connection.set('greeting', 'hello world') + const greeting = await connection.get('greeting') + assert.equal(greeting, 'hello world') + }) + + test('clean event listeners on quit', async ({ assert }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + await pEvent(connection, 'ready') + + await Promise.all([pEvent(connection, 'end'), connection.quit()]) + assert.equal(connection.ioConnection.listenerCount('connect'), 0) + assert.equal(connection.ioConnection.listenerCount('ready'), 0) + assert.equal(connection.ioConnection.listenerCount('error'), 0) + assert.equal(connection.ioConnection.listenerCount('close'), 0) + assert.equal(connection.ioConnection.listenerCount('reconnecting'), 0) + assert.equal(connection.ioConnection.listenerCount('end'), 0) + assert.equal(connection.ioConnection.listenerCount('wait'), 0) + }) + + test('clean event listeners on disconnect', async ({ assert }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + await pEvent(connection, 'ready') + + await Promise.all([pEvent(connection, 'end'), connection.disconnect()]) + assert.equal(connection.ioConnection.listenerCount('connect'), 0) + assert.equal(connection.ioConnection.listenerCount('ready'), 0) + assert.equal(connection.ioConnection.listenerCount('error'), 0) + assert.equal(connection.ioConnection.listenerCount('close'), 0) + assert.equal(connection.ioConnection.listenerCount('reconnecting'), 0) + assert.equal(connection.ioConnection.listenerCount('end'), 0) + assert.equal(connection.ioConnection.listenerCount('wait'), 0) + }) + + test('emit node:error when unable to connect', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { + clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], + }) + cleanup(() => connection.quit()) + + connection.on('error', () => {}) + const [error] = await pEvent(connection, 'node:error', { multiArgs: true }) + assert.equal(error.message, 'Connection is closed.') + }) + + test('access cluster nodes', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + assert.isAbove(connection.nodes().length, 2) // defined in compose file + }) + + test('get report for connection in ready state', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + + const report = await connection.getReport(true) + assert.equal(report.status, 'ready') + assert.isNull(report.error) + assert.isDefined(report.used_memory) + }) + + test('get report for errored connection', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { + clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], + }) + + cleanup(() => connection.quit()) + + await pEvent(connection, 'error') + + const report = await connection.getReport(true) + assert.notEqual(report.status, 'ready') + assert.equal(report.error.message, 'Failed to refresh slots cache.') + assert.equal(report.used_memory, null) + }) + + test('execute redis commands using lua scripts', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(async () => { + await connection.del('greeting') + await connection.quit() + }) + + connection.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + connection.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await connection.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await connection.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + }) + + test('subscribe to a channel and listen for messages', async ({ assert, cleanup }) => { + const connection = new RedisClusterConnection('main', { clusters: nodes }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + + const [message] = await Promise.all([ + new Promise((resolve) => { + connection.subscribe('new:user', resolve) + }), + pEvent(connection, 'subscription:ready').then(() => { + connection.publish('new:user', JSON.stringify({ username: 'virk' })) + }), + ]) + + assert.equal(message, JSON.stringify({ username: 'virk' })) + }) +}) diff --git a/tests/redis_connection.spec.ts b/tests/redis_connection.spec.ts new file mode 100644 index 0000000..8cb77ad --- /dev/null +++ b/tests/redis_connection.spec.ts @@ -0,0 +1,332 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { pEvent } from 'p-event' +import { test } from '@japa/runner' +import RedisConnection from '../src/connections/redis_connection.js' + +test.group('Redis connection', () => { + test('emit ready when connected to redis server', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + cleanup(() => connection.quit()) + await pEvent(connection, 'ready') + assert.equal(connection.status, 'ready') + }) + + test('emit connect event before the ready event', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'connect') + await pEvent(connection, 'ready') + assert.equal(connection.status, 'ready') + }) + + test('emit error when unable to connect', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { port: 4444 }) + cleanup(() => connection.disconnect()) + + const [error] = await pEvent(connection, 'error', { multiArgs: true }) + assert.equal(error.message, 'connect ECONNREFUSED 127.0.0.1:4444') + }) + + test('cleanup listeners on quit', async ({ assert }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + await Promise.all([pEvent(connection, 'end'), connection.quit()]) + assert.equal(connection.ioConnection.listenerCount('connect'), 0) + assert.equal(connection.ioConnection.listenerCount('ready'), 0) + assert.equal(connection.ioConnection.listenerCount('error'), 0) + assert.equal(connection.ioConnection.listenerCount('close'), 0) + assert.equal(connection.ioConnection.listenerCount('reconnecting'), 0) + assert.equal(connection.ioConnection.listenerCount('end'), 0) + assert.equal(connection.ioConnection.listenerCount('wait'), 0) + }) + + test('cleanup listeners on disconnect', async ({ assert }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + await Promise.all([pEvent(connection, 'end'), connection.disconnect()]) + assert.equal(connection.ioConnection.listenerCount('connect'), 0) + assert.equal(connection.ioConnection.listenerCount('ready'), 0) + assert.equal(connection.ioConnection.listenerCount('error'), 0) + assert.equal(connection.ioConnection.listenerCount('close'), 0) + assert.equal(connection.ioConnection.listenerCount('reconnecting'), 0) + assert.equal(connection.ioConnection.listenerCount('end'), 0) + assert.equal(connection.ioConnection.listenerCount('wait'), 0) + }) + + test('execute redis commands', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + cleanup(async () => { + await connection.del('greeting') + await connection.quit() + }) + + await connection.set('greeting', 'hello world') + const greeting = await connection.get('greeting') + assert.equal(greeting, 'hello world') + }) + + test('execute redis commands using lua scripts', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(async () => { + await connection.del('greeting') + await connection.quit() + }) + + connection.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + connection.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await connection.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await connection.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + }) + + test('get report for connection in ready state', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + + const report = await connection.getReport(true) + assert.equal(report.status, 'ready') + assert.isNull(report.error) + assert.isDefined(report.used_memory) + }) + + test('get report connection in error state', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: 4444, + }) + cleanup(() => connection.disconnect()) + + await pEvent(connection, 'error') + + const report = await connection.getReport(true) + assert.notEqual(report.status, 'ready') + assert.equal(report.error.code, 'ECONNREFUSED') + assert.equal(report.used_memory, null) + }) + + test('subscribe to a channel and listen for messages', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + + const [message] = await Promise.all([ + new Promise((resolve) => { + connection.subscribe('new:user', resolve) + }), + pEvent(connection, 'subscription:ready').then(() => { + connection.publish('new:user', JSON.stringify({ username: 'virk' })) + }), + ]) + + assert.equal(message, JSON.stringify({ username: 'virk' })) + }) + + test('throw error when subscribing to a channel twice', async ({ cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + + connection.subscribe('new:user', () => {}) + await pEvent(connection, 'subscription:ready') + connection.subscribe('new:user', () => {}) + }).throws('Cannot subscribe to "new:user" channel. Channel already has an active subscription') + + test('subscribe to a pattern and listen for messages', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + + const [message] = await Promise.all([ + new Promise((resolve) => { + connection.psubscribe('user:*', (channel, data) => resolve({ channel, data })) + }), + pEvent(connection, 'psubscription:ready').then(() => { + connection.publish('user:add', JSON.stringify({ username: 'virk' })) + }), + ]) + + assert.equal(message.channel, 'user:add') + assert.equal(message.data, JSON.stringify({ username: 'virk' })) + }) + + test('throw error when subscribing to a pattern twice', async ({ cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + + await pEvent(connection, 'ready') + + connection.psubscribe('user:*', () => {}) + await pEvent(connection, 'psubscription:ready') + + connection.psubscribe('user:*', () => {}) + }).throws('Cannot subscribe to "user:*" pattern. Pattern already has an active subscription') + + test('unsubscribe from a channel', async ({ cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + await pEvent(connection, 'ready') + + await Promise.all([ + new Promise((resolve, reject) => { + connection.subscribe('new:user', () => reject('Not expected to be called')) + setTimeout(() => { + resolve() + }, 1500) + }), + pEvent(connection, 'subscription:ready').then(() => { + return connection.unsubscribe('new:user').then(() => { + connection.publish('new:user', JSON.stringify({ username: 'virk' })) + }) + }), + ]) + }).timeout(4000) + + test('unsubscribe from a pattern', async ({ cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + cleanup(() => connection.quit()) + await pEvent(connection, 'ready') + + await Promise.all([ + new Promise((resolve, reject) => { + connection.psubscribe('user:*', () => reject('Not expected to be called')) + setTimeout(() => { + resolve() + }, 1500) + }), + pEvent(connection, 'psubscription:ready').then(() => { + return connection.punsubscribe('user:*').then(() => { + connection.publish('user:add', JSON.stringify({ username: 'virk' })) + }) + }), + ]) + }).timeout(4000) + + test('emit ready on subscriber connection', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + cleanup(() => connection.quit()) + connection.subscribe('foo', () => {}) + + await pEvent(connection, 'subscriber:ready') + assert.equal(connection.subscriberStatus, 'ready') + }) + + test('emit error when unable to make subscriber connection', async ({ assert, cleanup }) => { + const connection = new RedisConnection('main', { port: 4444 }) + await pEvent(connection, 'error', { multiArgs: true }) + cleanup(() => connection.disconnect()) + + connection.subscribe('foo', () => {}) + const [error] = await pEvent(connection, 'subscriber:error', { multiArgs: true }) + assert.equal(error.message, 'connect ECONNREFUSED 127.0.0.1:4444') + }) + + test('cleanup subscribers listeners on quit', async ({ assert }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + connection.subscribe('foo', () => {}) + await pEvent(connection, 'subscriber:ready') + await pEvent(connection, 'subscription:ready') + + await Promise.all([pEvent(connection, 'subscriber:end'), connection.quit()]) + assert.isUndefined(connection.ioSubscriberConnection) + }) + + test('cleanup subscribers listeners on disconnect', async ({ assert }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + connection.subscribe('foo', () => {}) + await pEvent(connection, 'subscriber:ready') + await pEvent(connection, 'subscription:ready') + + await Promise.all([pEvent(connection, 'subscriber:end'), connection.disconnect()]) + assert.isUndefined(connection.ioSubscriberConnection) + }) + + test('get subscriber status', async ({ assert }) => { + const connection = new RedisConnection('main', { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }) + + assert.isUndefined(connection.subscriberStatus) + + connection.subscribe('foo', () => {}) + await pEvent(connection, 'subscriber:ready') + await pEvent(connection, 'subscription:ready') + + assert.equal(connection.subscriberStatus, 'ready') + }) +}) diff --git a/tests/redis_manager.spec.ts b/tests/redis_manager.spec.ts new file mode 100644 index 0000000..2c8d411 --- /dev/null +++ b/tests/redis_manager.spec.ts @@ -0,0 +1,230 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { pEvent } from 'p-event' +import { test } from '@japa/runner' + +import { RedisManagerFactory } from '../factories/redis_manager.js' +import RedisConnection from '../src/connections/redis_connection.js' +import RedisClusterConnection from '../src/connections/redis_cluster_connection.js' + +const clusterNodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { + return { host: process.env.REDIS_HOST!, port: Number(port) } +}) + +export const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Redis Manager', () => { + test('.connection() types should be inferred from config', async ({ expectTypeOf }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + expectTypeOf(redis.connection).parameter(0).toEqualTypeOf<'primary' | 'secondary' | undefined>() + }) + + test('throw error when trying to use unregistered redis connection', async () => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + // @ts-expect-error + redis.connection('foo') + }).throws('Redis connection "foo" is not defined') + + test('run redis commands using default connection', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await redis.connection().set('greeting', 'hello-world') + const greeting = await redis.connection().get('greeting') + + assert.equal(greeting, 'hello-world') + + await redis.connection().del('greeting') + await redis.quit('primary') + }) + + test('run redis commands using an explicit connection', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await redis.connection('primary').set('greeting', 'hello-world') + const greeting = await redis.connection('primary').get('greeting') + assert.equal(greeting, 'hello-world') + + await redis.connection('primary').del('greeting') + await redis.quit('primary') + }) + + test('cache connections', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + assert.strictEqual(redis.connection(), redis.connection()) + await redis.quit() + }) + + test('connect to redis cluster when cluster array is defined', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + cluster: { clusters: clusterNodes }, + }, + }).create() + + await pEvent(redis.connection('cluster'), 'ready') + assert.isAbove(redis.connection('cluster').nodes().length, 2) + await redis.quit() + }) + + test('on quit clear connection from tracked list', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await pEvent(redis.connection(), 'ready') + + await Promise.all([pEvent(redis.connection(), 'end'), redis.quit()]) + assert.equal(redis.activeConnectionsCount, 0) + }) + + test('quit all connections', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await pEvent(redis.connection(), 'ready') + + await Promise.all([ + pEvent(redis.connection(), 'end'), + pEvent(redis.connection('secondary'), 'end'), + redis.quitAll(), + ]) + assert.equal(redis.activeConnectionsCount, 0) + }) + + test('on disconnect clear connection from tracked list', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await pEvent(redis.connection(), 'ready') + + await Promise.all([pEvent(redis.connection(), 'end'), redis.disconnect()]) + assert.equal(redis.activeConnectionsCount, 0) + }) + + test('disconnect all connections', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await pEvent(redis.connection(), 'ready') + + await Promise.all([ + pEvent(redis.connection(), 'end'), + pEvent(redis.connection('secondary'), 'end'), + redis.disconnectAll(), + ]) + assert.equal(redis.activeConnectionsCount, 0) + }) + + test('noop when trying to quit a non-existing connection', async () => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + redis.quit() + }) + + test('noop when trying to disconnect a non-existing connection', async () => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + secondary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + redis.disconnect() + }) + + test('clear connection from tracked list when quit from the connection instance directly', async ({ + assert, + }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await pEvent(redis.connection(), 'ready') + + await Promise.all([pEvent(redis.connection(), 'end'), redis.connection().quit()]) + assert.equal(redis.activeConnectionsCount, 0) + }) + + test('should have connection types inferred from config', async ({ expectTypeOf }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + cluster: { + clusters: clusterNodes, + }, + }, + }).create() + + expectTypeOf(redis.connection('cluster')).toEqualTypeOf() + expectTypeOf(redis.connection('primary')).toEqualTypeOf() + }) +}) diff --git a/test/redis_provider.spec.ts b/tests/redis_provider.spec.ts similarity index 52% rename from test/redis_provider.spec.ts rename to tests/redis_provider.spec.ts index dd853fa..49078a2 100644 --- a/test/redis_provider.spec.ts +++ b/tests/redis_provider.spec.ts @@ -7,8 +7,12 @@ * file that was distributed with this source code. */ -import { IgnitorFactory } from '@adonisjs/core/factories' +import { pEvent } from 'p-event' import { test } from '@japa/runner' +import { IgnitorFactory } from '@adonisjs/core/factories' + +import { defineConfig } from '../index.js' +import RedisManager from '../src/redis_manager.js' const BASE_URL = new URL('./tmp/', import.meta.url) @@ -29,7 +33,7 @@ test.group('Redis Provider', () => { await app.init() await app.boot() - assert.isTrue(app.container.hasBinding('redis')) + assert.instanceOf(await app.container.make('redis'), RedisManager) }) test('define repl bindings', async ({ assert }) => { @@ -53,4 +57,41 @@ test.group('Redis Provider', () => { assert.property(repl.getMethods(), 'loadRedis') assert.isFunction(repl.getMethods().loadRedis.handler) }) + + test('disconnect all connections on app termination', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['./providers/redis_provider.js'], + }, + }) + .withCoreConfig() + .merge({ + config: { + redis: defineConfig({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }, + }, + }), + }, + }) + .create(BASE_URL, { + importer: (filePath) => import(new URL(filePath, new URL('../', import.meta.url)).href), + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + const redis = await app.container.make('redis') + assert.isNull(await redis.connection().get('username')) + assert.equal(redis.activeConnectionsCount, 1) + + await Promise.all([pEvent(redis.connection(), 'end'), app.terminate()]) + assert.equal(redis.activeConnectionsCount, 0) + }) }) From 311ae4abb2643072966f70fa51ccdccd68750c64 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 27 Jul 2023 14:25:28 +0530 Subject: [PATCH 29/71] refactor: merge pubsub methods collection with io methods --- src/connections/io_methods.ts | 9 +++++++-- src/connections/pubsub_methods.ts | 13 ------------- 2 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 src/connections/pubsub_methods.ts diff --git a/src/connections/io_methods.ts b/src/connections/io_methods.ts index 288ab81..b22cbf4 100644 --- a/src/connections/io_methods.ts +++ b/src/connections/io_methods.ts @@ -8,7 +8,6 @@ */ import { Redis } from 'ioredis' -import { pubSubMethods } from './pubsub_methods.js' /** * Returns all method names for a given class @@ -41,7 +40,13 @@ const ignoredMethods = [ '__proto__', 'defineCommand', 'toLocaleString', -].concat(pubSubMethods) + // PubSub methods + 'subscribe', + 'unsubscribe', + 'psubscribe', + 'punsubscribe', + 'publish', +] /** * List of methods on Redis class diff --git a/src/connections/pubsub_methods.ts b/src/connections/pubsub_methods.ts deleted file mode 100644 index 0c1f687..0000000 --- a/src/connections/pubsub_methods.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/** - * An array of methods that exists on the connection class - */ -export const pubSubMethods = ['subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe', 'publish'] From 8512c879970c588149f4e201d05fb9c9c130b212 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 27 Jul 2023 14:35:45 +0530 Subject: [PATCH 30/71] refactor: abstracts errors --- index.ts | 1 + src/connections/abstract_connection.ts | 19 +++---------- src/define_config.ts | 38 ++++++++++++++------------ src/errors.ts | 22 +++++++++++++++ src/redis_manager.ts | 3 +- tests/define_config.spec.ts | 23 ++++++++++++---- 6 files changed, 67 insertions(+), 39 deletions(-) create mode 100644 src/errors.ts diff --git a/index.ts b/index.ts index fae4259..c9fa3fa 100644 --- a/index.ts +++ b/index.ts @@ -12,3 +12,4 @@ import './src/types/extended.js' export { defineConfig } from './src/define_config.js' export { stubsRoot } from './stubs/index.js' export { configure } from './configure.js' +export * as errors from './src/errors.js' diff --git a/src/connections/abstract_connection.ts b/src/connections/abstract_connection.ts index d27a9e2..96b53a7 100644 --- a/src/connections/abstract_connection.ts +++ b/src/connections/abstract_connection.ts @@ -8,9 +8,10 @@ */ import { EventEmitter } from 'node:events' -import { Exception } from '@poppinss/utils' import type { Redis, Cluster } from 'ioredis' import { setTimeout } from 'node:timers/promises' + +import * as errors from '../errors.js' import type { HealthReportNode, PubSubChannelHandler, PubSubPatternHandler } from '../types/main.js' /** @@ -245,13 +246,7 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.subscriptions.has(channel)) { - throw new Exception( - `Cannot subscribe to "${channel}" channel. Channel already has an active subscription`, - { - code: 'E_MULTIPLE_REDIS_SUBSCRIPTIONS', - status: 500, - } - ) + throw new errors.E_MULTIPLE_REDIS_SUBSCRIPTIONS([channel]) } /** @@ -291,13 +286,7 @@ export abstract class AbstractConnection extends Even * Disallow multiple subscriptions to a single channel */ if (this.psubscriptions.has(pattern)) { - throw new Exception( - `Cannot subscribe to "${pattern}" pattern. Pattern already has an active subscription`, - { - status: 500, - code: 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS', - } - ) + throw new errors.E_MULTIPLE_REDIS_PSUBSCRIPTIONS([pattern]) } /** diff --git a/src/define_config.ts b/src/define_config.ts index b51dbdc..9588778 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -7,34 +7,38 @@ * file that was distributed with this source code. */ -import { InvalidArgumentsException } from '@poppinss/utils' +import { RuntimeException } from '@poppinss/utils' import type { RedisConnectionsList } from './types/main.js' -/** - * Expected shape of the config accepted by the "defineConfig" - * method - */ -type RedisConfig = { - connections: RedisConnectionsList -} - /** * Define config for redis */ -export function defineConfig( - config: T -): T { +export function defineConfig(config: { + connection: keyof Connections + connections: Connections +}): { + connection: keyof Connections + connections: Connections +} { if (!config) { - throw new InvalidArgumentsException('Invalid config. It must be a valid object') + throw new RuntimeException('Invalid config. It must be an object') } if (!config.connections) { - throw new InvalidArgumentsException('Invalid config. Missing property "connections" inside it') + throw new RuntimeException('Missing "connections" property in the redis config file') + } + + if (!config.connection) { + throw new RuntimeException( + 'Missing "connection" property in redis config. Specify a default connection to use' + ) } - if (!config.connection || !(config.connection in config.connections)) { - throw new InvalidArgumentsException( - 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object' + if (!config.connections[config.connection]) { + throw new RuntimeException( + `Missing "connections.${String( + config.connection + )}". It is referenced by the "default" redis connection` ) } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..2790681 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,22 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createError } from '@poppinss/utils' + +export const E_MULTIPLE_REDIS_SUBSCRIPTIONS = createError<[string]>( + 'Cannot subscribe to "%s" channel. Channel already has an active subscription', + 'E_MULTIPLE_REDIS_SUBSCRIPTIONS', + 500 +) + +export const E_MULTIPLE_REDIS_PSUBSCRIPTIONS = createError<[string]>( + 'Cannot subscribe to "%s" pattern. Pattern already has an active subscription', + 'E_MULTIPLE_REDIS_PSUBSCRIPTIONS', + 500 +) diff --git a/src/redis_manager.ts b/src/redis_manager.ts index 77c3faf..4f7c03d 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import { RuntimeException } from '@poppinss/utils' import RedisConnection from './connections/redis_connection.js' import RedisClusterConnection from './connections/redis_cluster_connection.js' import type { GetConnectionType, RedisConnectionsList } from './types/main.js' @@ -65,7 +66,7 @@ export default class RedisManager */ const config = this.#config.connections[name] if (!config) { - throw new Error(`Redis connection "${name.toString()}" is not defined`) + throw new RuntimeException(`Redis connection "${name.toString()}" is not defined`) } /** diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts index f3f75d9..77cf74b 100644 --- a/tests/define_config.spec.ts +++ b/tests/define_config.spec.ts @@ -12,15 +12,15 @@ import { defineConfig } from '../src/define_config.js' test.group('Define Config', () => { test('should throw if no config passed', ({ assert }) => { - // @ts-ignore - assert.throws(defineConfig, 'Invalid config. It must be a valid object') + // @ts-expect-error + assert.throws(defineConfig, 'Invalid config. It must be an object') }) test('should throw if no connections', ({ assert }) => { assert.throws( - // @ts-ignore + // @ts-expect-error () => defineConfig({ connection: 'hey' }), - 'Invalid config. Missing property "connections" inside it' + 'Missing "connections" property in the redis config file' ) }) @@ -28,11 +28,22 @@ test.group('Define Config', () => { assert.throws( () => defineConfig({ - // @ts-ignore + // @ts-expect-error connection: 'hey', connections: {}, }), - 'Invalid config. Missing property "connection" or the connection name is not defined inside "connections" object' + 'Missing "connections.hey". It is referenced by the "default" redis connection' + ) + }) + + test('should throw if default connection is not defined', ({ assert }) => { + assert.throws( + () => + // @ts-expect-error + defineConfig({ + connections: {}, + }), + 'Missing "connection" property in redis config. Specify a default connection to use' ) }) }) From dc1c6da360a03c5cdd41405d9379ce2dcc467941 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 10:14:19 +0530 Subject: [PATCH 31/71] refactor: add debug logs --- src/connections/redis_cluster_connection.ts | 3 +++ src/connections/redis_connection.ts | 3 +++ src/debug.ts | 12 ++++++++++++ src/redis_manager.ts | 6 ++++++ 4 files changed, 24 insertions(+) create mode 100644 src/debug.ts diff --git a/src/connections/redis_cluster_connection.ts b/src/connections/redis_cluster_connection.ts index 9eee565..60c2699 100644 --- a/src/connections/redis_cluster_connection.ts +++ b/src/connections/redis_cluster_connection.ts @@ -9,6 +9,7 @@ import Redis, { type Cluster, type NodeRole } from 'ioredis' +import debug from '../debug.js' import { ioMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' import type { IORedisCommands, RedisClusterConnectionConfig } from '../types/main.js' @@ -22,6 +23,7 @@ export class RedisClusterConnection extends AbstractConnection { #config: RedisClusterConnectionConfig constructor(connectionName: string, config: RedisClusterConnectionConfig) { + debug('creating cluster connection %s: %O', connectionName, config) super(connectionName) this.#config = config @@ -37,6 +39,7 @@ export class RedisClusterConnection extends AbstractConnection { * invoke this method when first subscription is created. */ protected makeSubscriberConnection() { + debug('creating subscriber connection') this.ioSubscriberConnection = new Redis.Cluster( this.#config.clusters as [], this.#config.clusterOptions diff --git a/src/connections/redis_connection.ts b/src/connections/redis_connection.ts index 9455651..3bf534f 100644 --- a/src/connections/redis_connection.ts +++ b/src/connections/redis_connection.ts @@ -9,6 +9,7 @@ import { Redis, RedisOptions } from 'ioredis' +import debug from '../debug.js' import { ioMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' import { IORedisCommands, RedisConnectionConfig } from '../types/main.js' @@ -23,6 +24,7 @@ export class RedisConnection extends AbstractConnection { #config: RedisOptions constructor(connectionName: string, config: RedisConnectionConfig) { + debug('creating connection %s: %O', connectionName, config) super(connectionName) this.#config = this.#normalizeConfig(config) @@ -45,6 +47,7 @@ export class RedisConnection extends AbstractConnection { * invoke this method when first subscription is created. */ protected makeSubscriberConnection() { + debug('creating subscriber connection') this.ioSubscriberConnection = new Redis(this.#config) this.monitorSubscriberConnection() } diff --git a/src/debug.ts b/src/debug.ts new file mode 100644 index 0000000..c0decfd --- /dev/null +++ b/src/debug.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { debuglog } from 'node:util' + +export default debuglog('adonisjs:redis') diff --git a/src/redis_manager.ts b/src/redis_manager.ts index 4f7c03d..6f0306d 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -8,6 +8,8 @@ */ import { RuntimeException } from '@poppinss/utils' + +import debug from './debug.js' import RedisConnection from './connections/redis_connection.js' import RedisClusterConnection from './connections/redis_cluster_connection.js' import type { GetConnectionType, RedisConnectionsList } from './types/main.js' @@ -53,11 +55,13 @@ export default class RedisManager connectionName?: ConnectionName ): GetConnectionType { const name = connectionName || this.#config.connection + debug('resolving connection %s', name) /** * Return existing connection if already exists */ if (this.activeConnections[name]) { + debug('reusing existing connection %s', name) return this.activeConnections[name] as GetConnectionType } @@ -72,6 +76,7 @@ export default class RedisManager /** * Instantiate the connection based upon the config */ + debug('creating new connection %s', name) const connection = 'clusters' in config ? new RedisClusterConnection(name as string, config) @@ -81,6 +86,7 @@ export default class RedisManager * Remove connection from the list of tracked connections */ connection.on('end', ($connection) => { + debug('%s connection closed. Removing from tracked connections list', name) delete this.activeConnections[$connection.connectionName] }) From b4e98cc1ca49d7a40565288315f37d17564cad42 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 10:16:54 +0530 Subject: [PATCH 32/71] chore(release): 8.0.0-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index daa5a99..0093e2c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-3", + "version": "8.0.0-4", "type": "module", "main": "build/index.js", "files": [ From 55021257b9b1cc3100c6e879c727ef7072ab4c2d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 10:32:41 +0530 Subject: [PATCH 33/71] refactor: move code to infer types within the config file --- configure.ts | 5 ----- stubs/config/redis.stub | 9 +++++++-- stubs/types/redis.stub | 8 -------- 3 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 stubs/types/redis.stub diff --git a/configure.ts b/configure.ts index 8abef65..5cf8327 100644 --- a/configure.ts +++ b/configure.ts @@ -18,11 +18,6 @@ export async function configure(command: Configure) { */ await command.publishStub('config/redis.stub') - /** - * Publish typings file - */ - await command.publishStub('types/redis.stub') - /** * Add environment variables */ diff --git a/stubs/config/redis.stub b/stubs/config/redis.stub index a3e782c..4bce2a2 100644 --- a/stubs/config/redis.stub +++ b/stubs/config/redis.stub @@ -1,11 +1,10 @@ --- to: {{ app.configPath('redis.ts') }} --- - import env from '#start/env' import { defineConfig } from '@adonisjs/redis' -export default defineConfig({ +const redisConfig = defineConfig({ connection: env.get('REDIS_CONNECTION'), connections: { @@ -28,3 +27,9 @@ export default defineConfig({ }, }, }) + +export default redisConfig + +declare module '@adonisjs/redis/types' { + export interface RedisConnections extends InferConnections {} +} diff --git a/stubs/types/redis.stub b/stubs/types/redis.stub deleted file mode 100644 index 8587d87..0000000 --- a/stubs/types/redis.stub +++ /dev/null @@ -1,8 +0,0 @@ ---- -to: {{ app.makePath('types/redis.ts') }} ---- -import redis from '#config/redis' - -declare module '@adonisjs/redis/types' { - export interface RedisConnections extends InferConnections {} -} From 0a68e9ba53edee7c2a757f01d9039a4bb43b4169 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 10:33:38 +0530 Subject: [PATCH 34/71] docs: update README file --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5e1017a..13b2fbf 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@
-[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![snyk-image]][snyk-url] +[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] ## Introduction Redis provider for AdonisJS with support for multiple Redis connections, cluster, pub/sub and much more ## Official Documentation -The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/repl) +The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/redis) ## Contributing One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. @@ -19,7 +19,7 @@ We encourage you to read the [contribution guide](https://github.com/adonisjs/.g Easiest way to run tests is to launch the redis cluster using docker-compose and `docker-compose.yml` file. ```sh -docker-compose up -d +docker-compose up npm run test ``` @@ -41,6 +41,3 @@ AdonisJS Redis is open-sourced software licensed under the [MIT license](LICENSE [license-url]: LICENSE.md [license-image]: https://img.shields.io/github/license/adonisjs/redis?style=for-the-badge - -[snyk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/redis?label=Snyk%20Vulnerabilities&style=for-the-badge -[snyk-url]: https://snyk.io/test/github/adonisjs/redis?targetFile=package.json "snyk" From 80e5027a7c7e2e08d1e815c1db89f99059c770e5 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 10:38:20 +0530 Subject: [PATCH 35/71] test: fix breaking test --- tests/configure.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 6ba2779..a1b69c4 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -46,14 +46,15 @@ test.group('Configure', (group) => { context.fs.basePath = fileURLToPath(BASE_URL) }) - test('publish config and types files', async ({ assert }) => { + test('publish config file', async ({ assert }) => { const { command } = await setupConfigureCommand() await command.exec() await assert.fileExists('config/redis.ts') - await assert.fileContains('config/redis.ts', 'export default defineConfig({') - await assert.fileExists('types/redis.ts') + await assert.fileContains('config/redis.ts', 'const redisConfig = defineConfig({') + await assert.fileContains('config/redis.ts', 'export default redisConfig') + await assert.fileContains('config/redis.ts', `declare module '@adonisjs/redis/types'`) }) test('add redis_provider to the rc file', async ({ assert }) => { From 8300ac1db33ae4588f45797d2eef75b0b3682120 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 10:39:02 +0530 Subject: [PATCH 36/71] chore(release): 8.0.0-5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0093e2c..33c0f0e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-4", + "version": "8.0.0-5", "type": "module", "main": "build/index.js", "files": [ From 7dd89a08c27c769089e6ede5fd8c13b92bac238d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 12:18:01 +0530 Subject: [PATCH 37/71] feat: add ability to run commands on the RedisManager class --- src/connections/abstract_connection.ts | 8 + src/connections/io_methods.ts | 679 ++++++++++++++++++-- src/connections/redis_cluster_connection.ts | 12 +- src/connections/redis_connection.ts | 23 +- src/redis_manager.ts | 14 +- src/types/main.ts | 26 +- tests/redis_manager.spec.ts | 17 + 7 files changed, 710 insertions(+), 69 deletions(-) diff --git a/src/connections/abstract_connection.ts b/src/connections/abstract_connection.ts index 96b53a7..4eafa73 100644 --- a/src/connections/abstract_connection.ts +++ b/src/connections/abstract_connection.ts @@ -73,6 +73,14 @@ export abstract class AbstractConnection extends Even return this.ioSubscriberConnection?.status } + /** + * Get the number of commands queued in automatic pipelines. + * This is not available (and returns 0) until the cluster is connected and slots information have been received. + */ + get autoPipelineQueueSize() { + return this.ioConnection.autoPipelineQueueSize + } + /** * Parent class must implement makeSubscriberConnection */ diff --git a/src/connections/io_methods.ts b/src/connections/io_methods.ts index b22cbf4..7000a44 100644 --- a/src/connections/io_methods.ts +++ b/src/connections/io_methods.ts @@ -7,50 +7,647 @@ * file that was distributed with this source code. */ -import { Redis } from 'ioredis' +import type { Redis, Cluster } from 'ioredis' /** - * Returns all method names for a given class + * Base methods are shared by a regular Redis connection and + * the cluster connection. + * https://redis.github.io/ioredis/classes/Redis.html */ -function getAllMethodNames(obj: any) { - let methods = new Set() - while ((obj = Reflect.getPrototypeOf(obj))) { - let keys = Reflect.ownKeys(obj) - keys.forEach((k) => methods.add(k)) - } - return [...methods] as string[] -} - -const ignoredMethods = [ - 'constructor', - 'status', - 'connect', - 'disconnect', - 'duplicate', - 'quit', - '__defineGetter__', - '__defineSetter__', - 'hasOwnProperty', - '__lookupGetter__', - '__lookupSetter__', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'toString', - 'valueOf', - '__proto__', - 'defineCommand', - 'toLocaleString', - // PubSub methods - 'subscribe', - 'unsubscribe', - 'psubscribe', - 'punsubscribe', - 'publish', -] +export const baseMethods = [ + 'acl', + 'aclBuffer', + 'addBuiltinCommand', + 'append', + 'asking', + 'auth', + 'bgrewriteaof', + 'bgrewriteaofBuffer', + 'bgsave', + 'bitcount', + 'bitfield', + 'bitfield_ro', + 'bitop', + 'bitpos', + 'blmove', + 'blmoveBuffer', + 'blmpop', + 'blmpopBuffer', + 'blpop', + 'blpopBuffer', + 'brpop', + 'brpopBuffer', + 'brpoplpush', + 'brpoplpushBuffer', + 'bzmpop', + 'bzpopmax', + 'bzpopmaxBuffer', + 'bzpopmin', + 'bzpopminBuffer', + 'call', + 'callBuffer', + 'client', + 'clientBuffer', + 'cluster', + 'command', + 'config', + 'copy', + 'createBuiltinCommand', + 'dbsize', + 'debug', + 'decr', + 'decrby', + 'del', + 'discard', + 'dump', + 'dumpBuffer', + 'echo', + 'echoBuffer', + 'eval', + 'eval_ro', + 'evalsha', + 'evalsha_ro', + 'exec', + 'exists', + 'expire', + 'expireat', + 'expiretime', + 'failover', + 'fcall', + 'fcall_ro', + 'flushall', + 'flushdb', + 'function', + 'functionBuffer', + 'geoadd', + 'geodist', + 'geodistBuffer', + 'geohash', + 'geohashBuffer', + 'geopos', + 'georadius', + 'georadius_ro', + 'georadiusbymember', + 'georadiusbymember_ro', + 'geosearch', + 'geosearchstore', + 'get', + 'getBuffer', + 'getBuiltinCommands', + 'getbit', + 'getdel', + 'getdelBuffer', + 'getex', + 'getexBuffer', + 'getrange', + 'getrangeBuffer', + 'getset', + 'getsetBuffer', + 'hdel', + 'hello', + 'hexists', + 'hget', + 'hgetBuffer', + 'hgetall', + 'hgetallBuffer', + 'hincrby', + 'hincrbyfloat', + 'hincrbyfloatBuffer', + 'hkeys', + 'hkeysBuffer', + 'hlen', + 'hmget', + 'hmgetBuffer', + 'hmset', + 'hrandfield', + 'hrandfieldBuffer', + 'hscan', + 'hscanBuffer', + 'hscanBufferStream', + 'hscanStream', + 'hset', + 'hsetnx', + 'hstrlen', + 'hvals', + 'hvalsBuffer', + 'incr', + 'incrby', + 'incrbyfloat', + 'info', + 'keys', + 'keysBuffer', + 'lastsave', + 'latency', + 'lcs', + 'lindex', + 'lindexBuffer', + 'linsert', + 'llen', + 'lmove', + 'lmoveBuffer', + 'lmpop', + 'lmpopBuffer', + 'lolwut', + 'lpop', + 'lpopBuffer', + 'lpos', + 'lpush', + 'lpushx', + 'lrange', + 'lrangeBuffer', + 'lrem', + 'lset', + 'ltrim', + 'memory', + 'mget', + 'mgetBuffer', + 'migrate', + 'module', + 'move', + 'mset', + 'msetnx', + 'multi', + 'object', + 'persist', + 'pexpire', + 'pexpireat', + 'pexpiretime', + 'pfadd', + 'pfcount', + 'pfdebug', + 'pfmerge', + 'pfselftest', + 'ping', + 'pingBuffer', + 'pipeline', + 'psetex', + 'psync', + 'pttl', + 'pubsub', + 'randomkey', + 'randomkeyBuffer', + 'readonly', + 'readwrite', + 'rename', + 'renamenx', + 'replconf', + 'replicaof', + 'reset', + 'restore', + 'restore-asking', + 'role', + 'rpop', + 'rpopBuffer', + 'rpoplpush', + 'rpoplpushBuffer', + 'rpush', + 'rpushx', + 'sadd', + 'save', + 'scan', + 'scanBuffer', + 'scard', + 'script', + 'sdiff', + 'sdiffBuffer', + 'sdiffstore', + 'select', + 'set', + 'setBuffer', + 'setbit', + 'setex', + 'setnx', + 'setrange', + 'shutdown', + 'sinter', + 'sinterBuffer', + 'sintercard', + 'sinterstore', + 'sismember', + 'slaveof', + 'slowlog', + 'smembers', + 'smembersBuffer', + 'smismember', + 'smove', + 'sort', + 'sort_ro', + 'spop', + 'spopBuffer', + 'spublish', + 'srandmember', + 'srandmemberBuffer', + 'srem', + 'sscan', + 'sscanBuffer', + 'sscanBufferStream', + 'sscanStream', + 'strlen', + 'substr', + 'sunion', + 'sunionBuffer', + 'sunionstore', + 'swapdb', + 'sync', + 'time', + 'touch', + 'ttl', + 'type', + 'unlink', + 'unwatch', + 'wait', + 'watch', + 'xack', + 'xadd', + 'xaddBuffer', + 'xautoclaim', + 'xclaim', + 'xdel', + 'xgroup', + 'xinfo', + 'xlen', + 'xpending', + 'xrange', + 'xrangeBuffer', + 'xread', + 'xreadBuffer', + 'xreadgroup', + 'xrevrange', + 'xrevrangeBuffer', + 'xsetid', + 'xtrim', + 'zadd', + 'zaddBuffer', + 'zcard', + 'zcount', + 'zdiff', + 'zdiffBuffer', + 'zdiffstore', + 'zincrby', + 'zincrbyBuffer', + 'zinter', + 'zinterBuffer', + 'zintercard', + 'zinterstore', + 'zlexcount', + 'zmpop', + 'zmscore', + 'zmscoreBuffer', + 'zpopmax', + 'zpopmaxBuffer', + 'zpopmin', + 'zpopminBuffer', + 'zrandmember', + 'zrandmemberBuffer', + 'zrange', + 'zrangeBuffer', + 'zrangebylex', + 'zrangebylexBuffer', + 'zrangebyscore', + 'zrangebyscoreBuffer', + 'zrangestore', + 'zrank', + 'zrem', + 'zremrangebylex', + 'zremrangebyrank', + 'zremrangebyscore', + 'zrevrange', + 'zrevrangeBuffer', + 'zrevrangebylex', + 'zrevrangebylexBuffer', + 'zrevrangebyscore', + 'zrevrangebyscoreBuffer', + 'zrevrank', + 'zscan', + 'zscanBuffer', + 'zscanBufferStream', + 'zscanStream', + 'zscore', + 'zscoreBuffer', + 'zunion', + 'zunionBuffer', + 'zunionstore', +] satisfies (keyof Cluster)[] /** - * List of methods on Redis class + * Methods available on a non-cluster Redis + * connection */ -export const ioMethods = getAllMethodNames(Redis.prototype).filter( - (method) => !ignoredMethods.includes(method) -) as string[] +export const redisMethods = [ + 'acl', + 'aclBuffer', + 'addBuiltinCommand', + 'append', + 'asking', + 'auth', + 'bgrewriteaof', + 'bgrewriteaofBuffer', + 'bgsave', + 'bitcount', + 'bitfield', + 'bitfield_ro', + 'bitop', + 'bitpos', + 'blmove', + 'blmoveBuffer', + 'blmpop', + 'blmpopBuffer', + 'blpop', + 'blpopBuffer', + 'brpop', + 'brpopBuffer', + 'brpoplpush', + 'brpoplpushBuffer', + 'bzmpop', + 'bzpopmax', + 'bzpopmaxBuffer', + 'bzpopmin', + 'bzpopminBuffer', + 'call', + 'callBuffer', + 'client', + 'clientBuffer', + 'cluster', + 'command', + 'config', + 'copy', + 'createBuiltinCommand', + 'dbsize', + 'debug', + 'decr', + 'decrby', + 'del', + 'discard', + 'dump', + 'dumpBuffer', + 'echo', + 'echoBuffer', + 'eval', + 'eval_ro', + 'evalsha', + 'evalsha_ro', + 'exec', + 'exists', + 'expire', + 'expireat', + 'expiretime', + 'failover', + 'fcall', + 'fcall_ro', + 'flushall', + 'flushdb', + 'function', + 'functionBuffer', + 'geoadd', + 'geodist', + 'geodistBuffer', + 'geohash', + 'geohashBuffer', + 'geopos', + 'georadius', + 'georadius_ro', + 'georadiusbymember', + 'georadiusbymember_ro', + 'geosearch', + 'geosearchstore', + 'get', + 'getBuffer', + 'getBuiltinCommands', + 'getbit', + 'getdel', + 'getdelBuffer', + 'getex', + 'getexBuffer', + 'getrange', + 'getrangeBuffer', + 'getset', + 'getsetBuffer', + 'hdel', + 'hello', + 'hexists', + 'hget', + 'hgetBuffer', + 'hgetall', + 'hgetallBuffer', + 'hincrby', + 'hincrbyfloat', + 'hincrbyfloatBuffer', + 'hkeys', + 'hkeysBuffer', + 'hlen', + 'hmget', + 'hmgetBuffer', + 'hmset', + 'hrandfield', + 'hrandfieldBuffer', + 'hscan', + 'hscanBuffer', + 'hscanBufferStream', + 'hscanStream', + 'hset', + 'hsetnx', + 'hstrlen', + 'hvals', + 'hvalsBuffer', + 'incr', + 'incrby', + 'incrbyfloat', + 'info', + 'keys', + 'keysBuffer', + 'lastsave', + 'latency', + 'lcs', + 'lindex', + 'lindexBuffer', + 'linsert', + 'llen', + 'lmove', + 'lmoveBuffer', + 'lmpop', + 'lmpopBuffer', + 'lolwut', + 'lpop', + 'lpopBuffer', + 'lpos', + 'lpush', + 'lpushx', + 'lrange', + 'lrangeBuffer', + 'lrem', + 'lset', + 'ltrim', + 'memory', + 'mget', + 'mgetBuffer', + 'migrate', + 'module', + 'move', + 'mset', + 'msetnx', + 'multi', + 'object', + 'persist', + 'pexpire', + 'pexpireat', + 'pexpiretime', + 'pfadd', + 'pfcount', + 'pfdebug', + 'pfmerge', + 'pfselftest', + 'ping', + 'pingBuffer', + 'pipeline', + 'psetex', + 'psync', + 'pttl', + 'pubsub', + 'randomkey', + 'randomkeyBuffer', + 'readonly', + 'readwrite', + 'rename', + 'renamenx', + 'replconf', + 'replicaof', + 'reset', + 'restore', + 'restore-asking', + 'role', + 'rpop', + 'rpopBuffer', + 'rpoplpush', + 'rpoplpushBuffer', + 'rpush', + 'rpushx', + 'sadd', + 'save', + 'scan', + 'scanBuffer', + 'scard', + 'script', + 'sdiff', + 'sdiffBuffer', + 'sdiffstore', + 'select', + 'set', + 'setBuffer', + 'setbit', + 'setex', + 'setnx', + 'setrange', + 'shutdown', + 'sinter', + 'sinterBuffer', + 'sintercard', + 'sinterstore', + 'sismember', + 'slaveof', + 'slowlog', + 'smembers', + 'smembersBuffer', + 'smismember', + 'smove', + 'sort', + 'sort_ro', + 'spop', + 'spopBuffer', + 'spublish', + 'srandmember', + 'srandmemberBuffer', + 'srem', + 'sscan', + 'sscanBuffer', + 'sscanBufferStream', + 'sscanStream', + 'strlen', + 'substr', + 'sunion', + 'sunionBuffer', + 'sunionstore', + 'swapdb', + 'sync', + 'time', + 'touch', + 'ttl', + 'type', + 'unlink', + 'unwatch', + 'wait', + 'watch', + 'xack', + 'xadd', + 'xaddBuffer', + 'xautoclaim', + 'xclaim', + 'xdel', + 'xgroup', + 'xinfo', + 'xlen', + 'xpending', + 'xrange', + 'xrangeBuffer', + 'xread', + 'xreadBuffer', + 'xreadgroup', + 'xrevrange', + 'xrevrangeBuffer', + 'xsetid', + 'xtrim', + 'zadd', + 'zaddBuffer', + 'zcard', + 'zcount', + 'zdiff', + 'zdiffBuffer', + 'zdiffstore', + 'zincrby', + 'zincrbyBuffer', + 'zinter', + 'zinterBuffer', + 'zintercard', + 'zinterstore', + 'zlexcount', + 'zmpop', + 'zmscore', + 'zmscoreBuffer', + 'zpopmax', + 'zpopmaxBuffer', + 'zpopmin', + 'zpopminBuffer', + 'zrandmember', + 'zrandmemberBuffer', + 'zrange', + 'zrangeBuffer', + 'zrangebylex', + 'zrangebylexBuffer', + 'zrangebyscore', + 'zrangebyscoreBuffer', + 'zrangestore', + 'zrank', + 'zrem', + 'zremrangebylex', + 'zremrangebyrank', + 'zremrangebyscore', + 'zrevrange', + 'zrevrangeBuffer', + 'zrevrangebylex', + 'zrevrangebylexBuffer', + 'zrevrangebyscore', + 'zrevrangebyscoreBuffer', + 'zrevrank', + 'zscan', + 'zscanBuffer', + 'zscanBufferStream', + 'zscanStream', + 'zscore', + 'zscoreBuffer', + 'zunion', + 'zunionBuffer', + 'zunionstore', + 'end', + 'monitor', + 'scanBufferStream', + 'scanStream', +] satisfies (keyof Redis)[] diff --git a/src/connections/redis_cluster_connection.ts b/src/connections/redis_cluster_connection.ts index 60c2699..7187240 100644 --- a/src/connections/redis_cluster_connection.ts +++ b/src/connections/redis_cluster_connection.ts @@ -10,9 +10,9 @@ import Redis, { type Cluster, type NodeRole } from 'ioredis' import debug from '../debug.js' -import { ioMethods } from './io_methods.js' +import { baseMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' -import type { IORedisCommands, RedisClusterConnectionConfig } from '../types/main.js' +import type { IORedisBaseCommands, RedisClusterConnectionConfig } from '../types/main.js' /** * Redis cluster connection exposes the API to run Redis commands using `ioredis` as the @@ -22,6 +22,10 @@ import type { IORedisCommands, RedisClusterConnectionConfig } from '../types/mai export class RedisClusterConnection extends AbstractConnection { #config: RedisClusterConnectionConfig + get slots() { + return this.ioConnection.slots + } + constructor(connectionName: string, config: RedisClusterConnectionConfig) { debug('creating cluster connection %s: %O', connectionName, config) super(connectionName) @@ -59,8 +63,8 @@ export class RedisClusterConnection extends AbstractConnection { * Adding IORedis methods dynamically on the RedisClusterConnection * class and also extending its TypeScript types */ -export interface RedisClusterConnection extends IORedisCommands {} -ioMethods.forEach((method) => { +export interface RedisClusterConnection extends IORedisBaseCommands {} +baseMethods.forEach((method) => { ;(RedisClusterConnection.prototype as any)[method] = function redisConnectionProxyFn( ...args: any[] ) { diff --git a/src/connections/redis_connection.ts b/src/connections/redis_connection.ts index 3bf534f..8c3337f 100644 --- a/src/connections/redis_connection.ts +++ b/src/connections/redis_connection.ts @@ -10,9 +10,9 @@ import { Redis, RedisOptions } from 'ioredis' import debug from '../debug.js' -import { ioMethods } from './io_methods.js' +import { redisMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' -import { IORedisCommands, RedisConnectionConfig } from '../types/main.js' +import { IORedisConnectionCommands, RedisConnectionConfig } from '../types/main.js' /** * Redis connection exposes the API to run Redis commands using `ioredis` as the @@ -23,6 +23,21 @@ import { IORedisCommands, RedisConnectionConfig } from '../types/main.js' export class RedisConnection extends AbstractConnection { #config: RedisOptions + /** + * Returns the connection mode + */ + get mode() { + return this.ioConnection.mode + } + + /** + * Returns the connection mode for the subscriber + * connection + */ + get subscribeMode() { + return this.ioSubscriberConnection?.mode + } + constructor(connectionName: string, config: RedisConnectionConfig) { debug('creating connection %s: %O', connectionName, config) super(connectionName) @@ -57,8 +72,8 @@ export class RedisConnection extends AbstractConnection { * Adding IORedis methods dynamically on the RedisConnection * class and also extending its TypeScript types */ -export interface RedisConnection extends IORedisCommands {} -ioMethods.forEach((method) => { +export interface RedisConnection extends IORedisConnectionCommands {} +redisMethods.forEach((method) => { ;(RedisConnection.prototype as any)[method] = function redisConnectionProxyFn(...args: any[]) { return this.ioConnection[method](...args) } diff --git a/src/redis_manager.ts b/src/redis_manager.ts index 6f0306d..a6b7a0d 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -10,9 +10,10 @@ import { RuntimeException } from '@poppinss/utils' import debug from './debug.js' +import { baseMethods } from './connections/io_methods.js' import RedisConnection from './connections/redis_connection.js' import RedisClusterConnection from './connections/redis_cluster_connection.js' -import type { GetConnectionType, RedisConnectionsList } from './types/main.js' +import type { GetConnectionType, IORedisBaseCommands, RedisConnectionsList } from './types/main.js' /** * Redis Manager exposes the API to manage multiple redis connections @@ -20,7 +21,7 @@ import type { GetConnectionType, RedisConnectionsList } from './types/main.js' * * All connections are long-lived until they are closed explictly */ -export default class RedisManager { +class RedisManager { /** * User provided config */ @@ -137,3 +138,12 @@ export default class RedisManager await Promise.all(Object.keys(this.activeConnections).map((name) => this.disconnect(name))) } } + +interface RedisManager extends IORedisBaseCommands {} +baseMethods.forEach((method) => { + ;(RedisManager.prototype as any)[method] = function redisConnectionProxyFn(...args: any[]) { + return this.connection()[method](...args) + } +}) + +export default RedisManager diff --git a/src/types/main.ts b/src/types/main.ts index 8bef6be..17b5bd5 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -7,10 +7,10 @@ * file that was distributed with this source code. */ -import type { EventEmitter } from 'node:events' -import type { Redis as IoRedis, RedisOptions, ClusterOptions } from 'ioredis' +import type { Redis, Cluster, RedisOptions, ClusterOptions } from 'ioredis' import type RedisManager from '../redis_manager.js' +import type { baseMethods, redisMethods } from '../connections/io_methods.js' import type RedisConnection from '../connections/redis_connection.js' import type RedisClusterConnection from '../connections/redis_cluster_connection.js' @@ -37,22 +37,12 @@ export type HealthReportNode = { * List of commands on the IORedis. We omit their internal events and pub/sub * handlers, since we have our own. */ -export type IORedisCommands = Omit< - IoRedis, - | 'Promise' - | 'status' - | 'connect' - | 'disconnect' - | 'duplicate' - | 'subscribe' - | 'unsubscribe' - | 'psubscribe' - | 'punsubscribe' - | 'quit' - | 'publish' - | 'defineCommand' - | keyof EventEmitter -> +export type IORedisBaseCommands = { + [K in (typeof baseMethods)[number]]: Cluster[K] +} +export type IORedisConnectionCommands = { + [K in (typeof redisMethods)[number]]: Redis[K] +} /** * Configuration accepted by the redis connection. It is same diff --git a/tests/redis_manager.spec.ts b/tests/redis_manager.spec.ts index 2c8d411..f0480c8 100644 --- a/tests/redis_manager.spec.ts +++ b/tests/redis_manager.spec.ts @@ -45,6 +45,23 @@ test.group('Redis Manager', () => { redis.connection('foo') }).throws('Redis connection "foo" is not defined') + test('run redis commands from the manager', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + await redis.set('greeting', 'hello-world') + const greeting = await redis.get('greeting') + + assert.equal(greeting, 'hello-world') + + await redis.del('greeting') + await redis.quit('primary') + }) + test('run redis commands using default connection', async ({ assert }) => { const redis = new RedisManagerFactory({ connection: 'primary', From 50d3a26757a31ac42a1e27018102a866bc788787 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 12:43:48 +0530 Subject: [PATCH 38/71] chore(release): 8.0.0-6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33c0f0e..8eb3c98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-5", + "version": "8.0.0-6", "type": "module", "main": "build/index.js", "files": [ From 430160eff03249f6c5bae845a9582570e2615db6 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 15:17:38 +0530 Subject: [PATCH 39/71] feat: log connection errors and allow listening for new connections --- factories/redis_manager.ts | 13 ++++- package.json | 1 + providers/redis_provider.ts | 3 +- src/connections/io_methods.ts | 1 - src/redis_manager.ts | 53 ++++++++++++++++- tests/redis_manager.spec.ts | 106 ++++++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 5 deletions(-) diff --git a/factories/redis_manager.ts b/factories/redis_manager.ts index 7b5d34d..b5c2511 100644 --- a/factories/redis_manager.ts +++ b/factories/redis_manager.ts @@ -8,6 +8,7 @@ */ import RedisManager from '../src/redis_manager.js' +import { LoggerFactory } from '@adonisjs/core/factories/logger' import type { RedisClusterConnectionConfig, RedisConnectionConfig } from '../src/types/main.js' /** @@ -22,6 +23,8 @@ export class RedisManagerFactory< connections: ConnectionsList } + logs: string[] = [] + constructor(config: { connection: keyof ConnectionsList; connections: ConnectionsList }) { this.#config = config } @@ -30,6 +33,14 @@ export class RedisManagerFactory< * Create an instance of the redis manager */ create() { - return new RedisManager(this.#config) + return new RedisManager( + this.#config, + new LoggerFactory() + .merge({ + enabled: true, + }) + .pushLogsTo(this.logs) + .create() + ) } } diff --git a/package.json b/package.json index 8eb3c98..da53a52 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ }, "dependencies": { "@poppinss/utils": "6.5.0-3", + "emittery": "^1.0.1", "ioredis": "^5.3.2" }, "peerDependencies": { diff --git a/providers/redis_provider.ts b/providers/redis_provider.ts index 19c18f4..b84cf02 100644 --- a/providers/redis_provider.ts +++ b/providers/redis_provider.ts @@ -37,7 +37,8 @@ export default class RedisProvider { const { default: RedisManager } = await import('../src/redis_manager.js') const config = this.app.config.get('redis', {}) - return new RedisManager(config) + const logger = await this.app.container.make('logger') + return new RedisManager(config, logger) }) } diff --git a/src/connections/io_methods.ts b/src/connections/io_methods.ts index 7000a44..e01c51c 100644 --- a/src/connections/io_methods.ts +++ b/src/connections/io_methods.ts @@ -54,7 +54,6 @@ export const baseMethods = [ 'copy', 'createBuiltinCommand', 'dbsize', - 'debug', 'decr', 'decrby', 'del', diff --git a/src/redis_manager.ts b/src/redis_manager.ts index a6b7a0d..ca3a947 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -7,7 +7,9 @@ * file that was distributed with this source code. */ +import Emittery from 'emittery' import { RuntimeException } from '@poppinss/utils' +import type { Logger } from '@adonisjs/core/logger' import debug from './debug.js' import { baseMethods } from './connections/io_methods.js' @@ -21,7 +23,23 @@ import type { GetConnectionType, IORedisBaseCommands, RedisConnectionsList } fro * * All connections are long-lived until they are closed explictly */ -class RedisManager { +class RedisManager extends Emittery<{ + connection: RedisConnection | RedisClusterConnection +}> { + #logger: Logger + + /** + * Should we log redis errors or not + */ + #shouldLogRedisErrors: boolean = true + + /** + * The default error reporter we use to log redis errors + */ + #errorReporter = function logRedisError(this: RedisManager, error: any) { + this.#logger.fatal({ err: error }, 'Redis connection failure') + }.bind(this) + /** * User provided config */ @@ -45,8 +63,26 @@ class RedisManager { return Object.keys(this.activeConnections).length } - constructor(config: { connection: keyof ConnectionsList; connections: ConnectionsList }) { + constructor( + config: { connection: keyof ConnectionsList; connections: ConnectionsList }, + logger: Logger + ) { + super() this.#config = config + this.#logger = logger + } + + /** + * Disable error logging of redis connection errors. You must + * handle the errors manually, otheriwse the app will crash + */ + doNotLogErrors() { + this.#shouldLogRedisErrors = false + Object.keys(this.activeConnections).forEach((name) => { + debug('removing error reporter from %s connection', name) + this.activeConnections[name]?.removeListener('error', this.#errorReporter) + }) + return this } /** @@ -83,6 +119,19 @@ class RedisManager { ? new RedisClusterConnection(name as string, config) : new RedisConnection(name as string, config) + /** + * Notify about a new connection + */ + this.emit('connection', connection) + + /** + * Log errors when not disabled by the user + */ + if (this.#shouldLogRedisErrors) { + debug('attaching error reporter to log connection errors') + connection.on('error', this.#errorReporter) + } + /** * Remove connection from the list of tracked connections */ diff --git a/tests/redis_manager.spec.ts b/tests/redis_manager.spec.ts index f0480c8..d4a67d0 100644 --- a/tests/redis_manager.spec.ts +++ b/tests/redis_manager.spec.ts @@ -10,6 +10,7 @@ import { pEvent } from 'p-event' import { test } from '@japa/runner' +import type { Connection } from '../src/types/main.js' import { RedisManagerFactory } from '../factories/redis_manager.js' import RedisConnection from '../src/connections/redis_connection.js' import RedisClusterConnection from '../src/connections/redis_cluster_connection.js' @@ -244,4 +245,109 @@ test.group('Redis Manager', () => { expectTypeOf(redis.connection('cluster')).toEqualTypeOf() expectTypeOf(redis.connection('primary')).toEqualTypeOf() }) + + test('notify listener about a new connection', async ({ assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + + const [connection] = await Promise.all([ + pEvent<'connection', Connection>(redis, 'connection'), + redis.connection(), + ]) + + assert.strictEqual(connection, redis.connection()) + }) + + test('log errors using the logger', async ({ assert }) => { + const manager = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: 4444, + retryStrategy() { + return null + }, + }, + }, + }) + + const redis = manager.create() + + /** + * pEvent throws an exception when the error event is emitted. We are + * supressing that, because our error reporter should handle + * it + */ + await pEvent(redis.connection(), 'end', { rejectionEvents: [] }) + + const errorLog = JSON.parse(manager.logs[0]) + assert.equal(errorLog.level, 60) + assert.equal(errorLog.err.message, 'connect ECONNREFUSED 127.0.0.1:4444') + }) + + test('disable error logging', async ({ assert }) => { + const manager = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: 4444, + retryStrategy() { + return null + }, + }, + }, + }) + + const redis = manager.create() + redis.doNotLogErrors() + + redis.on('connection', (connection) => { + connection.on('error', () => {}) + }) + + /** + * pEvent throws an exception when the error event is emitted. We are + * supressing that, because our error reporter should handle + * it + */ + await pEvent(redis.connection(), 'end', { rejectionEvents: [] }) + assert.lengthOf(manager.logs, 0) + }) + + test('disable error logging for an existing connection', async ({ assert }) => { + const manager = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { + host: process.env.REDIS_HOST, + port: 4444, + retryStrategy() { + return null + }, + }, + }, + }) + + const redis = manager.create() + redis.on('connection', (connection) => { + connection.on('error', () => {}) + }) + + /** + * pEvent throws an exception when the error event is emitted. We are + * supressing that, because our error reporter should handle + * it + */ + await Promise.all([ + pEvent(redis.connection(), 'end', { rejectionEvents: [] }), + redis.doNotLogErrors(), + ]) + assert.lengthOf(manager.logs, 0) + }) }) From 187a6cf11ea31b11a3e2152f0e4739963f4eb413 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 15:24:18 +0530 Subject: [PATCH 40/71] refactor: rename connection in stub --- configure.ts | 1 - stubs/config/redis.stub | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/configure.ts b/configure.ts index 5cf8327..c2a080c 100644 --- a/configure.ts +++ b/configure.ts @@ -22,7 +22,6 @@ export async function configure(command: Configure) { * Add environment variables */ await command.defineEnvVariables({ - REDIS_CONNECTION: 'local', REDIS_HOST: '127.0.0.1', REDIS_PORT: '6379', REDIS_PASSWORD: '', diff --git a/stubs/config/redis.stub b/stubs/config/redis.stub index 4bce2a2..398529a 100644 --- a/stubs/config/redis.stub +++ b/stubs/config/redis.stub @@ -5,7 +5,7 @@ import env from '#start/env' import { defineConfig } from '@adonisjs/redis' const redisConfig = defineConfig({ - connection: env.get('REDIS_CONNECTION'), + connection: 'main', connections: { /* @@ -18,7 +18,7 @@ const redisConfig = defineConfig({ | redis driver. | */ - local: { + main: { host: env.get('REDIS_HOST'), port: env.get('REDIS_PORT'), password: env.get('REDIS_PASSWORD', ''), From 5649489d559a1aefbd7173f5ba0615df9a22b625 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 15:29:08 +0530 Subject: [PATCH 41/71] refactor: define retry strategy inside config --- stubs/config/redis.stub | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stubs/config/redis.stub b/stubs/config/redis.stub index 398529a..26d0025 100644 --- a/stubs/config/redis.stub +++ b/stubs/config/redis.stub @@ -24,6 +24,9 @@ const redisConfig = defineConfig({ password: env.get('REDIS_PASSWORD', ''), db: 0, keyPrefix: '', + retryStrategy(times) { + return times > 10 ? null : times * 50 + }, }, }, }) From a3f6d3fbd476cd3a9052c6e5ce775ffe7df07e2a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 17:47:55 +0530 Subject: [PATCH 42/71] feat: add pub/sub methods to redis_manager --- src/connections/abstract_connection.ts | 38 ++++----- src/redis_manager.ts | 57 +++++++++++++- tests/configure.spec.ts | 2 - tests/redis_manager.spec.ts | 102 +++++++++++++++++++++++++ tests/redis_provider.spec.ts | 20 +++-- 5 files changed, 191 insertions(+), 28 deletions(-) diff --git a/src/connections/abstract_connection.ts b/src/connections/abstract_connection.ts index 4eafa73..5be3bc3 100644 --- a/src/connections/abstract_connection.ts +++ b/src/connections/abstract_connection.ts @@ -320,6 +320,25 @@ export abstract class AbstractConnection extends Even return this.ioSubscriberConnection!.punsubscribe(pattern) } + /** + * Publish the pub/sub message + */ + publish( + channel: string, + message: string, + callback: (error: Error | null | undefined, count: number | undefined) => void + ): void + publish(channel: string, message: string): Promise + publish( + channel: string, + message: string, + callback?: (error: Error | null | undefined, count: number | undefined) => void + ) { + return callback + ? this.ioConnection.publish(channel, message, callback) + : this.ioConnection.publish(channel, message) + } + /** * Returns report for the connection */ @@ -381,25 +400,6 @@ export abstract class AbstractConnection extends Even } } - /** - * Publish the pub/sub message - */ - publish( - channel: string, - message: string, - callback: (error: Error | null | undefined, count: number | undefined) => void - ): void - publish(channel: string, message: string): Promise - publish( - channel: string, - message: string, - callback?: (error: Error | null | undefined, count: number | undefined) => void - ) { - return callback - ? this.ioConnection.publish(channel, message, callback) - : this.ioConnection.publish(channel, message) - } - /** * Define a custom command using LUA script. You can run the * registered command using the "runCommand" method. diff --git a/src/redis_manager.ts b/src/redis_manager.ts index ca3a947..f55202e 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -15,7 +15,13 @@ import debug from './debug.js' import { baseMethods } from './connections/io_methods.js' import RedisConnection from './connections/redis_connection.js' import RedisClusterConnection from './connections/redis_cluster_connection.js' -import type { GetConnectionType, IORedisBaseCommands, RedisConnectionsList } from './types/main.js' +import type { + GetConnectionType, + IORedisBaseCommands, + PubSubChannelHandler, + PubSubPatternHandler, + RedisConnectionsList, +} from './types/main.js' /** * Redis Manager exposes the API to manage multiple redis connections @@ -130,6 +136,7 @@ class RedisManager extends Emitter if (this.#shouldLogRedisErrors) { debug('attaching error reporter to log connection errors') connection.on('error', this.#errorReporter) + connection.on('subscriber:error', this.#errorReporter) } /** @@ -147,6 +154,54 @@ class RedisManager extends Emitter return connection as GetConnectionType } + /** + * Subscribe to a given channel to receive Redis pub/sub events. A + * new subscriber connection will be created/managed automatically. + */ + subscribe(channel: string, handler: PubSubChannelHandler): void { + return this.connection().subscribe(channel, handler) + } + + /** + * Unsubscribe from a channel + */ + unsubscribe(channel: string) { + return this.connection().unsubscribe(channel) + } + + /** + * Make redis subscription for a pattern + */ + psubscribe(pattern: string, handler: PubSubPatternHandler): void { + return this.connection().psubscribe(pattern, handler) + } + + /** + * Unsubscribe from a given pattern + */ + punsubscribe(pattern: string) { + return this.connection().punsubscribe(pattern) + } + + /** + * Publish the pub/sub message + */ + publish( + channel: string, + message: string, + callback: (error: Error | null | undefined, count: number | undefined) => void + ): void + publish(channel: string, message: string): Promise + publish( + channel: string, + message: string, + callback?: (error: Error | null | undefined, count: number | undefined) => void + ) { + return callback + ? this.connection().publish(channel, message, callback) + : this.connection().publish(channel, message) + } + /** * Quit a named connection or the default connection when no * name is defined. diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index a1b69c4..4eeb473 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -72,9 +72,7 @@ test.group('Configure', (group) => { await fs.create('.env', '') await command.exec() - await assert.fileContains('.env', 'REDIS_CONNECTION=local') await assert.fileContains('.env', 'REDIS_HOST=127.0.0.1') - await assert.fileContains('.env', 'REDIS_PORT=6379') await assert.fileContains('.env', 'REDIS_PASSWORD=') }) diff --git a/tests/redis_manager.spec.ts b/tests/redis_manager.spec.ts index d4a67d0..3728045 100644 --- a/tests/redis_manager.spec.ts +++ b/tests/redis_manager.spec.ts @@ -350,4 +350,106 @@ test.group('Redis Manager', () => { ]) assert.lengthOf(manager.logs, 0) }) + + test('subscribe to a channel using manager', async ({ assert, cleanup }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + cleanup(() => redis.quitAll()) + + const connection = redis.connection() + await pEvent(connection, 'ready') + + const [message] = await Promise.all([ + new Promise((resolve) => { + redis.subscribe('new:user', resolve) + }), + pEvent(connection, 'subscription:ready').then(() => { + redis.publish('new:user', JSON.stringify({ username: 'virk' })) + }), + ]) + + assert.equal(message, JSON.stringify({ username: 'virk' })) + }) + + test('subscribe to a pattern using manager', async ({ assert, cleanup }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + cleanup(() => redis.quitAll()) + + const connection = redis.connection() + await pEvent(connection, 'ready') + + const [message] = await Promise.all([ + new Promise((resolve) => { + redis.psubscribe('user:*', (_, data) => resolve(data)) + }), + pEvent(connection, 'psubscription:ready').then(() => { + redis.publish('user:add', JSON.stringify({ username: 'virk' })) + }), + ]) + + assert.equal(message, JSON.stringify({ username: 'virk' })) + }) + + test('unsubscribe using manager', async ({ cleanup }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + cleanup(() => redis.quitAll()) + + const connection = redis.connection() + await pEvent(connection, 'ready') + + await Promise.all([ + new Promise((resolve, reject) => { + redis.subscribe('new:user', () => reject('Not expected to be called')) + setTimeout(() => { + resolve() + }, 1500) + }), + pEvent(connection, 'subscription:ready').then(() => { + return redis.unsubscribe('new:user').then(() => { + redis.publish('new:user', JSON.stringify({ username: 'virk' })) + }) + }), + ]) + }).timeout(4000) + + test('punsubscribe using manager', async ({ cleanup }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + cleanup(() => redis.quitAll()) + + const connection = redis.connection() + await pEvent(connection, 'ready') + + await Promise.all([ + new Promise((resolve, reject) => { + redis.psubscribe('user:*', () => reject('Not expected to be called')) + setTimeout(() => { + resolve() + }, 1500) + }), + pEvent(connection, 'psubscription:ready').then(() => { + return redis.punsubscribe('user:*').then(() => { + redis.publish('new:user', JSON.stringify({ username: 'virk' })) + }) + }), + ]) + }).timeout(4000) }) diff --git a/tests/redis_provider.spec.ts b/tests/redis_provider.spec.ts index 49078a2..951ac41 100644 --- a/tests/redis_provider.spec.ts +++ b/tests/redis_provider.spec.ts @@ -15,18 +15,25 @@ import { defineConfig } from '../index.js' import RedisManager from '../src/redis_manager.js' const BASE_URL = new URL('./tmp/', import.meta.url) +const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + return import(filePath) +} test.group('Redis Provider', () => { test('register redis provider', async ({ assert }) => { const ignitor = new IgnitorFactory() .merge({ rcFileContents: { - providers: ['./providers/redis_provider.js'], + providers: ['../../providers/redis_provider.js'], }, }) .withCoreConfig() + .withCoreProviders() .create(BASE_URL, { - importer: (filePath) => import(new URL(filePath, new URL('../', import.meta.url)).href), + importer: IMPORTER, }) const app = ignitor.createApp('web') @@ -41,12 +48,12 @@ test.group('Redis Provider', () => { .withCoreConfig() .merge({ rcFileContents: { - providers: ['../providers/redis_provider.js'], + providers: ['../../providers/redis_provider.js'], }, }) .withCoreProviders() .create(BASE_URL, { - importer: (filePath) => import(filePath), + importer: IMPORTER, }) const app = ignitor.createApp('repl') @@ -62,10 +69,11 @@ test.group('Redis Provider', () => { const ignitor = new IgnitorFactory() .merge({ rcFileContents: { - providers: ['./providers/redis_provider.js'], + providers: ['../../providers/redis_provider.js'], }, }) .withCoreConfig() + .withCoreProviders() .merge({ config: { redis: defineConfig({ @@ -80,7 +88,7 @@ test.group('Redis Provider', () => { }, }) .create(BASE_URL, { - importer: (filePath) => import(new URL(filePath, new URL('../', import.meta.url)).href), + importer: IMPORTER, }) const app = ignitor.createApp('web') From c2e5414dd012e248a7d476a0ae3b01bf8cde585f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 18:01:48 +0530 Subject: [PATCH 43/71] feat: add defineCommand and runCommand methods on redis service --- src/connections/redis_cluster_connection.ts | 21 +++---- src/redis_manager.ts | 62 ++++++++++++++++++++- tests/redis_cluster_connection.spec.ts | 36 ++++++------ tests/redis_manager.spec.ts | 50 +++++++++++++++++ 4 files changed, 141 insertions(+), 28 deletions(-) diff --git a/src/connections/redis_cluster_connection.ts b/src/connections/redis_cluster_connection.ts index 7187240..2940ba1 100644 --- a/src/connections/redis_cluster_connection.ts +++ b/src/connections/redis_cluster_connection.ts @@ -20,21 +20,25 @@ import type { IORedisBaseCommands, RedisClusterConnectionConfig } from '../types * pub/sub connections by hand, since it handles that internally by itself. */ export class RedisClusterConnection extends AbstractConnection { - #config: RedisClusterConnectionConfig + #hosts: RedisClusterConnectionConfig['clusters'] + #config: RedisClusterConnectionConfig['clusterOptions'] get slots() { return this.ioConnection.slots } - constructor(connectionName: string, config: RedisClusterConnectionConfig) { + constructor( + connectionName: string, + hosts: RedisClusterConnectionConfig['clusters'], + config: RedisClusterConnectionConfig['clusterOptions'] + ) { debug('creating cluster connection %s: %O', connectionName, config) super(connectionName) + this.#hosts = hosts this.#config = config - this.ioConnection = new Redis.Cluster( - this.#config.clusters as any[], - this.#config.clusterOptions - ) + + this.ioConnection = new Redis.Cluster(this.#hosts as any[], this.#config) this.monitorConnection() } @@ -44,10 +48,7 @@ export class RedisClusterConnection extends AbstractConnection { */ protected makeSubscriberConnection() { debug('creating subscriber connection') - this.ioSubscriberConnection = new Redis.Cluster( - this.#config.clusters as [], - this.#config.clusterOptions - ) + this.ioSubscriberConnection = new Redis.Cluster(this.#hosts as any[], this.#config) this.monitorSubscriberConnection() } diff --git a/src/redis_manager.ts b/src/redis_manager.ts index f55202e..400912a 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -22,6 +22,7 @@ import type { PubSubPatternHandler, RedisConnectionsList, } from './types/main.js' +import { ClusterOptions, RedisOptions } from 'ioredis' /** * Redis Manager exposes the API to manage multiple redis connections @@ -32,6 +33,18 @@ import type { class RedisManager extends Emittery<{ connection: RedisConnection | RedisClusterConnection }> { + /** + * Lua scripts to apply to all the connections + */ + #scripts: Record< + string, + { + lua: string + numberOfKeys?: number + readOnly?: boolean + } + > = {} + #logger: Logger /** @@ -78,6 +91,14 @@ class RedisManager extends Emitter this.#logger = logger } + /** + * Merging manager scripts with the connection config + */ + #mergeScripts(config: Config): Config { + config.scripts = Object.assign({}, config.scripts, this.#scripts) + return config + } + /** * Disable error logging of redis connection errors. You must * handle the errors manually, otheriwse the app will crash @@ -122,8 +143,12 @@ class RedisManager extends Emitter debug('creating new connection %s', name) const connection = 'clusters' in config - ? new RedisClusterConnection(name as string, config) - : new RedisConnection(name as string, config) + ? new RedisClusterConnection( + name as string, + config.clusters, + this.#mergeScripts(config.clusterOptions || {}) + ) + : new RedisConnection(name as string, this.#mergeScripts(config)) /** * Notify about a new connection @@ -202,6 +227,39 @@ class RedisManager extends Emitter : this.connection().publish(channel, message) } + /** + * Define a custom command using LUA script. You can run the + * registered command using the "runCommand" method. + */ + defineCommand( + name: string, + definition: { + lua: string + numberOfKeys?: number + readOnly?: boolean + } + ): this { + /** + * Apply command on existing connections + */ + Object.keys(this.activeConnections).forEach((connectionName) => { + this.activeConnections[connectionName]?.defineCommand(name, definition) + }) + + /** + * Store reference to scripts for new commands + */ + this.#scripts[name] = definition + return this + } + + /** + * Run a pre registered command on the default command + */ + runCommand(command: string, ...args: any[]): any { + return this.connection().runCommand(command, ...args) + } + /** * Quit a named connection or the default connection when no * name is defined. diff --git a/tests/redis_cluster_connection.spec.ts b/tests/redis_cluster_connection.spec.ts index a715266..057a745 100644 --- a/tests/redis_cluster_connection.spec.ts +++ b/tests/redis_cluster_connection.spec.ts @@ -17,7 +17,7 @@ const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { test.group('Redis cluster factory', () => { test('emit ready when connected to redis server', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(() => connection.quit()) await pEvent(connection, 'ready') @@ -25,7 +25,7 @@ test.group('Redis cluster factory', () => { }) test('emit connect event before the ready event', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(() => connection.quit()) await pEvent(connection, 'connect') @@ -34,14 +34,14 @@ test.group('Redis cluster factory', () => { }) test('emit node:added event', async ({ cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(() => connection.quit()) await pEvent(connection, 'node:added') }) test('execute redis commands', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(async () => { await connection.del('greeting') await connection.quit() @@ -53,7 +53,7 @@ test.group('Redis cluster factory', () => { }) test('clean event listeners on quit', async ({ assert }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) await pEvent(connection, 'ready') await Promise.all([pEvent(connection, 'end'), connection.quit()]) @@ -67,7 +67,7 @@ test.group('Redis cluster factory', () => { }) test('clean event listeners on disconnect', async ({ assert }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) await pEvent(connection, 'ready') await Promise.all([pEvent(connection, 'end'), connection.disconnect()]) @@ -81,9 +81,11 @@ test.group('Redis cluster factory', () => { }) test('emit node:error when unable to connect', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }) + const connection = new RedisClusterConnection( + 'main', + [{ host: process.env.REDIS_HOST!, port: 5000 }], + {} + ) cleanup(() => connection.quit()) connection.on('error', () => {}) @@ -92,7 +94,7 @@ test.group('Redis cluster factory', () => { }) test('access cluster nodes', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(() => connection.quit()) await pEvent(connection, 'ready') @@ -100,7 +102,7 @@ test.group('Redis cluster factory', () => { }) test('get report for connection in ready state', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(() => connection.quit()) await pEvent(connection, 'ready') @@ -112,9 +114,11 @@ test.group('Redis cluster factory', () => { }) test('get report for errored connection', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { - clusters: [{ host: process.env.REDIS_HOST!, port: 5000 }], - }) + const connection = new RedisClusterConnection( + 'main', + [{ host: process.env.REDIS_HOST!, port: 5000 }], + {} + ) cleanup(() => connection.quit()) @@ -127,7 +131,7 @@ test.group('Redis cluster factory', () => { }) test('execute redis commands using lua scripts', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(async () => { await connection.del('greeting') await connection.quit() @@ -149,7 +153,7 @@ test.group('Redis cluster factory', () => { }) test('subscribe to a channel and listen for messages', async ({ assert, cleanup }) => { - const connection = new RedisClusterConnection('main', { clusters: nodes }) + const connection = new RedisClusterConnection('main', nodes, {}) cleanup(() => connection.quit()) await pEvent(connection, 'ready') diff --git a/tests/redis_manager.spec.ts b/tests/redis_manager.spec.ts index 3728045..c079380 100644 --- a/tests/redis_manager.spec.ts +++ b/tests/redis_manager.spec.ts @@ -452,4 +452,54 @@ test.group('Redis Manager', () => { }), ]) }).timeout(4000) + + test('apply defined commands to connections', async ({ cleanup, assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + cleanup(() => redis.quitAll()) + + redis.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + redis.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await redis.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await redis.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + }) + + test('apply defined commands on existing connections', async ({ cleanup, assert }) => { + const redis = new RedisManagerFactory({ + connection: 'primary', + connections: { + primary: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, + }, + }).create() + cleanup(() => redis.quitAll()) + + const connection = redis.connection() + + redis.defineCommand('defineValue', { + numberOfKeys: 1, + lua: `redis.call('set', KEYS[1], ARGV[1])`, + }) + + redis.defineCommand('readValue', { + numberOfKeys: 1, + lua: `return redis.call('get', KEYS[1])`, + }) + + await connection.runCommand('defineValue', 'greeting', 'hello world') + const greeting = await connection.runCommand('readValue', 'greeting') + assert.equal(greeting, 'hello world') + }) }) From 0845d186c12a4c9fb049c3ffe7d9d009d8b85013 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 18:21:44 +0530 Subject: [PATCH 44/71] feat: add reference to the IORedis Command property --- src/redis_manager.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/redis_manager.ts b/src/redis_manager.ts index 400912a..1569a58 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -10,6 +10,7 @@ import Emittery from 'emittery' import { RuntimeException } from '@poppinss/utils' import type { Logger } from '@adonisjs/core/logger' +import { type ClusterOptions, type RedisOptions, Redis } from 'ioredis' import debug from './debug.js' import { baseMethods } from './connections/io_methods.js' @@ -22,7 +23,6 @@ import type { PubSubPatternHandler, RedisConnectionsList, } from './types/main.js' -import { ClusterOptions, RedisOptions } from 'ioredis' /** * Redis Manager exposes the API to manage multiple redis connections @@ -67,6 +67,11 @@ class RedisManager extends Emitter connections: ConnectionsList } + /** + * Reference to "import('ioredis').Redis.Command" + */ + Command = Redis.Command + /** * A copy of live connections. We avoid re-creating a new connection * everytime and re-use connections. From 4808770333061bf51d1861c2dee5cd3834d06556 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 21:20:15 +0530 Subject: [PATCH 45/71] refactor: use emittery and make events typed --- package.json | 1 - src/connections/abstract_connection.ts | 124 +++++++++++++------- src/connections/io_methods.ts | 1 - src/connections/redis_cluster_connection.ts | 11 +- src/connections/redis_connection.ts | 9 +- src/redis_manager.ts | 11 +- src/types/main.ts | 34 ++++++ tests/redis_cluster_connection.spec.ts | 6 +- tests/redis_connection.spec.ts | 13 +- tests/redis_manager.spec.ts | 34 +----- tests_helpers/main.ts | 32 +++++ 11 files changed, 184 insertions(+), 92 deletions(-) create mode 100644 tests_helpers/main.ts diff --git a/package.json b/package.json index da53a52..5931107 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "p-event": "^6.0.0", "prettier": "^3.0.0", "ts-node": "^10.9.1", "typescript": "^5.1.6" diff --git a/src/connections/abstract_connection.ts b/src/connections/abstract_connection.ts index 5be3bc3..f3ab253 100644 --- a/src/connections/abstract_connection.ts +++ b/src/connections/abstract_connection.ts @@ -7,18 +7,27 @@ * file that was distributed with this source code. */ -import { EventEmitter } from 'node:events' +import Emittery from 'emittery' import type { Redis, Cluster } from 'ioredis' import { setTimeout } from 'node:timers/promises' import * as errors from '../errors.js' -import type { HealthReportNode, PubSubChannelHandler, PubSubPatternHandler } from '../types/main.js' +import type { + PubSubOptions, + HealthReportNode, + ConnectionEvents, + PubSubChannelHandler, + PubSubPatternHandler, +} from '../types/main.js' /** * Abstract factory implements the shared functionality required by Redis cluster * and the normal Redis connections. */ -export abstract class AbstractConnection extends EventEmitter { +export abstract class AbstractConnection< + T extends Redis | Cluster, + Events extends ConnectionEvents, +> extends Emittery { /** * Reference to the main ioRedis connection */ @@ -95,32 +104,38 @@ export abstract class AbstractConnection extends Even * things properly and also notify subscribers of this class */ protected monitorConnection() { - this.ioConnection.on('connect', () => this.emit('connect', this)) - this.ioConnection.on('wait', () => this.emit('wait', this)) + this.ioConnection.on('connect', () => this.emit('connect', { connection: this })) + this.ioConnection.on('wait', () => this.emit('wait', { connection: this })) this.ioConnection.on('ready', () => { /** * We must set the error to null when server is ready for accept * commands */ this.#lastError = null - this.emit('ready', this) + this.emit('ready', { connection: this }) }) this.ioConnection.on('error', (error: any) => { this.#lastError = error - this.emit('error', error, this) + this.emit('error', { error, connection: this }) }) - this.ioConnection.on('close', () => this.emit('close', this)) - this.ioConnection.on('reconnecting', () => this.emit('reconnecting', this)) + this.ioConnection.on('close', () => this.emit('close', { connection: this })) + this.ioConnection.on('reconnecting', (waitTime: number) => + this.emit('reconnecting', { connection: this, waitTime }) + ) /** * Cluster only events */ - this.ioConnection.on('+node', (node: Redis) => this.emit('node:added', this, node)) - this.ioConnection.on('-node', (node: Redis) => this.emit('node:removed', this, node)) + this.ioConnection.on('+node', (node: Redis) => + this.emit('node:added', { connection: this, node }) + ) + this.ioConnection.on('-node', (node: Redis) => + this.emit('node:removed', { connection: this, node }) + ) this.ioConnection.on('node error', (error: any, address: string) => { - this.emit('node:error', error, address, this) + this.emit('node:error', { error, address, connection: this }) }) /** @@ -128,17 +143,20 @@ export abstract class AbstractConnection extends Even */ this.ioConnection.on('end', async () => { this.ioConnection.removeAllListeners() - this.emit('end', this) - this.removeAllListeners('connect') - this.removeAllListeners('wait') - this.removeAllListeners('ready') - this.removeAllListeners('error') - this.removeAllListeners('close') - this.removeAllListeners('reconnecting') - this.removeAllListeners('node:added') - this.removeAllListeners('node:removed') - this.removeAllListeners('node:error') - this.removeAllListeners('end') + this.emit('end', { connection: this }).finally(() => { + this.clearListeners([ + 'connect', + 'wait', + 'ready', + 'error', + 'close', + 'reconnecting', + 'node:added', + 'node:error', + 'node:removed', + 'end', + ]) + }) }) } @@ -148,15 +166,20 @@ export abstract class AbstractConnection extends Even * this class. */ protected monitorSubscriberConnection() { - this.ioSubscriberConnection!.on('connect', () => this.emit('subscriber:connect', this)) - this.ioSubscriberConnection!.on('wait', () => this.emit('subscriber:wait', this)) - this.ioSubscriberConnection!.on('ready', () => this.emit('subscriber:ready', this)) + this.ioSubscriberConnection!.on('connect', () => + this.emit('subscriber:connect', { connection: this }) + ) + this.ioSubscriberConnection!.on('ready', () => + this.emit('subscriber:ready', { connection: this }) + ) this.ioSubscriberConnection!.on('error', (error: any) => { - this.emit('subscriber:error', error, this) + this.emit('subscriber:error', { error, connection: this }) }) - this.ioSubscriberConnection!.on('close', () => this.emit('subscriber:close', this)) - this.ioSubscriberConnection!.on('reconnecting', () => - this.emit('subscriber:reconnecting', this) + this.ioSubscriberConnection!.on('close', () => + this.emit('subscriber:close', { connection: this }) + ) + this.ioSubscriberConnection!.on('reconnecting', (waitTime: number) => + this.emit('subscriber:reconnecting', { connection: this, waitTime }) ) /** @@ -165,7 +188,7 @@ export abstract class AbstractConnection extends Even */ this.ioSubscriberConnection!.on('end', async () => { this.ioSubscriberConnection!.removeAllListeners() - this.emit('subscriber:end', this) + this.emit('subscriber:end', { connection: this }) /** * Cleanup subscriptions @@ -174,13 +197,14 @@ export abstract class AbstractConnection extends Even this.psubscriptions.clear() this.ioSubscriberConnection = undefined - this.removeAllListeners('subscriber:connect') - this.removeAllListeners('subscriber:wait') - this.removeAllListeners('subscriber:ready') - this.removeAllListeners('subscriber:error') - this.removeAllListeners('subscriber:close') - this.removeAllListeners('subscriber:reconnecting') - this.removeAllListeners('subscriber:end') + this.clearListeners([ + 'subscriber:connect', + 'subscriber:ready', + 'subscriber:error', + 'subscriber:close', + 'subscriber:reconnecting', + 'subscriber:end', + ]) }) } @@ -243,7 +267,7 @@ export abstract class AbstractConnection extends Even * Subscribe to a given channel to receive Redis pub/sub events. A * new subscriber connection will be created/managed automatically. */ - subscribe(channel: string, handler: PubSubChannelHandler): void { + subscribe(channel: string, handler: PubSubChannelHandler, options?: PubSubOptions): void { /** * Make the subscriber connection. The method results in a noop when * subscriber connection already exists. @@ -264,11 +288,17 @@ export abstract class AbstractConnection extends Even */ this.ioSubscriberConnection!.subscribe(channel) .then((count) => { - this.emit('subscription:ready', count, this) + if (options?.onSubscription) { + options?.onSubscription(count as number) + } + this.emit('subscription:ready', { count: count as number, connection: this }) this.subscriptions.set(channel, handler) }) .catch((error) => { - this.emit('subscription:error', error, this) + if (options?.onError) { + options?.onError(error) + } + this.emit('subscription:error', { error, connection: this }) }) } @@ -283,7 +313,7 @@ export abstract class AbstractConnection extends Even /** * Make redis subscription for a pattern */ - psubscribe(pattern: string, handler: PubSubPatternHandler): void { + psubscribe(pattern: string, handler: PubSubPatternHandler, options?: PubSubOptions): void { /** * Make the subscriber connection. The method results in a noop when * subscriber connection already exists. @@ -304,11 +334,17 @@ export abstract class AbstractConnection extends Even */ this.ioSubscriberConnection!.psubscribe(pattern) .then((count) => { - this.emit('psubscription:ready', count, this) + if (options?.onSubscription) { + options?.onSubscription(count as number) + } + this.emit('psubscription:ready', { count: count as number, connection: this }) this.psubscriptions.set(pattern, handler) }) .catch((error) => { - this.emit('psubscription:error', error, this) + if (options?.onError) { + options?.onError(error) + } + this.emit('psubscription:error', { error, connection: this }) }) } diff --git a/src/connections/io_methods.ts b/src/connections/io_methods.ts index e01c51c..793bea7 100644 --- a/src/connections/io_methods.ts +++ b/src/connections/io_methods.ts @@ -372,7 +372,6 @@ export const redisMethods = [ 'copy', 'createBuiltinCommand', 'dbsize', - 'debug', 'decr', 'decrby', 'del', diff --git a/src/connections/redis_cluster_connection.ts b/src/connections/redis_cluster_connection.ts index 2940ba1..1673acd 100644 --- a/src/connections/redis_cluster_connection.ts +++ b/src/connections/redis_cluster_connection.ts @@ -12,14 +12,21 @@ import Redis, { type Cluster, type NodeRole } from 'ioredis' import debug from '../debug.js' import { baseMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' -import type { IORedisBaseCommands, RedisClusterConnectionConfig } from '../types/main.js' +import type { + ConnectionEvents, + IORedisBaseCommands, + RedisClusterConnectionConfig, +} from '../types/main.js' /** * Redis cluster connection exposes the API to run Redis commands using `ioredis` as the * underlying client. The class abstracts the need of creating and managing multiple * pub/sub connections by hand, since it handles that internally by itself. */ -export class RedisClusterConnection extends AbstractConnection { +export class RedisClusterConnection extends AbstractConnection< + Cluster, + ConnectionEvents +> { #hosts: RedisClusterConnectionConfig['clusters'] #config: RedisClusterConnectionConfig['clusterOptions'] diff --git a/src/connections/redis_connection.ts b/src/connections/redis_connection.ts index 8c3337f..e64922f 100644 --- a/src/connections/redis_connection.ts +++ b/src/connections/redis_connection.ts @@ -12,7 +12,11 @@ import { Redis, RedisOptions } from 'ioredis' import debug from '../debug.js' import { redisMethods } from './io_methods.js' import { AbstractConnection } from './abstract_connection.js' -import { IORedisConnectionCommands, RedisConnectionConfig } from '../types/main.js' +import type { + ConnectionEvents, + RedisConnectionConfig, + IORedisConnectionCommands, +} from '../types/main.js' /** * Redis connection exposes the API to run Redis commands using `ioredis` as the @@ -20,7 +24,7 @@ import { IORedisConnectionCommands, RedisConnectionConfig } from '../types/main. * multiple pub/sub connections by hand, since it handles that internally * by itself. */ -export class RedisConnection extends AbstractConnection { +export class RedisConnection extends AbstractConnection> { #config: RedisOptions /** @@ -73,6 +77,7 @@ export class RedisConnection extends AbstractConnection { * class and also extending its TypeScript types */ export interface RedisConnection extends IORedisConnectionCommands {} + redisMethods.forEach((method) => { ;(RedisConnection.prototype as any)[method] = function redisConnectionProxyFn(...args: any[]) { return this.ioConnection[method](...args) diff --git a/src/redis_manager.ts b/src/redis_manager.ts index 1569a58..a6bcf5c 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -55,8 +55,11 @@ class RedisManager extends Emitter /** * The default error reporter we use to log redis errors */ - #errorReporter = function logRedisError(this: RedisManager, error: any) { - this.#logger.fatal({ err: error }, 'Redis connection failure') + #errorReporter = function logRedisError( + this: RedisManager, + data: { error: any } + ) { + this.#logger.fatal({ err: data.error }, 'Redis connection failure') }.bind(this) /** @@ -112,7 +115,7 @@ class RedisManager extends Emitter this.#shouldLogRedisErrors = false Object.keys(this.activeConnections).forEach((name) => { debug('removing error reporter from %s connection', name) - this.activeConnections[name]?.removeListener('error', this.#errorReporter) + this.activeConnections[name]?.off('error', this.#errorReporter) }) return this } @@ -172,7 +175,7 @@ class RedisManager extends Emitter /** * Remove connection from the list of tracked connections */ - connection.on('end', ($connection) => { + connection.on('end', ({ connection: $connection }) => { debug('%s connection closed. Removing from tracked connections list', name) delete this.activeConnections[$connection.connectionName] }) diff --git a/src/types/main.ts b/src/types/main.ts index 17b5bd5..813b2ad 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -23,6 +23,40 @@ export type PubSubPatternHandler = ( data: T ) => Promise | void +/** + * Options accepted during subscribe + */ +export type PubSubOptions = { + onError(error: any): void + onSubscription(count: number): void +} + +/** + * List of connection events + */ +export type ConnectionEvents = { + 'connect': { connection: T } + 'wait': { connection: T } + 'ready': { connection: T } + 'error': { error: any; connection: T } + 'close': { connection: T } + 'reconnecting': { connection: T; waitTime: number } + 'end': { connection: T } + 'subscriber:connect': { connection: T } + 'subscriber:ready': { connection: T } + 'subscriber:error': { error: any; connection: T } + 'subscriber:close': { connection: T } + 'subscriber:reconnecting': { connection: T; waitTime: number } + 'subscriber:end': { connection: T } + 'node:added': { connection: T; node: Redis } + 'node:removed': { connection: T; node: Redis } + 'node:error': { error: any; address: string; connection: T } + 'subscription:ready': { connection: T; count: number } + 'subscription:error': { connection: T; error: any } + 'psubscription:ready': { connection: T; count: number } + 'psubscription:error': { connection: T; error: any } +} + /** * Shape of the report node for the redis connection report */ diff --git a/tests/redis_cluster_connection.spec.ts b/tests/redis_cluster_connection.spec.ts index 057a745..95e2e3c 100644 --- a/tests/redis_cluster_connection.spec.ts +++ b/tests/redis_cluster_connection.spec.ts @@ -7,8 +7,8 @@ * file that was distributed with this source code. */ -import { pEvent } from 'p-event' import { test } from '@japa/runner' +import { pEvent } from '../tests_helpers/main.js' import RedisClusterConnection from '../src/connections/redis_cluster_connection.js' const nodes = process.env.REDIS_CLUSTER_PORTS!.split(',').map((port) => { @@ -89,8 +89,8 @@ test.group('Redis cluster factory', () => { cleanup(() => connection.quit()) connection.on('error', () => {}) - const [error] = await pEvent(connection, 'node:error', { multiArgs: true }) - assert.equal(error.message, 'Connection is closed.') + const response = await pEvent(connection, 'node:error') + assert.equal(response!.error.message, 'Connection is closed.') }) test('access cluster nodes', async ({ assert, cleanup }) => { diff --git a/tests/redis_connection.spec.ts b/tests/redis_connection.spec.ts index 8cb77ad..e5d3c64 100644 --- a/tests/redis_connection.spec.ts +++ b/tests/redis_connection.spec.ts @@ -7,8 +7,9 @@ * file that was distributed with this source code. */ -import { pEvent } from 'p-event' import { test } from '@japa/runner' + +import { pEvent } from '../tests_helpers/main.js' import RedisConnection from '../src/connections/redis_connection.js' test.group('Redis connection', () => { @@ -39,8 +40,8 @@ test.group('Redis connection', () => { const connection = new RedisConnection('main', { port: 4444 }) cleanup(() => connection.disconnect()) - const [error] = await pEvent(connection, 'error', { multiArgs: true }) - assert.equal(error.message, 'connect ECONNREFUSED 127.0.0.1:4444') + const response = await pEvent(connection, 'error') + assert.equal(response!.error.message, 'connect ECONNREFUSED 127.0.0.1:4444') }) test('cleanup listeners on quit', async ({ assert }) => { @@ -279,12 +280,12 @@ test.group('Redis connection', () => { test('emit error when unable to make subscriber connection', async ({ assert, cleanup }) => { const connection = new RedisConnection('main', { port: 4444 }) - await pEvent(connection, 'error', { multiArgs: true }) + await pEvent(connection, 'error') cleanup(() => connection.disconnect()) connection.subscribe('foo', () => {}) - const [error] = await pEvent(connection, 'subscriber:error', { multiArgs: true }) - assert.equal(error.message, 'connect ECONNREFUSED 127.0.0.1:4444') + const response = await pEvent(connection, 'subscriber:error') + assert.equal(response!.error.message, 'connect ECONNREFUSED 127.0.0.1:4444') }) test('cleanup subscribers listeners on quit', async ({ assert }) => { diff --git a/tests/redis_manager.spec.ts b/tests/redis_manager.spec.ts index c079380..b27a8e4 100644 --- a/tests/redis_manager.spec.ts +++ b/tests/redis_manager.spec.ts @@ -7,10 +7,9 @@ * file that was distributed with this source code. */ -import { pEvent } from 'p-event' import { test } from '@japa/runner' -import type { Connection } from '../src/types/main.js' +import { pEvent } from '../tests_helpers/main.js' import { RedisManagerFactory } from '../factories/redis_manager.js' import RedisConnection from '../src/connections/redis_connection.js' import RedisClusterConnection from '../src/connections/redis_cluster_connection.js' @@ -254,11 +253,7 @@ test.group('Redis Manager', () => { }, }).create() - const [connection] = await Promise.all([ - pEvent<'connection', Connection>(redis, 'connection'), - redis.connection(), - ]) - + const [connection] = await Promise.all([pEvent(redis, 'connection'), redis.connection()]) assert.strictEqual(connection, redis.connection()) }) @@ -277,13 +272,7 @@ test.group('Redis Manager', () => { }) const redis = manager.create() - - /** - * pEvent throws an exception when the error event is emitted. We are - * supressing that, because our error reporter should handle - * it - */ - await pEvent(redis.connection(), 'end', { rejectionEvents: [] }) + await pEvent(redis.connection(), 'end') const errorLog = JSON.parse(manager.logs[0]) assert.equal(errorLog.level, 60) @@ -311,12 +300,7 @@ test.group('Redis Manager', () => { connection.on('error', () => {}) }) - /** - * pEvent throws an exception when the error event is emitted. We are - * supressing that, because our error reporter should handle - * it - */ - await pEvent(redis.connection(), 'end', { rejectionEvents: [] }) + await pEvent(redis.connection(), 'end') assert.lengthOf(manager.logs, 0) }) @@ -339,15 +323,7 @@ test.group('Redis Manager', () => { connection.on('error', () => {}) }) - /** - * pEvent throws an exception when the error event is emitted. We are - * supressing that, because our error reporter should handle - * it - */ - await Promise.all([ - pEvent(redis.connection(), 'end', { rejectionEvents: [] }), - redis.doNotLogErrors(), - ]) + await Promise.all([pEvent(redis.connection(), 'end'), redis.doNotLogErrors()]) assert.lengthOf(manager.logs, 0) }) diff --git a/tests_helpers/main.ts b/tests_helpers/main.ts new file mode 100644 index 0000000..150c7a8 --- /dev/null +++ b/tests_helpers/main.ts @@ -0,0 +1,32 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import Emittery from 'emittery' + +/** + * Promisify an event + */ +export function pEvent( + emitter: Emittery, + event: K, + timeout: number = 500 +) { + return new Promise((resolve) => { + function handler(data: T[K]) { + emitter.off(event, handler) + resolve(data) + } + + setTimeout(() => { + emitter.off(event, handler) + resolve(null) + }, timeout) + emitter.on(event, handler) + }) +} From 43fa362f60b7915248984c3e1775fda2226523c0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 21:24:59 +0530 Subject: [PATCH 46/71] test: update tests --- tests/redis_cluster_connection.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/redis_cluster_connection.spec.ts b/tests/redis_cluster_connection.spec.ts index 95e2e3c..95aa025 100644 --- a/tests/redis_cluster_connection.spec.ts +++ b/tests/redis_cluster_connection.spec.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import { Redis } from 'ioredis' import { test } from '@japa/runner' import { pEvent } from '../tests_helpers/main.js' import RedisClusterConnection from '../src/connections/redis_cluster_connection.js' @@ -33,11 +34,12 @@ test.group('Redis cluster factory', () => { assert.equal(connection.status, 'ready') }) - test('emit node:added event', async ({ cleanup }) => { + test('emit node:added event', async ({ assert, cleanup }) => { const connection = new RedisClusterConnection('main', nodes, {}) cleanup(() => connection.quit()) - await pEvent(connection, 'node:added') + const response = await pEvent(connection, 'node:added') + assert.instanceOf(response?.node, Redis) }) test('execute redis commands', async ({ assert, cleanup }) => { From 4d1e946e62ab238f85b87f43afab24b1a5979e4d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 21:30:00 +0530 Subject: [PATCH 47/71] test: fix failing tests --- tests/redis_provider.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/redis_provider.spec.ts b/tests/redis_provider.spec.ts index 951ac41..09a031c 100644 --- a/tests/redis_provider.spec.ts +++ b/tests/redis_provider.spec.ts @@ -7,11 +7,11 @@ * file that was distributed with this source code. */ -import { pEvent } from 'p-event' import { test } from '@japa/runner' import { IgnitorFactory } from '@adonisjs/core/factories' import { defineConfig } from '../index.js' +import { pEvent } from '../tests_helpers/main.js' import RedisManager from '../src/redis_manager.js' const BASE_URL = new URL('./tmp/', import.meta.url) From 35bd96fe5c3f11a2cd8a47aaa041dd46c5c06ab5 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 21:34:04 +0530 Subject: [PATCH 48/71] test: fix failing tests --- tests/redis_manager.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/redis_manager.spec.ts b/tests/redis_manager.spec.ts index b27a8e4..842d0e0 100644 --- a/tests/redis_manager.spec.ts +++ b/tests/redis_manager.spec.ts @@ -276,7 +276,7 @@ test.group('Redis Manager', () => { const errorLog = JSON.parse(manager.logs[0]) assert.equal(errorLog.level, 60) - assert.equal(errorLog.err.message, 'connect ECONNREFUSED 127.0.0.1:4444') + assert.equal(errorLog.err.message, 'connect ECONNREFUSED 0.0.0.0:4444') }) test('disable error logging', async ({ assert }) => { From c291d6a4fbaf11766f5dd66de01846582a4ce797 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 28 Jul 2023 21:40:56 +0530 Subject: [PATCH 49/71] chore(release): 8.0.0-7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5931107..1130380 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-6", + "version": "8.0.0-7", "type": "module", "main": "build/index.js", "files": [ From 9af01f7399608c6e6903001c65023d902d1553e3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 31 Jul 2023 11:39:38 +0530 Subject: [PATCH 50/71] refactor: export redis manager from the main entrypoint --- index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.ts b/index.ts index c9fa3fa..c5e6356 100644 --- a/index.ts +++ b/index.ts @@ -13,3 +13,4 @@ export { defineConfig } from './src/define_config.js' export { stubsRoot } from './stubs/index.js' export { configure } from './configure.js' export * as errors from './src/errors.js' +export { default as RedisManager } from './src/redis_manager.js' From aa73701524d19e89f76d1ed748c35595d1c72f71 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 31 Jul 2023 11:45:22 +0530 Subject: [PATCH 51/71] chore(release): 8.0.0-8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1130380..e313fdc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-7", + "version": "8.0.0-8", "type": "module", "main": "build/index.js", "files": [ From 48911348dfb072b5be7dcd4589de909520c75af7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 16:52:45 +0530 Subject: [PATCH 52/71] chore: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e313fdc..b093d2b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { - "@adonisjs/core": "^6.1.5-12", + "@adonisjs/core": "^6.1.5-18", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", @@ -74,7 +74,7 @@ "ioredis": "^5.3.2" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-12" + "@adonisjs/core": "^6.1.5-18" }, "author": "virk,adonisjs", "license": "MIT", From 0a6ec4f07cad31a33d9ae2ccf25d735e6c6d6a54 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 17:00:24 +0530 Subject: [PATCH 53/71] feat: setup env validations --- configure.ts | 10 ++++++++++ package.json | 1 + tests/configure.spec.ts | 23 +++++++++++++++++------ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/configure.ts b/configure.ts index c2a080c..6595a13 100644 --- a/configure.ts +++ b/configure.ts @@ -27,6 +27,16 @@ export async function configure(command: Configure) { REDIS_PASSWORD: '', }) + /** + * Validate environment variables + */ + await command.defineEnvValidations({ + variables: { + REDIS_HOST: `Env.schema.string({ format: 'host' })`, + REDIS_PORT: 'Env.schema.number()', + }, + }) + /** * Add provider to rc file */ diff --git a/package.json b/package.json index b093d2b..a47ebb6 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { + "@adonisjs/assembler": "^6.1.3-18", "@adonisjs/core": "^6.1.5-18", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 4eeb473..87ac54f 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -57,23 +57,34 @@ test.group('Configure', (group) => { await assert.fileContains('config/redis.ts', `declare module '@adonisjs/redis/types'`) }) - test('add redis_provider to the rc file', async ({ assert }) => { - const { command } = await setupConfigureCommand() + test('add redis_provider to the rc file', async ({ fs, assert }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + const { command } = await setupConfigureCommand() await command.exec() - await assert.fileExists('.adonisrc.json') - await assert.fileContains('.adonisrc.json', '"@adonisjs/redis/redis_provider"') + await assert.fileExists('adonisrc.ts') + await assert.fileContains( + 'adonisrc.ts', + `providers: [() => import('@adonisjs/redis/redis_provider')]` + ) }) test('add env variables for the selected drivers', async ({ assert, fs }) => { - const { command } = await setupConfigureCommand() - + await fs.createJson('tsconfig.json', {}) await fs.create('.env', '') + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const { command } = await setupConfigureCommand() await command.exec() await assert.fileContains('.env', 'REDIS_HOST=127.0.0.1') await assert.fileContains('.env', 'REDIS_PORT=6379') await assert.fileContains('.env', 'REDIS_PASSWORD=') + + await assert.fileContains('start/env.ts', `REDIS_HOST: Env.schema.string({ format: 'host' })`) + await assert.fileContains('start/env.ts', 'REDIS_PORT: Env.schema.number()') }) }) From 9d2fab5157a2dcb0fd68aee6ae3e18a6a2453feb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 17:11:32 +0530 Subject: [PATCH 54/71] chore(release): 8.0.0-9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a47ebb6..fcc887c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-8", + "version": "8.0.0-9", "type": "module", "main": "build/index.js", "files": [ From 52f9079ef5000998fe4b2e798337ba739e3b618a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 11:34:15 +0530 Subject: [PATCH 55/71] chore: update dependencies --- configure.ts | 8 +++++--- package.json | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/configure.ts b/configure.ts index 6595a13..1e8bb22 100644 --- a/configure.ts +++ b/configure.ts @@ -18,10 +18,12 @@ export async function configure(command: Configure) { */ await command.publishStub('config/redis.stub') + const codemods = await command.createCodemods() + /** * Add environment variables */ - await command.defineEnvVariables({ + await codemods.defineEnvVariables({ REDIS_HOST: '127.0.0.1', REDIS_PORT: '6379', REDIS_PASSWORD: '', @@ -30,7 +32,7 @@ export async function configure(command: Configure) { /** * Validate environment variables */ - await command.defineEnvValidations({ + await codemods.defineEnvValidations({ variables: { REDIS_HOST: `Env.schema.string({ format: 'host' })`, REDIS_PORT: 'Env.schema.number()', @@ -40,7 +42,7 @@ export async function configure(command: Configure) { /** * Add provider to rc file */ - await command.updateRcFile((rcFile) => { + await codemods.updateRcFile((rcFile) => { rcFile.addProvider('@adonisjs/redis/redis_provider') }) } diff --git a/package.json b/package.json index fcc887c..0f6ef9d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "devDependencies": { "@adonisjs/assembler": "^6.1.3-18", - "@adonisjs/core": "^6.1.5-18", + "@adonisjs/core": "^6.1.5-19", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", @@ -75,7 +75,7 @@ "ioredis": "^5.3.2" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-18" + "@adonisjs/core": "^6.1.5-19" }, "author": "virk,adonisjs", "license": "MIT", From 3b478b791ca30795c8e2618cf74bb7de151de86d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 11:35:32 +0530 Subject: [PATCH 56/71] chore(release): 8.0.0-10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f6ef9d..8065e16 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-9", + "version": "8.0.0-10", "type": "module", "main": "build/index.js", "files": [ From 2f4316f5aebed27888df63c38472a5ac4a6af570 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 18 Oct 2023 11:18:16 +0530 Subject: [PATCH 57/71] chore: update dependencies --- package.json | 32 ++++++++++++++++---------------- stubs/config/redis.stub | 6 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 8065e16..31287e7 100644 --- a/package.json +++ b/package.json @@ -44,38 +44,38 @@ "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { - "@adonisjs/assembler": "^6.1.3-18", - "@adonisjs/core": "^6.1.5-19", + "@adonisjs/assembler": "^6.1.3-25", + "@adonisjs/core": "^6.1.5-27", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.6.7", - "@commitlint/config-conventional": "^17.6.7", - "@japa/assert": "2.0.0-1", - "@japa/expect-type": "2.0.0-0", - "@japa/file-system": "2.0.0-1", - "@japa/runner": "3.0.0-6", - "@swc/core": "^1.3.69", - "@types/node": "^20.4.2", + "@commitlint/cli": "^17.8.0", + "@commitlint/config-conventional": "^17.8.0", + "@japa/assert": "^2.0.0", + "@japa/expect-type": "^2.0.0", + "@japa/file-system": "^2.0.0", + "@japa/runner": "^3.0.2", + "@swc/core": "1.3.82", + "@types/node": "^20.8.6", "c8": "^8.0.0", "copyfiles": "^2.4.1", - "del-cli": "^5.0.0", + "del-cli": "^5.1.0", "dotenv": "^16.3.1", - "eslint": "^8.45.0", + "eslint": "^8.51.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.0", + "prettier": "^3.0.3", "ts-node": "^10.9.1", - "typescript": "^5.1.6" + "typescript": "^5.2.2" }, "dependencies": { - "@poppinss/utils": "6.5.0-3", + "@poppinss/utils": "^6.5.0", "emittery": "^1.0.1", "ioredis": "^5.3.2" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-19" + "@adonisjs/core": "^6.1.5-27" }, "author": "virk,adonisjs", "license": "MIT", diff --git a/stubs/config/redis.stub b/stubs/config/redis.stub index 26d0025..2c47133 100644 --- a/stubs/config/redis.stub +++ b/stubs/config/redis.stub @@ -1,6 +1,6 @@ ---- -to: {{ app.configPath('redis.ts') }} ---- +{{{ + exports({ to: app.configPath('redis.ts') }) +}}} import env from '#start/env' import { defineConfig } from '@adonisjs/redis' From 0fbfab9c9dcf8bce9c78776d1dabf4f4d19fc423 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 18 Oct 2023 11:32:49 +0530 Subject: [PATCH 58/71] chore: use tsup for bundling --- package.json | 31 ++++++++++++++++++------------- providers/redis_provider.ts | 7 +++++++ tests/configure.spec.ts | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 31287e7..e0917d1 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,7 @@ "type": "module", "main": "build/index.js", "files": [ - "build/configure.js", - "build/configure.d.ts", - "build/index.js", - "build/index.d.ts", - "build/src", - "build/services", - "build/providers", - "build/factories", - "build/stubs", - "build/index.d.ts", - "build/index.js" + "build" ], "engines": { "node": ">=18.16.0" @@ -33,8 +23,8 @@ "test:docker": "docker-compose -f docker-compose.ci.yml run --rm tests", "quick:test": "node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "clean": "del-cli build", - "copy:templates": "copyfiles \"stubs/**/**/*.stub\" build", - "compile": "npm run lint && npm run clean && tsc && npm run copy:templates", + "copy:templates": "copyfiles --up 1 \"stubs/**/**/*.stub\" build", + "compile": "npm run lint && npm run clean && tsup-node && npm run copy:templates", "build": "npm run compile", "release": "np", "version": "npm run build", @@ -67,6 +57,7 @@ "np": "^8.0.4", "prettier": "^3.0.3", "ts-node": "^10.9.1", + "tsup": "^7.2.0", "typescript": "^5.2.2" }, "dependencies": { @@ -119,5 +110,19 @@ "tests/**", "src/repl_bindings.ts" ] + }, + "tsup": { + "entry": [ + "./index.ts", + "./services/main.ts", + "./providers/redis_provider.ts", + "./factories/main.ts", + "./src/types/main.ts" + ], + "outDir": "./build", + "clean": true, + "format": "esm", + "dts": true, + "target": "esnext" } } diff --git a/providers/redis_provider.ts b/providers/redis_provider.ts index b84cf02..4c978a5 100644 --- a/providers/redis_provider.ts +++ b/providers/redis_provider.ts @@ -8,6 +8,13 @@ */ import type { ApplicationService } from '@adonisjs/core/types' +import type { RedisService } from '../src/types/main.js' + +declare module '@adonisjs/core/types' { + export interface ContainerBindings { + redis: RedisService + } +} /** * Registering the Redis manager as a singleton to the container diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 87ac54f..39da533 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -7,8 +7,8 @@ * file that was distributed with this source code. */ -import { fileURLToPath } from 'node:url' import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' import { IgnitorFactory } from '@adonisjs/core/factories' import Configure from '@adonisjs/core/commands/configure' From 3b786c0871a01228715ce5cdc9751578621cbc93 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 18 Oct 2023 11:36:48 +0530 Subject: [PATCH 59/71] refactor: cleanup types --- factories/redis_manager.ts | 2 +- index.ts | 10 +++++----- package.json | 4 ++-- providers/redis_provider.ts | 2 +- services/main.ts | 2 +- src/connections/abstract_connection.ts | 2 +- src/connections/redis_cluster_connection.ts | 2 +- src/connections/redis_connection.ts | 2 +- src/define_config.ts | 2 +- src/redis_manager.ts | 2 +- src/{types/main.ts => types.ts} | 8 ++++---- src/types/extended.ts | 16 ---------------- 12 files changed, 19 insertions(+), 35 deletions(-) rename src/{types/main.ts => types.ts} (92%) delete mode 100644 src/types/extended.ts diff --git a/factories/redis_manager.ts b/factories/redis_manager.ts index b5c2511..a7cfe51 100644 --- a/factories/redis_manager.ts +++ b/factories/redis_manager.ts @@ -9,7 +9,7 @@ import RedisManager from '../src/redis_manager.js' import { LoggerFactory } from '@adonisjs/core/factories/logger' -import type { RedisClusterConnectionConfig, RedisConnectionConfig } from '../src/types/main.js' +import type { RedisClusterConnectionConfig, RedisConnectionConfig } from '../src/types.js' /** * Redis manager factory is used to create an instance of the redis diff --git a/index.ts b/index.ts index c5e6356..680b6f3 100644 --- a/index.ts +++ b/index.ts @@ -7,10 +7,10 @@ * file that was distributed with this source code. */ -import './src/types/extended.js' - -export { defineConfig } from './src/define_config.js' -export { stubsRoot } from './stubs/index.js' -export { configure } from './configure.js' export * as errors from './src/errors.js' +export { configure } from './configure.js' +export { stubsRoot } from './stubs/index.js' +export { defineConfig } from './src/define_config.js' export { default as RedisManager } from './src/redis_manager.js' +export { RedisConnection } from './src/connections/redis_connection.js' +export { RedisClusterConnection } from './src/connections/redis_cluster_connection.js' diff --git a/package.json b/package.json index e0917d1..9a88429 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "./services/main": "./build/services/main.js", "./redis_provider": "./build/providers/redis_provider.js", "./factories": "./build/factories/main.js", - "./types": "./build/src/types/main.js" + "./types": "./build/src/types.js" }, "scripts": { "pretest": "npm run lint", @@ -117,7 +117,7 @@ "./services/main.ts", "./providers/redis_provider.ts", "./factories/main.ts", - "./src/types/main.ts" + "./src/types.ts" ], "outDir": "./build", "clean": true, diff --git a/providers/redis_provider.ts b/providers/redis_provider.ts index 4c978a5..b8cca18 100644 --- a/providers/redis_provider.ts +++ b/providers/redis_provider.ts @@ -8,7 +8,7 @@ */ import type { ApplicationService } from '@adonisjs/core/types' -import type { RedisService } from '../src/types/main.js' +import type { RedisService } from '../src/types.js' declare module '@adonisjs/core/types' { export interface ContainerBindings { diff --git a/services/main.ts b/services/main.ts index 47af1a1..91bf657 100644 --- a/services/main.ts +++ b/services/main.ts @@ -8,7 +8,7 @@ */ import app from '@adonisjs/core/services/app' -import { RedisService } from '../src/types/main.js' +import { RedisService } from '../src/types.js' let redis: RedisService diff --git a/src/connections/abstract_connection.ts b/src/connections/abstract_connection.ts index f3ab253..5681d65 100644 --- a/src/connections/abstract_connection.ts +++ b/src/connections/abstract_connection.ts @@ -18,7 +18,7 @@ import type { ConnectionEvents, PubSubChannelHandler, PubSubPatternHandler, -} from '../types/main.js' +} from '../types.js' /** * Abstract factory implements the shared functionality required by Redis cluster diff --git a/src/connections/redis_cluster_connection.ts b/src/connections/redis_cluster_connection.ts index 1673acd..ad3d00c 100644 --- a/src/connections/redis_cluster_connection.ts +++ b/src/connections/redis_cluster_connection.ts @@ -16,7 +16,7 @@ import type { ConnectionEvents, IORedisBaseCommands, RedisClusterConnectionConfig, -} from '../types/main.js' +} from '../types.js' /** * Redis cluster connection exposes the API to run Redis commands using `ioredis` as the diff --git a/src/connections/redis_connection.ts b/src/connections/redis_connection.ts index e64922f..1abe491 100644 --- a/src/connections/redis_connection.ts +++ b/src/connections/redis_connection.ts @@ -16,7 +16,7 @@ import type { ConnectionEvents, RedisConnectionConfig, IORedisConnectionCommands, -} from '../types/main.js' +} from '../types.js' /** * Redis connection exposes the API to run Redis commands using `ioredis` as the diff --git a/src/define_config.ts b/src/define_config.ts index 9588778..7cc56da 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -8,7 +8,7 @@ */ import { RuntimeException } from '@poppinss/utils' -import type { RedisConnectionsList } from './types/main.js' +import type { RedisConnectionsList } from './types.js' /** * Define config for redis diff --git a/src/redis_manager.ts b/src/redis_manager.ts index a6bcf5c..003159d 100644 --- a/src/redis_manager.ts +++ b/src/redis_manager.ts @@ -22,7 +22,7 @@ import type { PubSubChannelHandler, PubSubPatternHandler, RedisConnectionsList, -} from './types/main.js' +} from './types.js' /** * Redis Manager exposes the API to manage multiple redis connections diff --git a/src/types/main.ts b/src/types.ts similarity index 92% rename from src/types/main.ts rename to src/types.ts index 813b2ad..e283ba3 100644 --- a/src/types/main.ts +++ b/src/types.ts @@ -9,10 +9,10 @@ import type { Redis, Cluster, RedisOptions, ClusterOptions } from 'ioredis' -import type RedisManager from '../redis_manager.js' -import type { baseMethods, redisMethods } from '../connections/io_methods.js' -import type RedisConnection from '../connections/redis_connection.js' -import type RedisClusterConnection from '../connections/redis_cluster_connection.js' +import type RedisManager from './redis_manager.js' +import type RedisConnection from './connections/redis_connection.js' +import type { baseMethods, redisMethods } from './connections/io_methods.js' +import type RedisClusterConnection from './connections/redis_cluster_connection.js' /** * PubSub subscriber diff --git a/src/types/extended.ts b/src/types/extended.ts deleted file mode 100644 index e4aca4f..0000000 --- a/src/types/extended.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * @adonisjs/redis - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import type { RedisService } from './main.js' - -declare module '@adonisjs/core/types' { - export interface ContainerBindings { - redis: RedisService - } -} From 2fa425fbd5167bdd5d11b8520e0c2599761f52fd Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 18 Oct 2023 11:41:47 +0530 Subject: [PATCH 60/71] test: increase tests timeout for codemods tests --- tests/configure.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 39da533..b01ef7e 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -46,6 +46,8 @@ test.group('Configure', (group) => { context.fs.basePath = fileURLToPath(BASE_URL) }) + group.tap((t) => t.timeout(60 * 1000)) + test('publish config file', async ({ assert }) => { const { command } = await setupConfigureCommand() From 54a11c2ae61a388a35c69aa9d41e4fa50317d2d5 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 18 Oct 2023 11:44:07 +0530 Subject: [PATCH 61/71] chore(release): 8.0.0-11 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9a88429..ab2c83d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-10", + "version": "8.0.0-11", "type": "module", "main": "build/index.js", "files": [ @@ -111,7 +111,7 @@ "src/repl_bindings.ts" ] }, - "tsup": { + "tsup": { "entry": [ "./index.ts", "./services/main.ts", From a7ffacad19ba094b48629a1e287eb1e4e3ef2e2e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 19 Oct 2023 12:27:28 +0530 Subject: [PATCH 62/71] fix: import InferConnections from the types --- stubs/config/redis.stub | 1 + 1 file changed, 1 insertion(+) diff --git a/stubs/config/redis.stub b/stubs/config/redis.stub index 2c47133..a1afccb 100644 --- a/stubs/config/redis.stub +++ b/stubs/config/redis.stub @@ -3,6 +3,7 @@ }}} import env from '#start/env' import { defineConfig } from '@adonisjs/redis' +import { InferConnections } from '@adonisjs/redis/types' const redisConfig = defineConfig({ connection: 'main', From d727e1cd512deafa4ec91a30683606b02591b6ee Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 19 Oct 2023 12:29:34 +0530 Subject: [PATCH 63/71] chore(release): 8.0.0-12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab2c83d..d31de8e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-11", + "version": "8.0.0-12", "type": "module", "main": "build/index.js", "files": [ From c151b279219a487cdc4a25c4247aab6b37bb8fce Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 13:26:11 +0530 Subject: [PATCH 64/71] chore: update dependencies --- package.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index d31de8e..16b43a8 100644 --- a/package.json +++ b/package.json @@ -34,39 +34,39 @@ "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { - "@adonisjs/assembler": "^6.1.3-25", - "@adonisjs/core": "^6.1.5-27", - "@adonisjs/eslint-config": "^1.1.8", - "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.8.0", - "@commitlint/config-conventional": "^17.8.0", - "@japa/assert": "^2.0.0", + "@adonisjs/assembler": "^6.1.3-28", + "@adonisjs/core": "^6.1.5-32", + "@adonisjs/eslint-config": "^1.1.9", + "@adonisjs/prettier-config": "^1.1.9", + "@adonisjs/tsconfig": "^1.1.9", + "@commitlint/cli": "^18.4.3", + "@commitlint/config-conventional": "^18.4.3", + "@japa/assert": "^2.0.1", "@japa/expect-type": "^2.0.0", - "@japa/file-system": "^2.0.0", - "@japa/runner": "^3.0.2", - "@swc/core": "1.3.82", - "@types/node": "^20.8.6", + "@japa/file-system": "^2.0.1", + "@japa/runner": "^3.1.0", + "@swc/core": "^1.3.99", + "@types/node": "^20.9.4", "c8": "^8.0.0", "copyfiles": "^2.4.1", "del-cli": "^5.1.0", "dotenv": "^16.3.1", - "eslint": "^8.51.0", + "eslint": "^8.54.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "ts-node": "^10.9.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "tsup": "^8.0.1", + "typescript": "5.2.2" }, "dependencies": { - "@poppinss/utils": "^6.5.0", + "@poppinss/utils": "^6.5.1", "emittery": "^1.0.1", "ioredis": "^5.3.2" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-27" + "@adonisjs/core": "^6.1.5-32" }, "author": "virk,adonisjs", "license": "MIT", From dd80390bf71ea24b6a127886be0da8ad7b9c4377 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 14:17:36 +0530 Subject: [PATCH 65/71] chore: publish source maps and use tsc for generating types --- package.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 16b43a8..81e81ab 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "type": "module", "main": "build/index.js", "files": [ - "build" + "build", + "!build/bin", + "!build/tests", + "!build/tests_helpers" ], "engines": { "node": ">=18.16.0" @@ -24,7 +27,9 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm ./bin/test.js", "clean": "del-cli build", "copy:templates": "copyfiles --up 1 \"stubs/**/**/*.stub\" build", - "compile": "npm run lint && npm run clean && tsup-node && npm run copy:templates", + "precompile": "npm run lint && npm run clean", + "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", + "postcompile": "npm run copy:templates", "build": "npm run compile", "release": "np", "version": "npm run build", @@ -116,13 +121,13 @@ "./index.ts", "./services/main.ts", "./providers/redis_provider.ts", - "./factories/main.ts", - "./src/types.ts" + "./factories/main.ts" ], "outDir": "./build", "clean": true, "format": "esm", - "dts": true, + "dts": false, + "sourcemap": true, "target": "esnext" } } From 630f8df79afaf7ade1d8d42ae38ac4ca5cfed485 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 14:19:05 +0530 Subject: [PATCH 66/71] ci: update node versions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a6d4f0..a27f2ce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.16.0, 20.x] + node-version: [20.10.0, 21.x] steps: - name: Checkout code From cc51db7381c86353910c8e3123d9cca3c98e720a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 23 Nov 2023 14:28:13 +0530 Subject: [PATCH 67/71] chore(release): 8.0.0-13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81e81ab..4a2d9e3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-12", + "version": "8.0.0-13", "type": "module", "main": "build/index.js", "files": [ From 4290d1321cb38f814c510dcc2e23b0ee943481f8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sun, 24 Dec 2023 14:21:08 +0530 Subject: [PATCH 68/71] chore(package): update dependencies --- package.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 4a2d9e3..491841b 100644 --- a/package.json +++ b/package.json @@ -39,34 +39,34 @@ "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { - "@adonisjs/assembler": "^6.1.3-28", - "@adonisjs/core": "^6.1.5-32", - "@adonisjs/eslint-config": "^1.1.9", - "@adonisjs/prettier-config": "^1.1.9", - "@adonisjs/tsconfig": "^1.1.9", + "@adonisjs/assembler": "^7.0.0-0", + "@adonisjs/core": "^6.1.5-34", + "@adonisjs/eslint-config": "^1.2.0", + "@adonisjs/prettier-config": "^1.2.0", + "@adonisjs/tsconfig": "^1.2.0", "@commitlint/cli": "^18.4.3", "@commitlint/config-conventional": "^18.4.3", - "@japa/assert": "^2.0.1", - "@japa/expect-type": "^2.0.0", - "@japa/file-system": "^2.0.1", - "@japa/runner": "^3.1.0", - "@swc/core": "^1.3.99", - "@types/node": "^20.9.4", + "@japa/assert": "^2.1.0", + "@japa/expect-type": "^2.0.1", + "@japa/file-system": "^2.1.1", + "@japa/runner": "^3.1.1", + "@swc/core": "^1.3.101", + "@types/node": "^20.10.5", "c8": "^8.0.0", "copyfiles": "^2.4.1", "del-cli": "^5.1.0", "dotenv": "^16.3.1", - "eslint": "^8.54.0", + "eslint": "^8.56.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^8.0.4", - "prettier": "^3.1.0", - "ts-node": "^10.9.1", + "np": "^9.2.0", + "prettier": "^3.1.1", + "ts-node": "^10.9.2", "tsup": "^8.0.1", - "typescript": "5.2.2" + "typescript": "^5.3.3" }, "dependencies": { - "@poppinss/utils": "^6.5.1", + "@poppinss/utils": "^6.7.0", "emittery": "^1.0.1", "ioredis": "^5.3.2" }, From e76a141189744e68836d0cfac8cfc9d43be2aa0b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sun, 24 Dec 2023 14:23:47 +0530 Subject: [PATCH 69/71] docs(README): update link to docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13b2fbf..d5f73f4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Redis provider for AdonisJS with support for multiple Redis connections, cluster, pub/sub and much more ## Official Documentation -The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/redis) +The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/database/redis) ## Contributing One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. From 3ff8d1a75ac03a4ad586258eee1c7623c8fbcd63 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sun, 24 Dec 2023 14:59:14 +0530 Subject: [PATCH 70/71] chore(release): 8.0.0-14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 491841b..4d36c9d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/redis", "description": "AdonisJS addon for Redis", - "version": "8.0.0-13", + "version": "8.0.0-14", "type": "module", "main": "build/index.js", "files": [ From cbc6b145a057adb1e3614c3bee93c207c2a1107f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 8 Jan 2024 15:10:21 +0530 Subject: [PATCH 71/71] chore: update dependencies --- configure.ts | 7 ++++--- index.ts | 2 +- package.json | 23 ++++++++++++----------- stubs/{index.ts => main.ts} | 0 tests/redis_provider.spec.ts | 24 ++++++------------------ 5 files changed, 23 insertions(+), 33 deletions(-) rename stubs/{index.ts => main.ts} (100%) diff --git a/configure.ts b/configure.ts index 1e8bb22..04f0178 100644 --- a/configure.ts +++ b/configure.ts @@ -8,17 +8,18 @@ */ import type Configure from '@adonisjs/core/commands/configure' +import { stubsRoot } from './stubs/main.js' /** * Configures the package */ export async function configure(command: Configure) { + const codemods = await command.createCodemods() + /** * Publish config file */ - await command.publishStub('config/redis.stub') - - const codemods = await command.createCodemods() + await codemods.makeUsingStub(stubsRoot, 'config/redis.stub', {}) /** * Add environment variables diff --git a/index.ts b/index.ts index 680b6f3..d3d442a 100644 --- a/index.ts +++ b/index.ts @@ -9,7 +9,7 @@ export * as errors from './src/errors.js' export { configure } from './configure.js' -export { stubsRoot } from './stubs/index.js' +export { stubsRoot } from './stubs/main.js' export { defineConfig } from './src/define_config.js' export { default as RedisManager } from './src/redis_manager.js' export { RedisConnection } from './src/connections/redis_connection.js' diff --git a/package.json b/package.json index 4d36c9d..6752f8b 100644 --- a/package.json +++ b/package.json @@ -39,20 +39,20 @@ "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/redis" }, "devDependencies": { - "@adonisjs/assembler": "^7.0.0-0", - "@adonisjs/core": "^6.1.5-34", - "@adonisjs/eslint-config": "^1.2.0", - "@adonisjs/prettier-config": "^1.2.0", - "@adonisjs/tsconfig": "^1.2.0", - "@commitlint/cli": "^18.4.3", - "@commitlint/config-conventional": "^18.4.3", + "@adonisjs/assembler": "^7.0.0", + "@adonisjs/core": "^6.2.0", + "@adonisjs/eslint-config": "^1.2.1", + "@adonisjs/prettier-config": "^1.2.1", + "@adonisjs/tsconfig": "^1.2.1", + "@commitlint/cli": "^18.4.4", + "@commitlint/config-conventional": "^18.4.4", "@japa/assert": "^2.1.0", "@japa/expect-type": "^2.0.1", "@japa/file-system": "^2.1.1", "@japa/runner": "^3.1.1", - "@swc/core": "^1.3.101", - "@types/node": "^20.10.5", - "c8": "^8.0.0", + "@swc/core": "^1.3.102", + "@types/node": "^20.10.7", + "c8": "^9.0.0", "copyfiles": "^2.4.1", "del-cli": "^5.1.0", "dotenv": "^16.3.1", @@ -71,7 +71,7 @@ "ioredis": "^5.3.2" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-32" + "@adonisjs/core": "^6.2.0" }, "author": "virk,adonisjs", "license": "MIT", @@ -119,6 +119,7 @@ "tsup": { "entry": [ "./index.ts", + "./src/types.ts", "./services/main.ts", "./providers/redis_provider.ts", "./factories/main.ts" diff --git a/stubs/index.ts b/stubs/main.ts similarity index 100% rename from stubs/index.ts rename to stubs/main.ts diff --git a/tests/redis_provider.spec.ts b/tests/redis_provider.spec.ts index 09a031c..aec1f89 100644 --- a/tests/redis_provider.spec.ts +++ b/tests/redis_provider.spec.ts @@ -15,26 +15,18 @@ import { pEvent } from '../tests_helpers/main.js' import RedisManager from '../src/redis_manager.js' const BASE_URL = new URL('./tmp/', import.meta.url) -const IMPORTER = (filePath: string) => { - if (filePath.startsWith('./') || filePath.startsWith('../')) { - return import(new URL(filePath, BASE_URL).href) - } - return import(filePath) -} test.group('Redis Provider', () => { test('register redis provider', async ({ assert }) => { const ignitor = new IgnitorFactory() .merge({ rcFileContents: { - providers: ['../../providers/redis_provider.js'], + providers: [() => import('../providers/redis_provider.js')], }, }) .withCoreConfig() .withCoreProviders() - .create(BASE_URL, { - importer: IMPORTER, - }) + .create(BASE_URL) const app = ignitor.createApp('web') await app.init() @@ -48,13 +40,11 @@ test.group('Redis Provider', () => { .withCoreConfig() .merge({ rcFileContents: { - providers: ['../../providers/redis_provider.js'], + providers: [() => import('../providers/redis_provider.js')], }, }) .withCoreProviders() - .create(BASE_URL, { - importer: IMPORTER, - }) + .create(BASE_URL) const app = ignitor.createApp('repl') await app.init() @@ -69,7 +59,7 @@ test.group('Redis Provider', () => { const ignitor = new IgnitorFactory() .merge({ rcFileContents: { - providers: ['../../providers/redis_provider.js'], + providers: [() => import('../providers/redis_provider.js')], }, }) .withCoreConfig() @@ -87,9 +77,7 @@ test.group('Redis Provider', () => { }), }, }) - .create(BASE_URL, { - importer: IMPORTER, - }) + .create(BASE_URL) const app = ignitor.createApp('web') await app.init()