vlio20 / utils-decorators

Decorators for web and node applications
https://vlio20.github.io/utils-decorators/
MIT License
216 stars 13 forks source link

Cache burst for memoize/memoizeAsync #124

Open rdhainaut opened 2 years ago

rdhainaut commented 2 years ago

First, thanks for your work and this library.

Please is it possible to provide a sample how to "burst"/invalidate the cache when we use memoize/memoizeAsync function/decorator ?

Thank you

vlio20 commented 2 years ago

hi @rdhainaut, for both decorators you can pass your own cache object (see here https://vlio20.github.io/utils-decorators/#memoize) under the cache attribute of the config object. That way you can control the cache as you wish. Does it helps?

rdhainaut commented 2 years ago

Yeah it helps. I see you expose a generic interface and so I can control the cache behavior. I could use the delete method from Cache object to invalidate the cache.

Do you have considered to add a @BurstMemoizeAsync (or @InvalidateMemoizeAsync) to handle the case of cache invalidation for api calls for example ?

vlio20 commented 2 years ago

Do you mean to have a decorator to which you provide the same cache as you are providing to the memorize decorator?

rdhainaut commented 2 years ago

I have write a sample to explain better what i m trying to achieve. All explanations are in comments.

Note: I have choose the name "@burstMemoizeAsync" but it could be "@InvalidateMemoizeAsync" or "@FreeMemoizeAsync" or "@ClearMemoizeAsync". Chose a good name is the hardest thing in DEV :)

import { memoizeAsync } from "utils-decorators";

class LikesCounter {
  /** 
   * Description: Increase the likes counter and return the new total of likes
   */
  public async ILike(): Promise<number>{
    // INITIAL VALUE for likes counter = 100

    await this.IncrementLikesCount(); // likes counter has been incremented
    return await this.FetchLikesCount(); // EXPECTED VALUE for likes counter = 101
  }

  @memoizeAsync()
  public async FetchLikesCount(): Promise<number> {
    const url = 'https://www.api.com/Likes';

    const response = await fetch(url);
    const count = await response.text();
    return parseInt(count);
  }

  /** 
   * The decorator @burstMemoizeAsync clear previous cached value by @memoizeAsync. 
   * In other words, when i call this method i want "invalidate" the cache (= delete cached value) 
   */
  @burstMemoizeAsync({
    keyResolver: "FetchLikesCount"
  })
  public async IncrementLikesCount(){
    const url = 'https://www.api.com/Likes/PlusOne';

    return await fetch(url, { method: 'POST' });
  }
}
vlio20 commented 2 years ago

I understand your intent but there has to be a way to connect between the two decorators memoizeAsync and burstMemoizeAsync. What will happen if you have 2 methods in your class that are decorated with memoizeAsync how will you know which cache to clear when burstMemoizeAsync will be called.

I don't see a way to do it with couple the two decorators.

rdhainaut commented 2 years ago

First of all, thanks to take the time to answer me. My initial post was just a question. Now it s more a "feature request". You have already done an correct answer to use the Cache object directly but i want just see if it could be something usefull to add a new decorator to handle that case.

Just to be complete You should not have that issue that you re talking because the parameter keyResolver is required for decorator @burstMemoizeAsync (of course you must use the keyResolver that target the method with @MemoizeAsync) I suppose that from the name of method, you are able to retrieve the cache entry object.

@memoizeAsync()
public async FetchLikesCount(): Promise<number> { }

@burstMemoizeAsync({
  keyResolver: "FetchLikesCount" // WARNING we must use here the name of method that is decorated by @memoizeAsync decorator. In other word, you say here explicitely the name of methods where you want invalidate the cache.
})
public async IncrementLikesCount() { }
vlio20 commented 2 years ago

The key resolver is responsible for setting the key in the cache. It is agnostic to the cache it is working with. An alternative (if you don't pass your own cache") could be is to add a new parameter called "cacheName" and provide it to both decorators. WDYT?

rdhainaut commented 2 years ago

The cache is not a global object with "@memoizeAsync" ? If yes, why @bustMemoizeAsync cannot access to it ? If no, can we imported the globalCache in the class ?

// Solution 1
class LikeCounter {  
  @memoizeAsync() // use implicitly "global cache"
  public async FetchLikesCount(): Promise<number> { }

 @burstMemoizeAsync({  // use implicitly "global cache"
    keyResolver: "FetchLikesCount" 
  })
  public async IncrementLikesCount() { }
}
// Solution 2
import { cacheGlobal } from "utils-decorator/memoizeAsync";

class LikeCounter {  
  @memoizeAsync({ cache: cacheGlobal}) // use explicitely global cache
  public async FetchLikesCount(): Promise<number> { }

 @burstMemoizeAsync({
  cache: cacheGlobal, // use explicitely global cache
  keyResolver: "FetchLikesCount" 
  })
  public async IncrementLikesCount() { }
}

or maybe use a "local" Cache

// Solution 2 bis
class LikeCounter {
  cacheInstance: Cache = new Cache();

  @memoizeAsync({ cache: cacheInstance }) // use explicitely local cache
  public async FetchLikesCount(): Promise<number> { }

 @burstMemoizeAsync({
    cache: cacheInstance, // use explicitely local cache
    keyResolver: "FetchLikesCount" 
  })
  public async IncrementLikesCount() { }
}
vlio20 commented 2 years ago

It is not a global object as you might have multiple caches. I think the best approach is the one that is already implemented with the cache property and the burstMemoizeAsync decorator can share it with the memoizeAsync or memoize decorators.

You are welcome to create a PR. Let me know if you need any help with that.

masoud-msk commented 1 month ago

@rdhainaut Extending your 2nd solution, you can already use @after

class LikeCounter {
  cache = new Map<string, number>();

  @memoizeAsync({ cache, keyResolver: () => 'FetchLikesCount' })
  public async FetchLikesCount(): Promise<number> { }

  @after({
    func: () => cache.delete('FetchLikesCount'),
    wait: true,
  })
  public async IncrementLikesCount() { }
}

Note that IncrementLikesCount's input and output are also available to @after's func option if you have a more complex case.