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, 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. 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 {}