Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions lib/cache.providers.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -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<CacheManagerOptions, 'stores'>,
): Promise<Keyv> => {
): Promise<Keyv | Cacheable> => {
// If it's a Cacheable instance, return it directly to preserve nonBlocking mode
if (isCacheable(store)) {
return store;
}
if (store instanceof Keyv) {
return store;
}
Expand All @@ -38,7 +53,7 @@ export function createCacheManager(): Provider {
const cacheManager = stores
? createCache({
...options,
stores,
stores: stores as Keyv[],
})
: createCache({
ttl: options.ttl,
Expand Down
9 changes: 6 additions & 3 deletions lib/interfaces/cache-manager.interface.ts
Original file line number Diff line number Diff line change
@@ -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<CreateCacheOptions, 'stores'> {
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.
Expand Down
6 changes: 3 additions & 3 deletions lib/interfaces/cache-module.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ export interface CacheOptionsFactory<
export interface CacheModuleAsyncOptions<
StoreConfig extends Record<any, any> = Record<string, any>,
> extends ConfigurableModuleAsyncOptions<
CacheOptions<StoreConfig>,
keyof CacheOptionsFactory
> {
CacheOptions<StoreConfig>,
keyof CacheOptionsFactory
> {
/**
* Injection token resolving to an existing provider. The provider must implement
* the `CacheOptionsFactory` interface.
Expand Down
35 changes: 35 additions & 0 deletions tests/e2e/cacheable-nonblocking.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});

Original file line number Diff line number Diff line change
@@ -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<unknown> {
const value = await this.cacheManager.get('cacheable-key');
if (!value) {
await this.cacheManager.set('cacheable-key', 'cacheable-value');
}
return value;
}
}

30 changes: 30 additions & 0 deletions tests/src/cacheable-nonblocking/cacheable-nonblocking.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}