jperasmus / stale-while-revalidate-cache

Storage-agnostic and configurable stale-while-revalidate cache helper for any function, for any JavaScript environment.
MIT License
62 stars 5 forks source link

Getting external access to cache status (HIT,Stale, Miss, Dynamic) as well as the Save method. #18

Closed iplanwebsites closed 1 year ago

iplanwebsites commented 1 year ago

Would it be possible to expose cache status and the save method?

My API returns server needs to return server-cache status in the response header. I also need to report the hit-rate and stale-rate of requests so would need an easy way to access those variables.

Here's an example of my current server code using another lib (which doesn't support SWR), but show how I use my server-cache layer

if(skipcache) {
 res.set(API_CACHE_HEADER_NAME, "DYNAMIc");
 var result = compute()
 if(usuallyCachable) multiCache.save(cacheKey, result) // save things like hard refresh in the cache store too
 }else{ 
   multiCache.get(String(cacheKey), function (errCache, cached) {
        if (cached) {
          res.set(API_CACHE_HEADER_NAME, "HIT");
          ...
          else
           res.set(API_CACHE_HEADER_NAME, "MISS");

The save method should be easy to expose.

For the cache status, maybe an option could be to return things in an envelope?


var cacheReturn = await swr(cacheKey, methodPromise, {envelope:true});
/* { 

  data: object,
  status: 'MISS',
  computedDate: 'yesterday' //to also set in res.header and ensure client-side cache don't cache the stale cache too long.
}
jperasmus commented 1 year ago

If I understand you correctly, you want:

  1. A way to manually persist or save values into the storage without invoking the swr function.
  2. Know whether a specific invocation was retrieved from the cache (pre-stale without revalidate), was stale (with revalidate) or missed the cache.
  3. You want the hit- and stale-rate metrics across all the requests

If those are correct, number 3 is supported already and is the main reason why the swr function is also an event emitter. You can check the example here to subscribe to the events and log them to your service metrics. You can use the "invoke" event to get the total count of invocations and then use one of the other events, like "cacheHit", "cacheStale" or "cacheMiss" to calculate the rates of these events.

Regarding point 2, I'll have to think about it more. I like the idea of the envelope object, but I don't necessarily want to maintain different return types based on a config flag. In that case, I would more likely switch to the object as a breaking change. Also, I'm not 100% sure what the computedDate property means. Can you elaborate on that?

For point 1, this is not currently supported, but you could write directly into your storage yourself as a workaround, but it would be depended on an internal implementation detail because along with the cached value, you would need to write an entry for ${cacheKey}_time with a timestamp of when it was cached. This implementation could change in the future and it would break your code. Not planning on it, but it could. What I would likely do is use the event emitter to listen to a "persist" or "save" event to write the given cache value to the cache key and you would then use it like swr.emit('persist', { cacheKey, cacheValue }). How does that sound to you?

iplanwebsites commented 1 year ago

Correct! For the metrics, I needed external access to the cache status, to aggregate on something else than key (For an API framework, I'll report a cache status pie-chart for all backend function names). computedDate was simply the cached value in my example. I ended up rewriting a custom module for my server-specific implementation as it was getting beyond the scope of this library and wanted to keep things clean. Another feature I added was the multi-cache functionality: to check both a LRU memory + persistent storage; it's the best setup I found on cluster of long-pooling node backends, but it's less relevant for the browser.

jperasmus commented 1 year ago

Cool, glad you got sorted. The multi-cache with LRU + persistent storage sounds interesting. What do you do about cache busting for the LRU cache specifically? Do you still set some TTL on a cache entry?

I'll leave this issue open for a bit because I still want to potentially support the envelope idea in the next version.

iplanwebsites commented 1 year ago

Some LRU cache module can take a TTL (it doesn't however adds a bit of overhead compared to plain memory object). Cache invalidation is notoriously hard. For multiple processes (a backend distributed on 10 node), I haven't found a good way to cache.invalidate(key) using memory cache, so it's best used for single-process backend. In my case, I use a parameter or header to skip cache checks when I really need fresh data on the client (ie after an update).

jperasmus commented 1 year ago

v3 of this library is now released that uses a payload/envelope response object containing the cache value along with the cache status instead of just the cache value.