diff --git a/index.test.ts b/index.test.ts index 6119751..a5c7e8d 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,28 +1,45 @@ import { createServer } from 'node:http'; import * as de from 'descript'; -import { Redis } from 'ioredis'; +import { createSentinel } from '@redis/client'; import type { Mock } from 'vitest'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { Cache } from './index'; import type { Server } from 'http'; -vi.mock('ioredis', () => { - const Redis = vi.fn(); - Redis.prototype.get = vi.fn().mockResolvedValue(undefined); - Redis.prototype.set = vi.fn().mockResolvedValue(undefined); +vi.mock('@redis/client', () => { + const createSentinel = vi.fn(() => { + return { + 'get': vi.fn().mockResolvedValue(undefined), + 'set': vi.fn().mockResolvedValue(undefined), + on() { return this;}, + connect() {return this;}, + }; + }); return { - Redis, + createSentinel, }; }); -const redisGet = Redis.prototype.get as Mock; -const redisSet = Redis.prototype.set as Mock; - +let redisGet: Mock; +let redisSet: Mock; let cache: Cache; let server: Server; beforeEach(() => { + const client = createSentinel({ + name: 'sentinel-db', + sentinelRootNodes: [{ + host: 'example', + port: 1234, + }], + masterPoolSize: 10, + replicaPoolSize: 10, + }); + + redisGet = client.get as Mock; + redisSet = client.set as Mock; + cache = new Cache({ - redis: {}, + client, }); server = createServer((req, res) => { @@ -56,16 +73,13 @@ it('de.func', async() => { expect(redisGet).toHaveBeenCalledOnce(); expect(redisGet).toHaveBeenCalledWith( 'e33d0afae8e08752efa1a467653931ae9ba60c6a3ea693e684a6a56ef3b18ba3c7e711edee33f99471d9bb2d02302e92512cc3c8513ab473bbe71d52b8f7e39a', - expect.anything(), ); expect(redisSet).toHaveBeenCalledOnce(); expect(redisSet).toHaveBeenCalledWith( 'e33d0afae8e08752efa1a467653931ae9ba60c6a3ea693e684a6a56ef3b18ba3c7e711edee33f99471d9bb2d02302e92512cc3c8513ab473bbe71d52b8f7e39a', '"somevalue"', - 'EX', - 86400, - expect.anything(), + { expiration: { 'type': 'EX', value: 86400 } }, ); }); @@ -87,15 +101,12 @@ it('de.http', async() => { expect(redisGet).toHaveBeenCalledOnce(); expect(redisGet).toHaveBeenCalledWith( 'e33d0afae8e08752efa1a467653931ae9ba60c6a3ea693e684a6a56ef3b18ba3c7e711edee33f99471d9bb2d02302e92512cc3c8513ab473bbe71d52b8f7e39a', - expect.anything(), ); expect(redisSet).toHaveBeenCalledOnce(); expect(redisSet).toHaveBeenCalledWith( 'e33d0afae8e08752efa1a467653931ae9ba60c6a3ea693e684a6a56ef3b18ba3c7e711edee33f99471d9bb2d02302e92512cc3c8513ab473bbe71d52b8f7e39a', '{"statusCode":200,"headers":{"date":"Thu, 01 Jan 1970 00:00:00 GMT","connection":"keep-alive","keep-alive":"timeout=5","content-length":"15"},"result":"some http value"}', - 'EX', - 86400, - expect.anything(), + { expiration: { 'type': 'EX', value: 86400 } }, ); }); diff --git a/index.ts b/index.ts index 746e3aa..1ab613a 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,9 @@ import { hash } from 'node:crypto'; import type { CacheInterface, LoggerInterface } from 'descript'; import { error as deError } from 'descript'; -import type { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis'; -import { Cluster, Redis } from 'ioredis'; +import type { RedisClientType, RedisClusterType, RedisSentinelType } from '@redis/client'; + +type RedisClient = RedisClientType | RedisSentinelType | RedisClusterType; export interface Options { /** key TTL in seconds (default: 60 * 60 * 24) */ @@ -11,10 +12,8 @@ export interface Options { generation?: number; /** read timeout in milliseconds (default: 100) */ readTimeout?: number; - /** use two clients (reader and writer) with Sentinel (default: false) */ - useReaderAndWriterWithSentinel?: boolean; - /** redis config */ - redis: RedisOptions | { startupNodes: ClusterNode[], options?: ClusterOptions }; + /** @redis/client */ + client: RedisClient; } interface InnerOptions extends Options { @@ -110,8 +109,7 @@ export type LoggerEvent = ( ); export class Cache implements CacheInterface { - #writer: Cluster | Redis; - #reader: Cluster | Redis; + #client: RedisClient; #logger?: Logger; #options: InnerOptions; @@ -124,33 +122,7 @@ export class Cache implements CacheInterface { }; this.#logger = logger; - - if ('startupNodes' in this.#options.redis) { - this.#reader = new Cluster( - this.#options.redis.startupNodes, - this.#options.redis.options, - ); - this.#writer = this.#reader; - } else { - if (this.#options.useReaderAndWriterWithSentinel) { - // Client for write (always on master) - this.#writer = new Redis({ - ...this.#options.redis, - role: 'master', - }); - - // Client for read (replica, only read-only commands) - this.#reader = new Redis({ - ...this.#options.redis, - role: 'slave', - readOnly: true, - }); - } else { - this.#reader = new Redis(this.#options.redis); - this.#writer = this.#reader; - } - - } + this.#client = options.client; this.#log({ 'type': EVENT.REDIS_CACHE_INITIALIZED, @@ -158,13 +130,6 @@ export class Cache implements CacheInterface { }); } - getClient() { - return { - reader: this.#reader, - writer: this.#writer, - }; - } - get({ key }: { key: string }): Promise { const normalizedKey = this.#normalizeKey(key); @@ -196,43 +161,30 @@ export class Cache implements CacheInterface { })); }, this.#options.readTimeout); - this.#reader.get(normalizedKey, (error, data) => { - if (isTimeout) { - return; - } - - clearTimeout(timer); + this.#client.get(normalizedKey) + .then((data) => { + clearTimeout(timer); + if (isTimeout) { + return; + } - if (error) { - this.#log({ - 'type': EVENT.REDIS_CACHE_READ_ERROR, - error, - key, - normalizedKey, - timers: { - start, - end: Date.now(), - }, - }); + if (!data) { + this.#log({ + 'type': EVENT.REDIS_CACHE_READ_KEY_NOT_FOUND, + key, + normalizedKey, + timers: { + start, + end: Date.now(), + }, + }); - reject(deError({ - id: EVENT.REDIS_CACHE_READ_ERROR, - })); - } else if (!data) { - this.#log({ - 'type': EVENT.REDIS_CACHE_READ_KEY_NOT_FOUND, - key, - normalizedKey, - timers: { - start, - end: Date.now(), - }, - }); + reject(deError({ + id: EVENT.REDIS_CACHE_READ_KEY_NOT_FOUND, + })); + return; + } - reject(deError({ - id: EVENT.REDIS_CACHE_READ_KEY_NOT_FOUND, - })); - } else { let parsedValue; try { parsedValue = JSON.parse(data); @@ -267,8 +219,24 @@ export class Cache implements CacheInterface { }); resolve(parsedValue); - } - }); + }) + .catch((error) => { + clearTimeout(timer); + + this.#log({ + 'type': EVENT.REDIS_CACHE_READ_ERROR, + error, + key, + normalizedKey, + timers: { + start, + end: Date.now(), + }, + }); + reject(deError({ + id: EVENT.REDIS_CACHE_READ_ERROR, + })); + }); }); } @@ -308,12 +276,29 @@ export class Cache implements CacheInterface { return; } + // https://redis.io/docs/latest/commands/getex/ // maxage - seconds - this.#writer.set(normalizedKey, json, 'EX', maxage, (error, done) => { - if (error) { + this.#client.set(normalizedKey, json, { expiration: { 'type': 'EX', value: maxage } }) + .then(( done) => { + if (!done) { + this.#log({ + 'type': EVENT.REDIS_CACHE_WRITE_FAILED, + key, + normalizedKey, + timers: { + start, + end: Date.now(), + }, + }); + reject(deError({ + id: EVENT.REDIS_CACHE_WRITE_FAILED, + })); + return; + } + this.#log({ - 'type': EVENT.REDIS_CACHE_WRITE_ERROR, - error, + 'type': EVENT.REDIS_CACHE_WRITE_DONE, + data: json, key, normalizedKey, timers: { @@ -321,12 +306,12 @@ export class Cache implements CacheInterface { end: Date.now(), }, }); - reject(deError({ - id: EVENT.REDIS_CACHE_WRITE_ERROR, - })); - } else if (!done) { + resolve(); + }) + .catch((error) => { this.#log({ - 'type': EVENT.REDIS_CACHE_WRITE_FAILED, + 'type': EVENT.REDIS_CACHE_WRITE_ERROR, + error, key, normalizedKey, timers: { @@ -335,22 +320,9 @@ export class Cache implements CacheInterface { }, }); reject(deError({ - id: EVENT.REDIS_CACHE_WRITE_FAILED, + id: EVENT.REDIS_CACHE_WRITE_ERROR, })); - } else { - this.#log({ - 'type': EVENT.REDIS_CACHE_WRITE_DONE, - data: json, - key, - normalizedKey, - timers: { - start, - end: Date.now(), - }, - }); - resolve(); - } - }); + }); }); } diff --git a/package-lock.json b/package-lock.json index 9bd3138..d6a473e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "4.1.1", "license": "MIT", "dependencies": { - "@stylistic/eslint-plugin": "^5.3.1", - "ioredis": "^5.7.0" + "@redis/client": "^5.8.2", + "@stylistic/eslint-plugin": "^5.3.1" }, "devDependencies": { "@eslint/js": "^9.14.0", @@ -956,12 +956,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@ioredis/commands": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz", - "integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==", - "license": "MIT" - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1024,6 +1018,18 @@ "node": ">= 8" } }, + "node_modules/@redis/client": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.2.tgz", + "integrity": "sha512-WtMScno3+eBpTac1Uav2zugXEoXqaU23YznwvFgkPwBQVwEHTDgOG7uEAObtZ/Nyn8SmAMbqkEubJaMOvnqdsQ==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.50.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz", @@ -1968,15 +1974,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "license": "MIT" }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/descript": { "version": "4.0.16", "resolved": "https://registry.npmjs.org/descript/-/descript-4.0.16.tgz", @@ -2447,30 +2444,6 @@ "node": ">=0.8.19" } }, - "node_modules/ioredis": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", - "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "^1.3.0", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2582,18 +2555,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2870,27 +2831,6 @@ ], "license": "MIT" }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3034,12 +2974,6 @@ "dev": true, "license": "MIT" }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", diff --git a/package.json b/package.json index 0b9ec29..e3e9eca 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "build" ], "dependencies": { - "@stylistic/eslint-plugin": "^5.3.1", - "ioredis": "^5.7.0" + "@redis/client": "^5.8.2", + "@stylistic/eslint-plugin": "^5.3.1" }, "peerDependencies": { "descript": ">=4"