From 6bfe11e1ac6bd233341746342935a1f7708fa5bb Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Thu, 11 Dec 2025 09:40:41 +0200 Subject: [PATCH 1/4] feat(cache): add cacheable type support to stores option --- lib/interfaces/cache-manager.interface.ts | 9 ++++++--- lib/interfaces/cache-module.interface.ts | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/interfaces/cache-manager.interface.ts b/lib/interfaces/cache-manager.interface.ts index f762902c..b72e9513 100644 --- a/lib/interfaces/cache-manager.interface.ts +++ b/lib/interfaces/cache-manager.interface.ts @@ -1,19 +1,22 @@ import { CreateCacheOptions } from 'cache-manager'; import { Keyv, KeyvStoreAdapter } from 'keyv'; +import type { Cacheable } from 'cacheable'; /** * Interface defining Cache Manager configuration options. * * @publicApi */ -export interface CacheManagerOptions - extends Omit { +export interface CacheManagerOptions extends Omit< + CreateCacheOptions, + 'stores' +> { /** * Cache storage manager. Default is `'memory'` (in-memory store). See * [Different stores](https://docs.nestjs.com/techniques/caching#different-stores) * for more info. */ - stores?: Keyv | KeyvStoreAdapter | (Keyv | KeyvStoreAdapter)[]; + stores?: Keyv | KeyvStoreAdapter | Cacheable | (Keyv | KeyvStoreAdapter)[]; /** * Cache storage namespace, default is `keyv`. * This is a global configuration that applies to all `KeyvStoreAdapter` instances. diff --git a/lib/interfaces/cache-module.interface.ts b/lib/interfaces/cache-module.interface.ts index 9e593104..6c62cccf 100644 --- a/lib/interfaces/cache-module.interface.ts +++ b/lib/interfaces/cache-module.interface.ts @@ -43,9 +43,9 @@ export interface CacheOptionsFactory< export interface CacheModuleAsyncOptions< StoreConfig extends Record = Record, > extends ConfigurableModuleAsyncOptions< - CacheOptions, - keyof CacheOptionsFactory - > { + CacheOptions, + keyof CacheOptionsFactory +> { /** * Injection token resolving to an existing provider. The provider must implement * the `CacheOptionsFactory` interface. From c1da524d36ac8f9f3580b507045944ee9a01ce7a Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Thu, 11 Dec 2025 09:41:01 +0200 Subject: [PATCH 2/4] feat(cache): preserve cacheable instances in caching factory --- lib/cache.providers.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/cache.providers.ts b/lib/cache.providers.ts index 621920d3..cd46ee32 100644 --- a/lib/cache.providers.ts +++ b/lib/cache.providers.ts @@ -1,10 +1,21 @@ import { Provider } from '@nestjs/common'; import { createCache } from 'cache-manager'; import Keyv, { type KeyvStoreAdapter } from 'keyv'; +import type { Cacheable } from 'cacheable'; import { CACHE_MANAGER } from './cache.constants'; import { MODULE_OPTIONS_TOKEN } from './cache.module-definition'; import { CacheManagerOptions } from './interfaces/cache-manager.interface'; +function isCacheable(store: any): store is Cacheable { + return ( + store && + typeof store === 'object' && + 'primary' in store && + 'secondary' in store && + 'nonBlocking' in store + ); +} + /** * Creates a CacheManager Provider. * @@ -15,9 +26,13 @@ export function createCacheManager(): Provider { provide: CACHE_MANAGER, useFactory: async (options: CacheManagerOptions) => { const cachingFactory = async ( - store: Keyv | KeyvStoreAdapter, + store: Keyv | KeyvStoreAdapter | Cacheable, options: Omit, - ): Promise => { + ): Promise => { + // If it's a Cacheable instance, return it directly to preserve nonBlocking mode + if (isCacheable(store)) { + return store; + } if (store instanceof Keyv) { return store; } @@ -38,7 +53,7 @@ export function createCacheManager(): Provider { const cacheManager = stores ? createCache({ ...options, - stores, + stores: stores as Keyv[], }) : createCache({ ttl: options.ttl, From 26f7ab95a8af24405b1406f6b42029f112501870 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Thu, 11 Dec 2025 09:41:20 +0200 Subject: [PATCH 3/4] test(cache): add e2e tests for cacheable instance support --- tests/e2e/cacheable-nonblocking.spec.ts | 35 ++++++++++++++++ .../cacheable-nonblocking.controller.ts | 17 ++++++++ .../cacheable-nonblocking.module.ts | 30 ++++++++++++++ tests/src/cacheable-nonblocking/tsconfig.json | 41 +++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 tests/e2e/cacheable-nonblocking.spec.ts create mode 100644 tests/src/cacheable-nonblocking/cacheable-nonblocking.controller.ts create mode 100644 tests/src/cacheable-nonblocking/cacheable-nonblocking.module.ts create mode 100644 tests/src/cacheable-nonblocking/tsconfig.json diff --git a/tests/e2e/cacheable-nonblocking.spec.ts b/tests/e2e/cacheable-nonblocking.spec.ts new file mode 100644 index 00000000..738cb619 --- /dev/null +++ b/tests/e2e/cacheable-nonblocking.spec.ts @@ -0,0 +1,35 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { Server } from 'net'; +import request from 'supertest'; +import { CACHE_MANAGER } from '../../lib'; +import { CacheableNonBlockingModule } from '../src/cacheable-nonblocking/cacheable-nonblocking.module'; + +describe('Caching with Cacheable nonBlocking', () => { + let server: Server; + let app: INestApplication; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [CacheableNonBlockingModule], + }).compile(); + + app = module.createNestApplication(); + server = app.getHttpServer(); + await app.init(); + }); + + it(`should return empty on first call`, async () => { + return request(server).get('/').expect(200, ''); + }); + + it(`should return cached data on second call`, async () => { + return request(server).get('/').expect(200, 'cacheable-value'); + }); + + afterAll(async () => { + await app.get(CACHE_MANAGER).clear(); + await app.close(); + }); +}); + diff --git a/tests/src/cacheable-nonblocking/cacheable-nonblocking.controller.ts b/tests/src/cacheable-nonblocking/cacheable-nonblocking.controller.ts new file mode 100644 index 00000000..218895a4 --- /dev/null +++ b/tests/src/cacheable-nonblocking/cacheable-nonblocking.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, Inject } from '@nestjs/common'; +import { Cache, CACHE_MANAGER } from '../../../lib'; + +@Controller() +export class CacheableNonBlockingController { + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} + + @Get() + async getFromCacheable(): Promise { + const value = await this.cacheManager.get('cacheable-key'); + if (!value) { + await this.cacheManager.set('cacheable-key', 'cacheable-value'); + } + return value; + } +} + diff --git a/tests/src/cacheable-nonblocking/cacheable-nonblocking.module.ts b/tests/src/cacheable-nonblocking/cacheable-nonblocking.module.ts new file mode 100644 index 00000000..36cbf40b --- /dev/null +++ b/tests/src/cacheable-nonblocking/cacheable-nonblocking.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { CacheModule } from '../../../lib'; +import { CacheableNonBlockingController } from './cacheable-nonblocking.controller'; +import KeyvRedis from '@keyv/redis'; +import { Keyv } from 'keyv'; +import { Cacheable, CacheableMemory } from 'cacheable'; + +@Module({ + imports: [ + CacheModule.registerAsync({ + useFactory: async () => { + const cacheable = new Cacheable({ + primary: new Keyv({ + store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }), + }), + secondary: new KeyvRedis('redis://localhost:6379'), + nonBlocking: true, + ttl: 30000, + namespace: 'test-cacheable', + }); + + return { + stores: cacheable, + }; + }, + }), + ], + controllers: [CacheableNonBlockingController], +}) +export class CacheableNonBlockingModule {} diff --git a/tests/src/cacheable-nonblocking/tsconfig.json b/tests/src/cacheable-nonblocking/tsconfig.json new file mode 100644 index 00000000..c0fc6ff3 --- /dev/null +++ b/tests/src/cacheable-nonblocking/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": true, + "allowJs": true, + "outDir": "./dist", + "paths": { + "@nestjs/common": ["../../packages/common"], + "@nestjs/common/*": ["../../packages/common/*"], + "@nestjs/core": ["../../packages/core"], + "@nestjs/core/*": ["../../packages/core/*"], + "@nestjs/microservices": ["../../packages/microservices"], + "@nestjs/microservices/*": ["../../packages/microservices/*"], + "@nestjs/websockets": ["../../packages/websockets"], + "@nestjs/websockets/*": ["../../packages/websockets/*"], + "@nestjs/testing": ["../../packages/testing"], + "@nestjs/testing/*": ["../../packages/testing/*"], + "@nestjs/platform-express": ["../../packages/platform-express"], + "@nestjs/platform-express/*": ["../../packages/platform-express/*"], + "@nestjs/platform-socket.io": ["../../packages/platform-socket.io"], + "@nestjs/platform-socket.io/*": ["../../packages/platform-socket.io/*"], + "@nestjs/platform-ws": ["../../packages/platform-ws"], + "@nestjs/platform-ws/*": ["../../packages/platform-ws/*"] + } + }, + "include": [ + "src/**/*", + "e2e/**/*" + ], + "exclude": [ + "node_modules", + ] +} + From 97b3e69c88b883b18fb682719f0995a3b98af7f6 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Thu, 11 Dec 2025 10:39:06 +0200 Subject: [PATCH 4/4] test(cache): remove redundant tsconfig.json from test directory --- tests/src/cacheable-nonblocking/tsconfig.json | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 tests/src/cacheable-nonblocking/tsconfig.json diff --git a/tests/src/cacheable-nonblocking/tsconfig.json b/tests/src/cacheable-nonblocking/tsconfig.json deleted file mode 100644 index c0fc6ff3..00000000 --- a/tests/src/cacheable-nonblocking/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": false, - "noImplicitAny": false, - "removeComments": true, - "noLib": false, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "es6", - "sourceMap": true, - "allowJs": true, - "outDir": "./dist", - "paths": { - "@nestjs/common": ["../../packages/common"], - "@nestjs/common/*": ["../../packages/common/*"], - "@nestjs/core": ["../../packages/core"], - "@nestjs/core/*": ["../../packages/core/*"], - "@nestjs/microservices": ["../../packages/microservices"], - "@nestjs/microservices/*": ["../../packages/microservices/*"], - "@nestjs/websockets": ["../../packages/websockets"], - "@nestjs/websockets/*": ["../../packages/websockets/*"], - "@nestjs/testing": ["../../packages/testing"], - "@nestjs/testing/*": ["../../packages/testing/*"], - "@nestjs/platform-express": ["../../packages/platform-express"], - "@nestjs/platform-express/*": ["../../packages/platform-express/*"], - "@nestjs/platform-socket.io": ["../../packages/platform-socket.io"], - "@nestjs/platform-socket.io/*": ["../../packages/platform-socket.io/*"], - "@nestjs/platform-ws": ["../../packages/platform-ws"], - "@nestjs/platform-ws/*": ["../../packages/platform-ws/*"] - } - }, - "include": [ - "src/**/*", - "e2e/**/*" - ], - "exclude": [ - "node_modules", - ] -} -