Code Monkey home page Code Monkey logo

nestjs-omacache's Introduction

NestJS-Omacache

omacache_logo
by talented designer, [email protected]

Motivation

Nest's cache-manager has limitations and inconveniences(i.e. cache applied for controller only). this problem arises from the fact that @nestjs/cache-manager implements features using interceptor, so its capabilities limited within interceptor's.

This package provides you full capabilities for most caching strategy on server.

It solves:

  1. enables partial caching during Request-Response cycle
  2. set-on-start caching(persistent, and can be refreshed)
  3. can be applied to both controllers and services(Injectable)
  4. key-based cache control. It gives you convenience to set and bust the cache using same key

Cache option type automatically switched by 'kind' option(persistent or temporal)

Usage

Import CacheModule

// root module
import { CacheModule } from 'nestjs-omacache'

@Module({
    // this import enables set-on-start caching
    imports: [CacheModule],
    ...
})
export class AppModule {}

Build your own Cache Decorator with storage

// imported 'Cache' is factory to receive storage that implements ICacheStorage
import { Cache, ICacheStorage } from "nestjs-omacache";

// for example, we can use redis for storage
// What ever your implementation is, it must satisfies ICacheStorage interface.
// set() ttl param is optional, but implementing signature including ttl is strongly recommended
class RedisStorage implements ICacheStorage {
    get(key: string) {...}
    set(key: string, val: any, ttl: number) {...}
    has(key: string) {...}
    delete(key: string) {...}
}

// Then you can make External Redis Cache!
const ExternalCache = Cache({ storage: new RedisStorage() })

// ...or
// you can just initialize it using default storage(in-memory cache)
// default storage is based on lru-cache package, so it can handle TTL and cache eviction
// default max size is 10000
// make sure you are not making memory overhead by using default in-memory storage
const InMemoryCache = Cache();

// you can implement your custom in-memory cache, which is more configurable.

Use it anywhere

// regardless class is Controller or Injectable, you can use produced cache decorator
@Controller()
class AppController {
    @Get()
    @ExternalCache({
        // persistent cache also needs key to control cache internally
        key: 'some key',
        // persistent cache sets cache automatically on server start
        kind: 'persistent',
        // refresh interval is optional
        // use it if you want cache refreshing
        refreshIntervalSec: 60 * 60 * 3 // 3 hours
    })
    async noParameterMethod() {
        ...
    }

    @Get('/:id')
    @ExternalCache({
        key: 'other key',
        kind: 'temporal',
        ttl: 10 * MIN, // 10 mins
        // You have to specify parameter indexes which will be referenced dynamically
        // In this case, cache key will be concatenated string of key, id param, q2 query
        paramIndex: [0, 2]
    })
    async haveParametersMethod(
        @Param('id') id: number,
        // q1 will not affect cache key because paramIndex is specified to refer param index 0 and 2
        @Query('query_1') q1: string,
        @Query('query_2') q2: string
    ) {
        ...
    }
}

Partial Caching

partial caching is particularly useful when an operation combined with cacheable and not cacheable jobs

// let's say SomeService have three methods: taskA, taskB, taskC
// assume that taskA and taskC can be cached, but taskB not
// each of task takes 1 second to complete

// in this scenario, @Nestjs/cache-manager can't handle caching because it's stick with interceptor
// but we can cover this case using partial caching
@Injectable()
class SomeService {

    @InMemoryCache(...)
    taskA() {} // originally takes 1 second

    // not cacheable
    taskB() {} // takes 1 second

    @InMemoryCache(...)
    taskC() {} // originally takes 1 second
}


@Controller()
class SomeController {
    constructor(
        private someService: SomeService
    ) {}

    // this route can take slightest time because taskA and taskC is partially cached
    // execution time can be reduced 3 seconds to 1 second
    @Get()
    route1() {
        someService.taskA(); // takes no time
        someService.taskB(); // still takes 1 second
        someService.taskC(); // takes no time
    }
}

Cache Busting

// we need to set same key to set & unset cache
// keep in mind that cache control by parameters is supported for temporal cache only
@Controller()
class SomeController {
    @Get()
    @InMemoryCache({
        key: 'hello',
        kind: 'persistent',
    })
    getSomethingHeavy() {
        ...
    }

    @Put()
    // in this case, we are busting persistent cache
    // after busting persistent cache, when busting method is done,
    // persistent cached method(getSomethingHeavy in this case) will invoked immediately
    // so you can still get the updated cache from persistent cache route!
    @InMemoryCache({
        key: 'hello',
        kind: 'bust',
    })
    updateSomethingHeavy() {
        ...
    }


    // this route sets cache for key 'some'
    @Get('/some')
    @InMemoryCache({
        key: 'some',
        kind:'temporal',
        ttl: 30 * SECOND, // 30 seconds
    })
    getSome() {
        ...
    }

    // and this route will unset cache for key 'some', before the 'some' cache's ttl expires
    @Patch('/some')
    @InMemoryCache({
        key: 'some',
        kind: 'bust',
    })
    updateSome() {
        ...
    }

