diff --git a/.eslintignore b/.eslintignore index a24e501..618ef2b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ test/fixtures coverage +__snapshots__ diff --git a/.eslintrc b/.eslintrc index c799fe5..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "eslint-config-egg" + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] } diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index b3c2981..25bb2a4 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -11,6 +11,6 @@ jobs: name: Node.js uses: node-modules/github-actions/.github/workflows/node-test-mysql.yml@master with: - version: '16, 18, 20, 22' + version: '18.19.0, 18, 20, 22' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 6575f21..7e41e53 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,12 @@ logs/ npm-debug.log node_modules/ coverage/ -.idea/ -run/ +test/fixtures/**/run .DS_Store -*.swp -.nyc_output/ +.tshy* +.eslintcache +dist +package-lock.json +.package-lock.json +test/fixtures/**/*.d.ts +run/ diff --git a/README.md b/README.md index 5dd48ea..0ff42d4 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,35 @@ -# egg-redis +# @eggjs/redis [![NPM version][npm-image]][npm-url] [![Node.js CI](https://github.com/eggjs/redis/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/redis/actions/workflows/nodejs.yml) [![Test coverage][codecov-image]][codecov-url] [![Known Vulnerabilities][snyk-image]][snyk-url] [![npm download][download-image]][download-url] -[![Node.js Version](https://img.shields.io/node/v/egg-redis.svg?style=flat)](https://nodejs.org/en/download/) +[![Node.js Version](https://img.shields.io/node/v/@eggjs/redis.svg?style=flat)](https://nodejs.org/en/download/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/eggjs/redis) -[npm-image]: https://img.shields.io/npm/v/egg-redis.svg?style=flat-square -[npm-url]: https://npmjs.org/package/egg-redis +[npm-image]: https://img.shields.io/npm/v/@eggjs/redis.svg?style=flat-square +[npm-url]: https://npmjs.org/package/@eggjs/redis [codecov-image]: https://codecov.io/gh/eggjs/redis/branch/master/graph/badge.svg [codecov-url]: https://codecov.io/gh/eggjs/redis -[snyk-image]: https://snyk.io/test/npm/egg-redis/badge.svg?style=flat-square -[snyk-url]: https://snyk.io/test/npm/egg-redis -[download-image]: https://img.shields.io/npm/dm/egg-redis.svg?style=flat-square -[download-url]: https://npmjs.org/package/egg-redis +[snyk-image]: https://snyk.io/test/npm/@eggjs/redis/badge.svg?style=flat-square +[snyk-url]: https://snyk.io/test/npm/@eggjs/redis +[download-image]: https://img.shields.io/npm/dm/@eggjs/redis.svg?style=flat-square +[download-url]: https://npmjs.org/package/@eggjs/redis -Redis client(support redis protocol) based on ioredis for egg framework +Valkey / Redis client (support [redis protocol](https://redis.io/docs/latest/develop/reference/protocol-spec/)) based on iovalkey for egg framework ## Install ```bash -npm i egg-redis --save +npm i @eggjs/redis ``` -redis Plugin for egg, support egg application access to redis. +Valkey / Redis Plugin for egg, support egg application access to Valkey / Redis Service. -This plugin based on [ioredis](https://github.com/luin/ioredis), if you want to know specific usage, you should refer to the document of [ioredis](https://github.com/luin/ioredis). +This plugin based on [ioredis](https://github.com/redis/ioredis). +If you want to know specific usage, you should refer to the document of [ioredis](https://github.com/redis/ioredis). ## Configuration @@ -37,7 +38,7 @@ Change `${app_root}/config/plugin.js` to enable redis plugin: ```js exports.redis = { enable: true, - package: 'egg-redis', + package: '@eggjs/redis', }; ``` @@ -82,10 +83,14 @@ config.redis = { ```javascript config.redis = { client: { - sentinels: [{ // Sentinel instances - port: 26379, // Sentinel port - host: '127.0.0.1', // Sentinel host - }], + // Sentinel instances + sentinels: [ + { + port: 26379, // Sentinel port + host: '127.0.0.1', // Sentinel host + }, + // other sentinel instance config + ], name: 'mymaster', // Master name password: 'auth', db: 0 @@ -110,11 +115,12 @@ Because it may be cause security risk, refer: If you want to access redis with no password, use `password: null`. -See [ioredis API Documentation](https://github.com/luin/ioredis/blob/master/API.md#new_Redis) for all available options. +See [ioredis API Documentation](https://github.com/redis/ioredis#basic-usage) for all available options. ### Customize `ioredis` version -`egg-redis` using ioredis@4 now, if you want to use other version of ioredis, you can pass the instance by `config.redis.Redis`: +`@eggjs/redis` using `ioredis@5` now, if you want to use other version of iovalkey or ioredis, +you can pass the instance by `config.redis.Redis`: ```js // config/config.default.js @@ -138,14 +144,14 @@ config.redis = { host: '127.0.0.1', // Redis host password: 'auth', db: 0, - weakDependent: true, // this redis instance won't block app start + weakDependent: true, // the redis instance won't block app start }, } ``` ## Usage -In controller, you can use `app.redis` to get the redis instance, check [ioredis](https://github.com/luin/ioredis#basic-usage) to see how to use. +In controller, you can use `app.redis` to get the redis instance, check [ioredis](https://github.com/redis/ioredis#basic-usage) to see how to use. ```js // app/controller/home.js @@ -190,30 +196,30 @@ Before you start to use Redis Cluster, please checkout the [document](https://re In controller, you also can use `app.redis` to get the redis instance based on Redis Cluster. ```js - // app/config/config.default.js - exports.redis = { - client: { - cluster: true, - nodes: [{ - host: '127.0.0.1', - port: '6379', - family: 'user', - password: 'password', - db: 'db', - }, { - host: '127.0.0.1', - port: '6380', - family: 'user', - password: 'password', - db: 'db', - }] - }, + client: { + cluster: true, + nodes: [ + { + host: '127.0.0.1', + port: '6379', + family: 'user', + password: 'password', + db: 'db', + }, + { + host: '127.0.0.1', + port: '6380', + family: 'user', + password: 'password', + db: 'db', + }, + ], + }, }; // app/controller/home.js - module.exports = app => { return class HomeController extends app.Controller { async index() { diff --git a/__snapshots__/redis.test.ts.js b/__snapshots__/redis.test.ts.js new file mode 100644 index 0000000..35965bf --- /dev/null +++ b/__snapshots__/redis.test.ts.js @@ -0,0 +1,12 @@ +exports['test/redis.test.js default config should make default config stable 1'] = { + "default": {}, + "app": true, + "agent": false, + "supportTimeCommand": true, + "client": { + "host": "127.0.0.1", + "port": 6379, + "password": "", + "db": "0" + } +} diff --git a/agent.js b/agent.js deleted file mode 100644 index 034542e..0000000 --- a/agent.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const redis = require('./lib/redis'); - -module.exports = agent => { - if (agent.config.redis.agent) redis(agent); -}; diff --git a/app.js b/app.js deleted file mode 100644 index d57a816..0000000 --- a/app.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const redis = require('./lib/redis'); - -module.exports = app => { - if (app.config.redis.app) redis(app); -}; diff --git a/config/config.default.js b/config/config.default.js deleted file mode 100644 index 641f3de..0000000 --- a/config/config.default.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -exports.redis = { - default: { - }, - app: true, - agent: false, - // redis client will try to use TIME command to detect client is ready or not - // if your redis server not support TIME command, please set this config to false - // see https://redis.io/commands/time - supportTimeCommand: true, - // Redis: require('ioredis'), // customize ioredis version, only set when you needed - - // Single Redis - // client: { - // host: 'host', - // port: 'port', - // family: 'user', - // password: 'password', - // db: 'db', - // }, - - // Cluster Redis - // client: { - // cluster: true, - // nodes: [{ - // host: 'host', - // port: 'port', - // family: 'user', - // password: 'password', - // db: 'db', - // },{ - // host: 'host', - // port: 'port', - // family: 'user', - // password: 'password', - // db: 'db', - // }, - // ]}, - - // Multi Redis - // clients: { - // instance1: { - // host: 'host', - // port: 'port', - // family: 'user', - // password: 'password', - // db: 'db', - // }, - // instance2: { - // host: 'host', - // port: 'port', - // family: 'user', - // password: 'password', - // db: 'db', - // }, - // }, -}; diff --git a/example/hello/app/router.ts b/example/hello/app/router.ts new file mode 100644 index 0000000..3f7682d --- /dev/null +++ b/example/hello/app/router.ts @@ -0,0 +1,12 @@ +import { Application } from 'egg'; + +export default (app: Application) => { + const { router } = app; + + router.get('/', async ctx => { + const redis = app.redis; + await redis.set('foo', 'bar'); + const cacheValue = await redis.get('foo'); + ctx.body = cacheValue; + }); +}; diff --git a/example/hello/config/config.default.ts b/example/hello/config/config.default.ts new file mode 100644 index 0000000..7370c06 --- /dev/null +++ b/example/hello/config/config.default.ts @@ -0,0 +1,3 @@ +export default { + keys: 'bala', +}; diff --git a/example/hello/config/plugin.ts b/example/hello/config/plugin.ts new file mode 100644 index 0000000..17e2689 --- /dev/null +++ b/example/hello/config/plugin.ts @@ -0,0 +1,6 @@ +export default { + redis: { + enable: true, + path: '../../../../..', + }, +}; diff --git a/example/hello/package.json b/example/hello/package.json new file mode 100644 index 0000000..2400d65 --- /dev/null +++ b/example/hello/package.json @@ -0,0 +1,4 @@ +{ + "name": "hello-redis", + "type": "module" +} diff --git a/example/hello/start.ts b/example/hello/start.ts new file mode 100644 index 0000000..0744c59 --- /dev/null +++ b/example/hello/start.ts @@ -0,0 +1,12 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { startEgg } from 'egg'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = await startEgg({ + baseDir: __dirname, +}); + +console.log(`Server started at http://localhost:${app.config.cluster.listen.port}`); diff --git a/example/hello/tsconfig.json b/example/hello/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/example/hello/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/example/hello/typings/index.d.ts b/example/hello/typings/index.d.ts new file mode 100644 index 0000000..9587dd5 --- /dev/null +++ b/example/hello/typings/index.d.ts @@ -0,0 +1,4 @@ +// make sure to import egg typings and let typescript know about it +// @see https://github.com/whxaxes/blog/issues/11 +// and https://www.typescriptlang.org/docs/handbook/declaration-merging.html +import '../../../src/index.ts'; diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index d476da0..0000000 --- a/index.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Redis, RedisOptions } from "ioredis"; - -interface ClusterOptions extends RedisOptions { - cluster?: boolean; - nodes?: RedisOptions[]; -} - -interface EggRedisOptions { - Redis?: Redis; - default?: object; - app?: boolean; - agent?: boolean; - client?: ClusterOptions; - clients?: Record; -} - -declare module 'egg' { - interface Application { - redis: Redis & Singleton; - } - - interface EggAppConfig { - redis: EggRedisOptions; - } -} diff --git a/lib/redis.js b/lib/redis.js deleted file mode 100644 index 9b4dc94..0000000 --- a/lib/redis.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const awaitFirst = require('await-first'); - -module.exports = app => { - app.addSingleton('redis', createClient); -}; - -let count = 0; -function createClient(config, app) { - const Redis = app.config.redis.Redis || require('ioredis'); - let client; - - if (config.cluster === true) { - assert(config.nodes && config.nodes.length !== 0, '[egg-redis] cluster nodes configuration is required when use cluster redis'); - - config.nodes.forEach(client => { - assert(client.host && client.port, `[egg-redis] 'host: ${client.host}', 'port: ${client.port}' are required on config`); - }); - app.coreLogger.info('[egg-redis] cluster connecting'); - client = new Redis.Cluster(config.nodes, config); - } else if (config.sentinels) { - assert(config.sentinels && config.sentinels.length !== 0, '[egg-redis] sentinels configuration is required when use redis sentinel'); - - config.sentinels.forEach(sentinel => { - assert(sentinel.host && sentinel.port, - `[egg-redis] 'host: ${sentinel.host}', 'port: ${sentinel.port}' are required on config`); - }); - - assert(config.name && config.password !== undefined && config.db !== undefined, - `[egg-redis] 'name of master: ${config.name}', 'password: ${config.password}', 'db: ${config.db}' are required on config`); - - app.coreLogger.info('[egg-redis] sentinel connecting start'); - client = new Redis(config); - } else { - assert((config.host && config.port && config.password !== undefined && config.db !== undefined) || config.path, - `[egg-redis] 'host: ${config.host}', 'port: ${config.port}', 'password: ${config.password}', 'db: ${config.db}' or 'path:${config.path}' are required on config`); - if (config.host) { - app.coreLogger.info('[egg-redis] server connecting redis://:***@%s:%s/%s', - config.host, config.port, config.db); - } else { - app.coreLogger.info('[egg-redis] server connecting %s', - config.path || config); - } - - client = new Redis(config); - } - - client.on('connect', () => { - app.coreLogger.info('[egg-redis] client connect success'); - }); - client.on('error', err => { - app.coreLogger.error('[egg-redis] client error: %s', err); - app.coreLogger.error(err); - }); - - app.beforeStart(async () => { - const index = count++; - if (config.weakDependent) { - app.coreLogger.info(`[egg-redis] instance[${index}] is weak dependent and won't block app start`); - client.once('ready', () => { - app.coreLogger.info(`[egg-redis] instance[${index}] status OK`); - }); - return; - } - - await awaitFirst(client, [ 'ready', 'error' ]); - app.coreLogger.info(`[egg-redis] instance[${index}] status OK, client ready`); - }); - - return client; -} diff --git a/package.json b/package.json index 1f524fc..6e7901c 100644 --- a/package.json +++ b/package.json @@ -1,53 +1,26 @@ { - "name": "egg-redis", + "name": "@eggjs/redis", "version": "2.6.1", - "description": "Redis plugin for egg", + "publishConfig": { + "access": "public" + }, + "description": "Valkey / Redis plugin for egg", "eggPlugin": { - "name": "redis" + "name": "redis", + "exports": { + "import": "./dist/esm", + "require": "./dist/commonjs", + "typescript": "./src" + } }, - "files": [ - "index.d.ts", - "app.js", - "agent.js", - "lib", - "config" - ], - "types": "./index.d.ts", "keywords": [ "egg", "eggPlugin", "egg-plugin", "redis", + "Valkey", "database" ], - "dependencies": { - "@types/ioredis": "^4.0.10", - "await-first": "^1.0.0", - "ioredis": "^4.9.0" - }, - "devDependencies": { - "@types/node": "^22.10.7", - "autod": "^3.1.0", - "egg": "^3.30.1", - "egg-bin": "^6.13.0", - "egg-mock": "^5.15.1", - "eslint": "^5.16.0", - "eslint-config-egg": "^7.3.1", - "supertest": "^4.0.2", - "typescript": "^5.7.3", - "urllib": "^4.6.11", - "utility": "^1.9.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "scripts": { - "test": "npm run lint -- --fix && npm run test-local", - "test-local": "egg-bin test --ts false", - "cov": "egg-bin cov --ts false", - "lint": "eslint .", - "ci": "npm run lint && npm run cov" - }, "repository": { "type": "git", "url": "git+https://github.com/eggjs/redis.git" @@ -57,5 +30,65 @@ }, "homepage": "https://github.com/eggjs/redis#readme", "author": "jtyjty99999", - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "dependencies": { + "@eggjs/core": "^6.3.0", + "ioredis": "^5.4.2" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/bin": "7", + "@eggjs/mock": "^6.0.5", + "@eggjs/tsconfig": "1", + "@types/mocha": "10", + "@types/node": "22", + "egg": "^4.0.3", + "eslint": "8", + "eslint-config-egg": "14", + "rimraf": "6", + "snap-shot-it": "^7.9.10", + "tshy": "3", + "tshy-after": "1", + "typescript": "5" + }, + "scripts": { + "lint": "eslint --cache src test --ext .ts", + "pretest": "npm run clean && npm run lint -- --fix", + "test": "egg-bin test", + "preci": "npm run clean && npm run lint", + "ci": "egg-bin cov", + "postci": "npm run prepublishOnly && npm run clean", + "clean": "rimraf dist", + "prepublishOnly": "tshy && tshy-after && attw --pack" + }, + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "src" + ], + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/src/agent.ts b/src/agent.ts new file mode 100644 index 0000000..6336ee3 --- /dev/null +++ b/src/agent.ts @@ -0,0 +1,3 @@ +import { RedisBoot } from './lib/redis.js'; + +export default RedisBoot; diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..6336ee3 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,3 @@ +import { RedisBoot } from './lib/redis.js'; + +export default RedisBoot; diff --git a/src/config/config.default.ts b/src/config/config.default.ts new file mode 100644 index 0000000..0db6dd8 --- /dev/null +++ b/src/config/config.default.ts @@ -0,0 +1,111 @@ +import type { RedisOptions, ClusterOptions } from 'ioredis'; + +export interface RedisClientOptions extends RedisOptions { + /** + * Whether to enable weakDependent mode, the redis client start will not block the application start + * + * Default to `undefined` + */ + weakDependent?: boolean; +} + +export interface RedisClusterOptions extends ClusterOptions { + cluster: true; + nodes: RedisClientOptions[]; +} + +export interface RedisConfig { + /** + * Default redis client config + * + * Default to `{}` + */ + default: RedisClientOptions; + /** + * Single Redis or Cluster Redis config + */ + client?: RedisClientOptions | RedisClusterOptions; + /** + * Multi Redis config + */ + clients?: Record; + /** + * redis client will try to use TIME command to detect client is ready or not + * if your redis server not support TIME command, please set this config to false + * see https://redis.io/commands/time + * + * Default to `true` + */ + supportTimeCommand: boolean; + /** + * Whether to enable redis for `app` + * + * Default to `true` + */ + app: boolean; + /** + * Whether to enable redis for `agent` + * + * Default to `false` + */ + agent: boolean; + /** + * Customize iovalkey version, only set when you needed + * + * Default to `undefined`, which means using the built-in ioredis + */ + Redis?: any; +} + +export default { + redis: { + default: {}, + app: true, + agent: false, + supportTimeCommand: true, + // Single Redis + // client: { + // host: 'host', + // port: 'port', + // family: 'user', + // password: 'password', + // db: 'db', + // }, + // + // Cluster Redis + // client: { + // cluster: true, + // nodes: [{ + // host: 'host', + // port: 'port', + // family: 'user', + // password: 'password', + // db: 'db', + // }, { + // host: 'host', + // port: 'port', + // family: 'user', + // password: 'password', + // db: 'db', + // }, + // ]}, + // + // Multi Redis + // clients: { + // instance1: { + // host: 'host', + // port: 'port', + // family: 'user', + // password: 'password', + // db: 'db', + // }, + // instance2: { + // host: 'host', + // port: 'port', + // family: 'user', + // password: 'password', + // db: 'db', + // }, + // }, + } as RedisConfig, +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ce5fb25 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +import './types.js'; diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..3393364 --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,92 @@ +import assert from 'node:assert'; +import { once } from 'node:events'; +import { Redis } from 'ioredis'; +import type { ILifecycleBoot, EggCore } from '@eggjs/core'; +import type { RedisClusterOptions, RedisClientOptions } from '../config/config.default.js'; + +export class RedisBoot implements ILifecycleBoot { + constructor(private readonly app: EggCore) { + // empty + } + async didLoad() { + const app = this.app; + if (app.type === 'application' && app.config.redis.app) { + app.addSingleton('redis', createClient); + } else if (app.type === 'agent' && app.config.redis.agent) { + app.addSingleton('redis', createClient); + } + } +} + +let count = 0; +function createClient(options: RedisClusterOptions | RedisClientOptions, app: EggCore) { + const RedisClass: typeof Redis = app.config.redis.Redis ?? Redis; + let client; + + if ('cluster' in options && options.cluster === true) { + const config = options as RedisClusterOptions; + assert(config.nodes && config.nodes.length !== 0, '[@eggjs/redis] cluster nodes configuration is required when use cluster redis'); + + config.nodes.forEach(client => { + assert(client.host && client.port, `[@eggjs/redis] 'host: ${client.host}', 'port: ${client.port}' are required on config`); + }); + app.coreLogger.info('[@eggjs/redis] cluster connecting'); + client = new RedisClass.Cluster(config.nodes, config as any); + } else if ('sentinels' in options && options.sentinels) { + const config = options as RedisClientOptions; + assert(config.sentinels && config.sentinels.length !== 0, '[@eggjs/redis] sentinels configuration is required when use redis sentinel'); + + config.sentinels.forEach(sentinel => { + assert(sentinel.host && sentinel.port, + `[@eggjs/redis] 'host: ${sentinel.host}', 'port: ${sentinel.port}' are required on config`); + }); + + const mask = config.password ? '***' : config.password; + assert(config.name && config.password !== undefined && config.db !== undefined, + `[@eggjs/redis] 'name of master: ${config.name}', 'password: ${mask}', 'db: ${config.db}' are required on config`); + + app.coreLogger.info('[@eggjs/redis] sentinel connecting start'); + client = new RedisClass(config as any); + } else { + const config = options as RedisClientOptions; + const mask = config.password ? '***' : config.password; + assert((config.host && config.port && config.password !== undefined && config.db !== undefined) || config.path, + `[@eggjs/redis] 'host: ${config.host}', 'port: ${config.port}', 'password: ${mask}', 'db: ${config.db}' or 'path:${config.path}' are required on config`); + if (config.host) { + app.coreLogger.info('[@eggjs/redis] server connecting redis://:***@%s:%s/%s', + config.host, config.port, config.db); + } else { + app.coreLogger.info('[@eggjs/redis] server connecting %s', + config.path || config); + } + + client = new RedisClass(config as any); + } + + client.on('connect', () => { + app.coreLogger.info('[@eggjs/redis] client connect success'); + }); + client.on('error', err => { + app.coreLogger.error('[@eggjs/redis] client error: %s', err); + app.coreLogger.error(err); + }); + + const index = count++; + app.lifecycle.registerBeforeStart(async () => { + if ('weakDependent' in options && options.weakDependent) { + app.coreLogger.info(`[@eggjs/redis] instance[${index}] is weak dependent and won't block app start`); + client.once('ready', () => { + app.coreLogger.info(`[@eggjs/redis] instance[${index}] status OK`); + }); + return; + } + + await Promise.race([ + once(client, 'ready'), + once(client, 'error'), + ]); + app.coreLogger.info(`[@eggjs/redis] instance[${index}] status OK, client ready`); + }, `[@eggjs/redis] instance[${index}] start check`); + + return client; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..14a917e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,14 @@ +import type { Singleton } from '@eggjs/core'; +import type { Redis } from 'ioredis'; +import type { RedisConfig } from './config/config.default.js'; + +declare module '@eggjs/core' { + // add EggAppConfig overrides types + interface EggAppConfig { + redis: RedisConfig; + } + + interface EggCore { + redis: Redis & Singleton; + } +} diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts new file mode 100644 index 0000000..53c65c7 --- /dev/null +++ b/src/typings/index.d.ts @@ -0,0 +1,4 @@ +// make sure to import egg typings and let typescript know about it +// @see https://github.com/whxaxes/blog/issues/11 +// and https://www.typescriptlang.org/docs/handbook/declaration-merging.html +import 'egg'; diff --git a/test/fixtures/apps/redisapp-customize/app/controller/home.js b/test/fixtures/apps/redisapp-customize/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redisapp-customize/app/controller/home.js +++ b/test/fixtures/apps/redisapp-customize/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/redisapp-default/app/controller/home.js b/test/fixtures/apps/redisapp-default/app/controller/home.js new file mode 100644 index 0000000..d77ceb9 --- /dev/null +++ b/test/fixtures/apps/redisapp-default/app/controller/home.js @@ -0,0 +1,9 @@ +module.exports = app => { + return class HomeController extends app.Controller { + async index() { + const { ctx, app } = this; + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); + } + }; +}; diff --git a/test/fixtures/apps/redisapp-default/app/router.js b/test/fixtures/apps/redisapp-default/app/router.js new file mode 100644 index 0000000..96d8892 --- /dev/null +++ b/test/fixtures/apps/redisapp-default/app/router.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function(app) { + app.get('/', 'home.index'); +}; diff --git a/test/fixtures/apps/redisapp-default/config/config.js b/test/fixtures/apps/redisapp-default/config/config.js new file mode 100644 index 0000000..00bff1e --- /dev/null +++ b/test/fixtures/apps/redisapp-default/config/config.js @@ -0,0 +1,16 @@ +exports.redis = { + client: { + host: '127.0.0.1', + port: 6379, + password: '', + db: '0', + }, +}; + +exports.logger = { + coreLogger: { + level: 'INFO', + }, +}; + +exports.keys = 'keys'; diff --git a/test/fixtures/apps/redisapp-default/package.json b/test/fixtures/apps/redisapp-default/package.json new file mode 100644 index 0000000..78a3f5b --- /dev/null +++ b/test/fixtures/apps/redisapp-default/package.json @@ -0,0 +1,3 @@ +{ + "name": "redisapp" +} diff --git a/test/fixtures/apps/redisapp-disable-offline-queue/app/controller/home.js b/test/fixtures/apps/redisapp-disable-offline-queue/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redisapp-disable-offline-queue/app/controller/home.js +++ b/test/fixtures/apps/redisapp-disable-offline-queue/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/redisapp-supportTimeCommand-false/app/controller/home.js b/test/fixtures/apps/redisapp-supportTimeCommand-false/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redisapp-supportTimeCommand-false/app/controller/home.js +++ b/test/fixtures/apps/redisapp-supportTimeCommand-false/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/redisapp-weakdependent/app/controller/home.js b/test/fixtures/apps/redisapp-weakdependent/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redisapp-weakdependent/app/controller/home.js +++ b/test/fixtures/apps/redisapp-weakdependent/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/redisapp-weakdependent/config/config.js b/test/fixtures/apps/redisapp-weakdependent/config/config.js index 75d4b76..ed3bdef 100644 --- a/test/fixtures/apps/redisapp-weakdependent/config/config.js +++ b/test/fixtures/apps/redisapp-weakdependent/config/config.js @@ -1,5 +1,3 @@ -'use strict'; - exports.redis = { client: { weakDependent: true, diff --git a/test/fixtures/apps/redisapp/app/controller/home.js b/test/fixtures/apps/redisapp/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redisapp/app/controller/home.js +++ b/test/fixtures/apps/redisapp/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/redisapp/config/config.js b/test/fixtures/apps/redisapp/config/config.js index 3a9c005..b28e997 100644 --- a/test/fixtures/apps/redisapp/config/config.js +++ b/test/fixtures/apps/redisapp/config/config.js @@ -1,5 +1,3 @@ -'use strict'; - exports.redis = { client: { host: '127.0.0.1', @@ -7,7 +5,7 @@ exports.redis = { password: '', db: '0', }, - agent:true, + agent: true, }; exports.logger = { diff --git a/test/fixtures/apps/redisclusterapp/app/controller/home.js b/test/fixtures/apps/redisclusterapp/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redisclusterapp/app/controller/home.js +++ b/test/fixtures/apps/redisclusterapp/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/redispathapp/app/controller/home.js b/test/fixtures/apps/redispathapp/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redispathapp/app/controller/home.js +++ b/test/fixtures/apps/redispathapp/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/redissentinelapp/app/controller/home.js b/test/fixtures/apps/redissentinelapp/app/controller/home.js index 13ccead..d77ceb9 100644 --- a/test/fixtures/apps/redissentinelapp/app/controller/home.js +++ b/test/fixtures/apps/redissentinelapp/app/controller/home.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { return class HomeController extends app.Controller { - * index() { + async index() { const { ctx, app } = this; - yield app.redis.set('foo', 'bar'); - ctx.body = yield app.redis.get('foo'); + await app.redis.set('foo', 'bar'); + ctx.body = await app.redis.get('foo'); } }; }; diff --git a/test/fixtures/apps/ts-multi/redisapp-ts/app/controller/home.ts b/test/fixtures/apps/ts-multi/redisapp-ts/app/controller/home.ts index ca8071a..afb2f60 100644 --- a/test/fixtures/apps/ts-multi/redisapp-ts/app/controller/home.ts +++ b/test/fixtures/apps/ts-multi/redisapp-ts/app/controller/home.ts @@ -1,17 +1,23 @@ -import {Controller, Singleton} from 'egg'; +import { Controller, Singleton } from 'egg'; import { Redis } from 'ioredis'; declare module 'egg' { - interface IController { - home: HomeController; - } + interface IController { + home: HomeController; + } + + interface EggApplicationCore { + redis: Redis & Singleton; + } +} + +export default class HomeController extends Controller { + async index() { + const { ctx,app } = this; + const redis = app.redis.get('cache') as unknown as Redis; + await redis.set('foo', 'bar'); + const redis2 = app.redis.getSingletonInstance('cache'); + await redis2.set('foo2', 'bar2'); + ctx.body = await redis.get('foo'); } - - export default class HomeController extends Controller { - async index() { - const { ctx,app } = this; - const redis = (app.redis as Singleton).get('cache'); - await redis.set('foo', 'bar'); - ctx.body = await redis.get('foo'); - } } diff --git a/test/fixtures/apps/ts-multi/tsconfig.json b/test/fixtures/apps/ts-multi/tsconfig.json index 75cb28f..ff41b73 100644 --- a/test/fixtures/apps/ts-multi/tsconfig.json +++ b/test/fixtures/apps/ts-multi/tsconfig.json @@ -1,16 +1,10 @@ { - "compilerOptions": { - "target": "es2017", - "module": "commonjs", - "baseUrl": ".", - "strict": true, - "lib": [ - "es2017" - ], - "skipLibCheck": false, - "noImplicitAny": false - }, - "include": [ - "../../../../**/*" - ] + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" } +} diff --git a/test/fixtures/apps/ts/redisapp-ts/app/controller/home.ts b/test/fixtures/apps/ts/redisapp-ts/app/controller/home.ts index 8835abc..d73af40 100644 --- a/test/fixtures/apps/ts/redisapp-ts/app/controller/home.ts +++ b/test/fixtures/apps/ts/redisapp-ts/app/controller/home.ts @@ -1,16 +1,22 @@ -import { Controller } from 'egg'; +import { Controller, Singleton } from 'egg'; +import { Redis } from 'ioredis'; declare module 'egg' { interface IController { home: HomeController; } + + interface EggApplicationCore { + redis: Redis & Singleton; + } } export default class HomeController extends Controller { async index() { - const { ctx,app } = this; - const redis = app.redis + const { ctx, app } = this; + const redis = app.redis; await redis.set('foo', 'bar'); - ctx.body = await redis.get('foo'); + const cacheValue = await redis.get('foo'); + ctx.body = cacheValue; } } diff --git a/test/fixtures/apps/ts/redisapp-ts/tsconfig.json b/test/fixtures/apps/ts/redisapp-ts/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/test/fixtures/apps/ts/redisapp-ts/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/test/fixtures/apps/ts/tsconfig.json b/test/fixtures/apps/ts/tsconfig.json deleted file mode 100644 index 75cb28f..0000000 --- a/test/fixtures/apps/ts/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "es2017", - "module": "commonjs", - "baseUrl": ".", - "strict": true, - "lib": [ - "es2017" - ], - "skipLibCheck": false, - "noImplicitAny": false - }, - "include": [ - "../../../../**/*" - ] - } diff --git a/test/redis.test.js b/test/redis.test.ts similarity index 64% rename from test/redis.test.js rename to test/redis.test.ts index b6dac68..5ec1b86 100644 --- a/test/redis.test.js +++ b/test/redis.test.ts @@ -1,13 +1,34 @@ -'use strict'; - -const mm = require('egg-mock'); -const request = require('supertest'); -const path = require('path'); -const compile = require('child_process'); +import compile from 'node:child_process'; +import path from 'node:path'; +import { mm, MockApplication } from '@eggjs/mock'; +import snapshot from 'snap-shot-it'; describe('test/redis.test.js', () => { + describe('default config', () => { + let app: MockApplication; + before(async () => { + app = mm.app({ + baseDir: 'apps/redisapp-default', + }); + await app.ready(); + }); + after(() => app.close()); + afterEach(mm.restore); + + it('should make default config stable', () => { + snapshot(app.config.redis); + }); + + it('should query', () => { + return app.httpRequest() + .get('/') + .expect(200) + .expect('bar'); + }); + }); + describe('single client', () => { - let app; + let app: MockApplication; before(async () => { app = mm.app({ baseDir: 'apps/redisapp', @@ -18,7 +39,7 @@ describe('test/redis.test.js', () => { afterEach(mm.restore); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); @@ -26,7 +47,7 @@ describe('test/redis.test.js', () => { }); describe('weak dependent', () => { - let app; + let app: MockApplication; before(async () => { app = mm.app({ baseDir: 'apps/redisapp-weakdependent', @@ -37,7 +58,7 @@ describe('test/redis.test.js', () => { afterEach(mm.restore); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); @@ -45,7 +66,7 @@ describe('test/redis.test.js', () => { }); describe('single client supportTimeCommand = false', () => { - let app; + let app: MockApplication; before(async () => { app = mm.app({ baseDir: 'apps/redisapp-supportTimeCommand-false', @@ -56,7 +77,7 @@ describe('test/redis.test.js', () => { afterEach(mm.restore); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); @@ -64,7 +85,7 @@ describe('test/redis.test.js', () => { }); describe('single client with customize ioredis', () => { - let app; + let app: MockApplication; before(async () => { app = mm.app({ baseDir: 'apps/redisapp-customize', @@ -75,31 +96,37 @@ describe('test/redis.test.js', () => { afterEach(mm.restore); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); }); }); - describe('single client for ts', () => { - let app; + let app: MockApplication; + const destPath = path.resolve('./test/fixtures/apps/ts/redisapp-ts'); + const compilerPath = path.resolve('./node_modules/typescript/bin/tsc'); + before(async () => { // Add new dynamic compiler to compile from ts to js - const destPath = path.resolve('./test/fixtures/apps/ts'); - const compilerPath = path.resolve('./node_modules/typescript/bin/tsc'); - compile.execSync(`node ${compilerPath} -p ${destPath}`); + compile.execSync(`node ${compilerPath} -p ${destPath}`, { + cwd: destPath, + stdio: 'inherit', + }); app = mm.app({ baseDir: 'apps/ts/redisapp-ts', }); await app.ready(); }); - after(() => app.close()); - afterEach(mm.restore); + after(async () => { + // cleanup + compile.execSync(`node ${compilerPath} --build --clean`); + await app?.close(); + }); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); @@ -107,22 +134,25 @@ describe('test/redis.test.js', () => { }); describe('multi client for ts', () => { - let app; + let app: MockApplication; + const destPath = path.resolve('./test/fixtures/apps/ts-multi'); + const compilerPath = path.resolve('./node_modules/typescript/bin/tsc'); before(async () => { // Add new dynamic compiler to compile from ts to js - const destPath = path.resolve('./test/fixtures/apps/ts-multi'); - const compilerPath = path.resolve('./node_modules/typescript/bin/tsc'); compile.execSync(`node ${compilerPath} -p ${destPath}`); app = mm.app({ baseDir: 'apps/ts-multi/redisapp-ts', }); await app.ready(); }); - after(() => app.close()); - afterEach(mm.restore); + after(async () => { + // cleanup + compile.execSync(`node ${compilerPath} --build --clean`); + await app?.close(); + }); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); @@ -131,7 +161,7 @@ describe('test/redis.test.js', () => { // TODO: make github action support sentinel describe.skip('redis sentinel', () => { - let app; + let app: MockApplication; before(async () => { app = mm.app({ baseDir: 'apps/redissentinelapp', @@ -142,7 +172,7 @@ describe('test/redis.test.js', () => { afterEach(mm.restore); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); @@ -150,7 +180,7 @@ describe('test/redis.test.js', () => { }); describe('await ready event', () => { - let app; + let app: MockApplication; before(async () => { app = mm.app({ baseDir: 'apps/redisapp-disable-offline-queue', @@ -161,7 +191,7 @@ describe('test/redis.test.js', () => { afterEach(mm.restore); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); @@ -170,7 +200,7 @@ describe('test/redis.test.js', () => { // TODO: make github action support redis start with path describe.skip('redis path', () => { - let app; + let app: MockApplication; before(async () => { app = mm.app({ baseDir: 'apps/redispathapp', @@ -181,7 +211,7 @@ describe('test/redis.test.js', () => { afterEach(mm.restore); it('should query', () => { - return request(app.callback()) + return app.httpRequest() .get('/') .expect(200) .expect('bar'); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +}