diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a1655..1942555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -## [7.0.5] - 2025-11-14 +## [7.0.6] - 2026-01-19 + +### Changed + +- Switch back to redis, fix concurrent redis connection issues + +## [7.0.5] - 2026-01-14 ### Changed diff --git a/package.json b/package.json index 71d25b9..ef5fea3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-essentials-ts", - "version": "7.0.5", + "version": "7.0.6", "description": "A selection of the finest modules supporting authorization, API routing, error handling, logging and sending HTTP requests.", "main": "lib/index.js", "private": false, @@ -80,6 +80,6 @@ }, "peerDependencies": { "newrelic": "^12.0.0", - "ioredis": "^5.9.1" + "redis": "5.10.0" } } diff --git a/src/httpClient/redisStorage.ts b/src/httpClient/redisStorage.ts index d785c56..11d0315 100644 --- a/src/httpClient/redisStorage.ts +++ b/src/httpClient/redisStorage.ts @@ -1,25 +1,36 @@ import { buildStorage, canStale } from 'axios-cache-interceptor'; import type { StorageValue } from 'axios-cache-interceptor'; +// eslint-disable-next-line import/no-extraneous-dependencies +// @ts-ignore +import { createClient } from 'redis'; const KEY_PREFIX = 'axios-cache-'; const MIN_TTL = 60000; -export default function createRedisStorage(redisEndpoint: string) { - // eslint-disable-next-line import/no-extraneous-dependencies - const Redis = require('ioredis'); +export default function createRedisStorage(client: ReturnType) { + const connectionPromise: Promise = client.connect(); - const client = new Redis(redisEndpoint); + const connectIfNeeded = async () => { + if (client.isReady) { + return; + } + + await connectionPromise; + }; // source https://axios-cache-interceptor.js.org/guide/storages#node-redis-storage return buildStorage({ async find(key) { + await connectIfNeeded(); const result = await client.get(`${KEY_PREFIX}${key}`); return result ? (JSON.parse(result) as StorageValue) : undefined; }, // eslint-disable-next-line complexity async set(key, value, req) { + await connectIfNeeded(); + await client.set(`${KEY_PREFIX}${key}`, JSON.stringify(value), { PXAT: // We don't want to keep indefinitely values in the storage if @@ -38,6 +49,7 @@ export default function createRedisStorage(redisEndpoint: string) { }, async remove(key) { + await connectIfNeeded(); await client.del(`${KEY_PREFIX}${key}`); }, }); diff --git a/tests/httpClient/redisStorage.test.ts b/tests/httpClient/redisStorage.test.ts new file mode 100644 index 0000000..8699ef8 --- /dev/null +++ b/tests/httpClient/redisStorage.test.ts @@ -0,0 +1,47 @@ +jest.mock( + 'redis', + () => ({ + createClient: jest.fn(), + }), + { virtual: true }, +); + +import createRedisStorage from '../../src/httpClient/redisStorage'; + +const mClient = { + connect: jest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + mClient.isReady = true; + }), + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue('OK'), + del: jest.fn().mockResolvedValue(1), + isReady: false, + on: jest.fn(), +}; + +describe('redisStorage', () => { + beforeEach(() => { + jest.clearAllMocks(); + mClient.isReady = false; + }); + + it('should connect when not ready', async () => { + const storage = createRedisStorage(mClient as any); + mClient.isReady = false; + + await storage.get('key'); + + expect(mClient.connect).toHaveBeenCalledTimes(1); + expect(mClient.isReady).toBe(true); + }); + + it('should connect only once when multiple requests are made', async () => { + mClient.isReady = false; + const storage = createRedisStorage(mClient as any); + + await Promise.all([storage.get('key1'), storage.get('key2')]); + + expect(mClient.connect).toHaveBeenCalledTimes(1); + }); +});