    // above operation also can handle parameter based cache
    @Get('/:p1/:p2')
    @ExternalCache({
        key: 'some',
        kind:'temporal',
        ttl: 30 * SECOND, // 30 seconds
        paramIndex: [0, 1]
    })
    getSomeOther(@Param('p1') p1: string, @Param('p2') p2: string) {
        ...
    }

    // will unset cache of some + p1 + p2
    @Patch('/:p1/:p2')
    @ExternalCache({
        key: 'some',
        kind: 'bust',
        paramIndex: [0, 1]
    })
    updateSomeOther(@Param('p1') p1: string, @Param('p2') p2: string) {
        ...
    }

    // if you want to unset all cache based on a key, you can use bustAllChildren option.
    // this will only work for temporal cache.
    @Get('/foo')
    @ExternalCache({
        key: 'foo',
        kind: 'temporal',
        ttl: 30 * SECOND,
    })
    getFoo() {
        ...
    }

    @Get('foo/:p1/:p2')
    @ExternalCache({
        key: 'foo',
        kind: 'temporal',
        ttl: 30 * SECOND,
        paramIndex: [0, 1]
    })
    getFooOther(@Param('p1') p1: string, @Param('p2') p2: string) {
        ...
    }

    @Patch('/foo')
    @ExternalCache({
        key: 'foo',
        kind: 'bust',
        bustAllChildren: true
    })
    updateFoo() {
        ...
    }
}

Additional Busting within one route

In many cases, change to a single data affects multiple outputs. For example, if you modify "User" data, you have to remove caches of "User", "MyPage", "Friends". So, Omacache provide capability to remove all related caches at once.

@Put("/users/:userId")
@InMemoryCache({
    kind: "bust",
    key: "user",
    paramIndex:[0],
    // additional bustings
    // It's same type of "bust" but except kind & addition
    addition: [
        {
            key: "my_page",
            // this would not remove cache if
            // route that sets "my_page" cache construct cache key with different parameter poisition
            paramIndex:[0]
        },
        {
            key: "friends",
            bustAllChildren: true
        }
    ]
})
modifyUser(@Param("userId") userId: string, @Body() body: any) {
    ...
}

Caution

  1. persistent cache must used on method without parameters, otherwise, it will throw error that presents persistent cache cannot applied to method that have parameters.
  2. cache set does not awaited internally for not interrupting business logics. only when integrity matters, it awaits(i.e. 'has' method). If you implemented all ICacheStorage signatures synchronously, you don't have to concern about it.

nestjs-omacache's People

Contributors

jsbyeon1 avatar bjs-kr avatar ganguklee avatar sangyeopchae avatar alaazorkane avatar jinho-lee-ts avatar

Stargazers

Jiho Lee avatar Roy Lin avatar Wenzel avatar Murat Demircioglu avatar  avatar Leedoyoung avatar Deivid Donchev avatar kaleab solomon avatar ozmeneyupfurkan avatar Jamshidbek Makhmudov avatar MOHAMED MAGDY MOHAMED EMARA avatar Eva avatar Loginov Roman avatar Alfredo Cobo avatar  avatar Bagas Nugroho avatar Feras Aloudah avatar Dominus Prime avatar  avatar Vinicius Maciel avatar  avatar Eric Cheah avatar  avatar  avatar ohbin kwon avatar Tri avatar Inácio Rodrigues avatar Alexey Dugnist avatar  avatar star knight avatar Sam Bernard avatar Jakub Andrzejewski avatar Jungeun lee avatar  avatar Aleks avatar  avatar Manda Ravalison avatar Danijel Dedic avatar  avatar  avatar David Rojo avatar Fernando Rosal avatar Yoo Dongryul avatar hubts avatar Marcos Viana avatar DongHee Kang avatar Jeongho Nam avatar Blazegrad avatar wefactory avatar Teddy avatar Younha Lee avatar Kyungsu Kang avatar Park jihee avatar  avatar Anthony lee avatar dev Kim avatar  avatar  avatar hunooxi-mandros avatar Dok6n avatar 이재열 avatar  avatar Dahye Kim avatar delta avatar  avatar

Watchers

 avatar

nestjs-omacache's Issues

Bust all cache with params

I'm caching some data with params like this :

@InMemoryCache({
        key: 'inventories',
        kind: 'temporal',
        ttl: 60 * 30,
        paramIndex: [0, 1]
 })
getInventories(p1:  string, p2: number) {
 // return inventories based on params
}

Now when updating inventories, How can I bust all cached data with the key inventories ?
For example:

@InMemoryCache({
        key: 'inventories',
        kind: 'bust',
       allParams: true
 })

bust multiple cache keys

When a data changed, there should be multiple routes affected by the change

i.e.) When User info changed: User, MyPage, Profile routes should remove their cache immediately

it can improve control over cache much more easily

Handle null parameters value

I have this method in my controller :

