cache-manager icon indicating copy to clipboard operation
cache-manager copied to clipboard

SWC builder causes cacheFactory is always create a new instance of KeyV

Open yawhide opened this issue 6 months ago • 7 comments
trafficstars

Is there an existing issue for this?

  • [x] I have searched the existing issues

Current behavior

Using swc as the compiler causes @nestjs/cache-manager's cache.provider.js' cachingFactory to always use the else case

function createCacheManager() {
    return {
        provide: cache_constants_1.CACHE_MANAGER,
        useFactory: async (options) => {
            const cachingFactory = async (store, options) => {
                if (store instanceof keyv_1.default) { // <--------- this line is always false even if i am passing in an instance of KeyV as the store
                    return store;
                }
                return new keyv_1.default({
                    store,
                    ttl: options.ttl,
                    namespace: options.namespace,
                });
            };
...

I am not sure why this is the case 🤔

What is unfortunate about this behaviour is I cannot follow the examples in the nestjs documentation. I have to find out the hard way on how to get @nestjs/cache-manager to use @keyv/redis properly. What ends up happening is, even if your store is an instance of KeyV, it'll get wrapped by another instance KeyV and the options wont be passed in properly and wont use @keyv/redis as the underlying cache store

This is the nestjs example i am talking about:


import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';
import { createKeyv } from '@keyv/redis';
import { Keyv } from 'keyv';
import { CacheableMemory } from 'cacheable';

@Module({
  imports: [
    CacheModule.registerAsync({
      useFactory: async () => {
        return {
          stores: [
            new Keyv({
              store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }),
            }),
            createKeyv('redis://localhost:6379'),
          ],
        };
      },
    }),
  ],
  controllers: [AppController],
})
export class AppModule {}

Minimum reproduction code

https://github.com/yawhide/nestjs-cache-manager

Steps to reproduce

clone my repo npm install

store !== keyv_1.default

npm run start:debug put a breakpoint on line 21 in node_modules/@nestjs/cache-manager/dist/cache.providers.js (if (store instanceof keyv_1.default) {)

observe that this line will always be false.

store === keyv_1.default

delete "builder": "swc" in nest-cli.json

npm run start:debug put a breakpoint on line 21 in node_modules/@nestjs/cache-manager/dist/cache.providers.js (if (store instanceof keyv_1.default) {)

observe that this line will be true

Expected behavior

i can use swc as the builder and when I pass in an instance of Keyv, @nestjs/manager wont wrap it in another instance of KeyV

Package version

3.0.1

NestJS version

11.1.0

Node.js version

20.19.0

In which operating systems have you tested?

  • [x] macOS
  • [ ] Windows
  • [ ] Linux

Other

No response

yawhide avatar May 09 '25 19:05 yawhide

I am not sure why this is the case 🤔

that may be related with having multiple keyv packages loaded in your app. Check that out! With NPM it would be:

npm ls keyv

micalevisk avatar May 09 '25 19:05 micalevisk

i thought so too! but unfortunately, i dont think that is the case because when i was in the debugger stepping through @nestjs/cache-manager and keyv, i would check what version each package is and it was consistent: 5.3.3 for keyv and 3.0.1 for nestjs/cache-manager.

if i build with swc, store !== keyv_1.default. If i build with typescript, store === keyv_1.default

Could it be something to do with commonjs vs esm?

yawhide avatar May 09 '25 19:05 yawhide

As this is not supposed to be a package manager related issue, can you please update the reproduction to use NPM instead? then make sure that the commands are working as you said in the steps to reproduce

micalevisk avatar May 09 '25 19:05 micalevisk

As this is not supposed to be a package manager related issue, can you please update the reproduction to use NPM instead? then make sure that the commands are working as you said in the steps to reproduce

I updated the steps to reproduce to use npm instead of yarn

yawhide avatar May 09 '25 20:05 yawhide

I'm getting this SWC error on npm run start:dev

Image

[Nest] 236146  - 05/09/2025, 4:10:11 PM   ERROR [ExceptionHandler] TypeError: Cannot read properties of undefined (reading 'includes')
    at /tmp/nestjs-cache-manager/node_modules/keyv/dist/index.cjs:437:123
    at Array.some (<anonymous>)
    at Keyv._checkIterableAdapter (/tmp/nestjs-cache-manager/node_modules/keyv/dist/index.cjs:437:84)
    at new Keyv (/tmp/nestjs-cache-manager/node_modules/keyv/dist/index.cjs:283:72)
    at cachingFactory (/tmp/nestjs-cache-manager/node_modules/@nestjs/cache-manager/dist/cache.providers.js:24:24)
    at /tmp/nestjs-cache-manager/node_modules/@nestjs/cache-manager/dist/cache.providers.js:31:65
    at Array.map (<anonymous>)
    at InstanceWrapper.useFactory [as metatype] (/tmp/nestjs-cache-manager/node_modules/@nestjs/cache-manager/dist/cache.providers.js:31:52)
    at Injector.instantiateClass (/tmp/nestjs-cache-manager/node_modules/@nestjs/core/injector/injector.js:376:55)
    at callback (/tmp/nestjs-cache-manager/node_modules/@nestjs/core/injector/injector.js:65:45)

same as https://github.com/jaredwray/keyv/issues/1300 I guess

so changing your code to:

            stores: [new KeyvRedis({
              socket: {
                host: "localhost",
                port: 6379,
              },
            })],

addresses that

micalevisk avatar May 09 '25 20:05 micalevisk

I've managed to reproduce that using the code snippet from the nestjs docs. It only breaks if we use SWC builder but I'm not sure if this is something that we can address on nestjs side to be honest

Image

micalevisk avatar May 09 '25 20:05 micalevisk

same as jaredwray/keyv#1300 I guess

so changing your code to:

        stores: [new KeyvRedis({
          socket: {
            host: "localhost",
            port: 6379,
          },
        })],

addresses that

Yeah, that has been my workaround as well.

I've managed to reproduce that using the code snippet from the nestjs docs. It only breaks if we use SWC builder but I'm not sure if this is something that we can address on nestjs side to be honest

if you think a fix is not possible, then maybe it is worth documenting the alternative way of using @nestjs/cache-manager where you pass in a KeyvRedis instance instead?

yawhide avatar May 10 '25 14:05 yawhide

any updates on this? Our caches seem to not be connecting to redis and silently defaulting to in mem cache.

scoobaSteve007 avatar May 30 '25 21:05 scoobaSteve007

@scoobaSteve007

@Module({
  imports: [
    CacheModule.registerAsync({
      useFactory: async () => {
        return {
          stores: [
            new Keyv({
              store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }),
            }),
            createKeyv('redis://localhost:6379'),
          ],
        };
      },
    }),
  ],
  controllers: [AppController],
})

this code use two cache store in-memory cache is default and redis is fallback

Documentation said...

In this example, we've registered two stores: CacheableMemory and KeyvRedis. The CacheableMemory store is a simple in-memory store, while KeyvRedis is a Redis store. The stores array is used to specify the stores you want to use. The first store in the array is the default store, and the rest are fallback stores.

yomapi avatar Jun 27 '25 04:06 yomapi

@yawhide I think, it is connected to ESM / CJS.

I'm encountering microsoft/typescript#62328 using @nestjs/cache-manager and @keyv/redis together in a way outlined in the docs. The reason for it is that keyv exports both ESM and CJS definitions, and while @keyv/redis uses ESM ones, @nestjs/cache-manager uses CJS ones, and as @andarist user pointed out, instances of those classes are not completely interchangeable (different module systems, different prototype chains).

The reason new KeyvRedis(…) works is because stores property accepts either Keyv instance (and Keyv is a class) or KeyvStoreAdapter-shaped object (and KeyvStoreAdapter is a type, not a class). Since class KeyvRedis implements KeyvStoreAdapter, even though KeyvStoreAdapter is defined twice (once per each module system), the type information is the same, and it doesn't exist at runtime, so there is no error here.

Proofs:

  • this breaks: https://tsplay.dev/m0jyrw
  • this works: https://tsplay.dev/wEKvbm

I think, the most durable way of fixing both issues (the OP's and mine) would be for @nestjs/cache-manager to start providing both CJS and ESM definitions. In the meantime, it's easy to just update the docs from createKeyv(…) to new KeyvRedis(…).

parzhitsky avatar Aug 25 '25 09:08 parzhitsky