@InMemoryCache({
    key: 'facilities',
    kind: 'temporal',
    ttl: 60 * 60 * 10,
    paramIndex: [0]
 })
async loadData(@Query('someParam') param?: number) {
 ...
}

The problem is that when userId is null (or undefined), it throw this error :
TypeError : The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received null

Is it possible to allow optional parameters ?

persistent kind Internal Server Error

Issue
persistent kind Internal Server Error when applied to an API with params

Docs
persistent cache must used on method without parameters, otherwise, it will throw error that presents persistent cache cannot applied to method that have parameters.

expected behaviour

it should act like temporal kind with ttl = 0 which means infinity time to live cache
each request should be treated as a separate key

for example :
@Get('/uploads/:filename') async getFile(@Param('filename') filename: string){ return await this.service.getFile(fileName); }
in the above example each request scoped under uploads API with a specific file name should have a cache key

if file name is test.png then key should look like this key:uploads/test.png
if file name is guest.png then key should look like this key:uploads/guest.png

make sure to include query params as well

actual behaviour
Internal server error

Great package by the way many thanks

Update reflect-metadata version on peer deps.

Issue

Current reflect-metadata version used in peer dependencies is outdated

"reflect-metadata": "^0.1.12",

Proposed Solution

  • Upgrade peer deps to match latest major version of reflect-metadata -> 0.2.2 preferably.
  • Mention in peer deps that both 1.x and 2.x are both supported (which is the case I assume?)

Why?

  • Projects made today with new version will keep getting warn prompts:
 WARN  Issues with peer dependencies found
my-project-ts
└─┬ nestjs-omacache 1.1.3
  └── ✕ unmet peer reflect-metadata@^0.1.12: found 0.2.2

Set url as default key

As of now, we have to set key.
It's good for a lot of case but is it possible to set "url" as default key ?

In my case, I have "getTicker24h" endpoints from multiple provider.
Then, I have multiple controllers and a generic super controller where I set my logic.
Endpoints are going to look like "/provider/{provider name}/{endpoints}", for example "/provider/binance/getTicker24h"

Standard nestjs cache-manager is going to set my key to "/provider/binance/getTicker24h" because it caches url.
Your library is asking for a key, so i'm going to set getTicker24h because my controller logic is defined in super.

If i have in my controller

 @Get('getTicker24h')
  @CacheStorage({
    key: `getTicker24h`,
    kind: 'temporal',
    ttl: 60,
    paramIndex: [0],
  })
  @UseInterceptors(CacheInterceptor)
  async getTicker24h(@Query() params: ZTickersParamsDto): Promise<ITickerResponse[]> {
    return this.exchangeService.execPublic(this.provider, 'fetchTickers', params);
  }

I get in redis :
image

Because decorator needs static string, I can't set my provider name, meaning I can't prefixe the cache depending on the full endpoint that was called.
Do you think we could set logic so that it takes the url (before params) by default if no key are defined ?

Thanks

BustAllChildren does not exist in type CacheOptions

I updated the package to 1.0.2 and it gives me this error when I try to use the bustAllChildren option.

Argument of type '{ key: string; kind: "bust"; bustAllChildren: boolean; }' is not assignable to parameter of type 'CacheOptions<"bust">'.
  Object literal may only specify known properties, and 'bustAllChildren' does not exist in type 'CacheOptions<"bust">'

I think it's because the code in npm package is not updated yet

Nest can't resolve dependencies of the CacheService (DiscoveryService, MetadataScanner, ?)

 ERROR [ExceptionHandler] Nest can't resolve dependencies of the CacheService (DiscoveryService, MetadataScanner, ?). Please make sure that the argument Reflector at index [2] is available in the CacheModule context.

Potential solutions:
- Is CacheModule a valid NestJS module?
- If Reflector is a provider, is it part of the current CacheModule?
- If Reflector is exported from a separate @Module, is that module imported within CacheModule?
  @Module({
    imports: [ /* the Module containing Reflector */ ]
  })

My "app.module.ts":

// Node modules
import { Module } from '@nestjs/common';
import { RateLimiterModule } from 'nestjs-rate-limiter';
import { CacheModule } from 'nestjs-omacache';
// APP Modules
import { DBModule } from 'modules/db/db.module';
import { AuthModule } from 'modules/auth/auth.module';
import { UsersModule } from 'modules/users/user.module';
import { AccessModule } from 'modules/access/access.module';
import { ProfileModule } from 'modules/profiles/profiles.module';
import { SettingsModule } from 'modules/settings/settings.module';
// Controllers
import { AppController } from './controllers/app.controller';
// Services
import { AppService } from './services/app.service';

@Module({
  imports: [
    // First load modules:
    RateLimiterModule,
    CacheModule,
    DBModule,
    // Business logic modules:
    AuthModule,
    UsersModule,
    AccessModule,
    ProfileModule,
    SettingsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

OSX
Node v20.7.0
@nestjs/common": "^10.0.0"
@nestjs/core": "^10.0.0"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.