Cross-posting this from GitHub discussion, in the hopes this gets more traction here.
Usecase
In some cases it might be useful to provide cache tags to a cache entry, after the result has been received.
For instance, I might fetch a product from the api:
await unstable_cache(
() => api.products.get({ id }),
["product", id],
{
tags: [`product:${id}`],
},
)()
// { type: "product", id: 42, ... }
I pass the product:$42
tag so I can invalidate the cached item when the product with that id
changes.
It is possible, because we know the id
we’re trying to fetch ahead of time and so we can add the tag when calling unstable_cache
.
Additionally, I might want to include related products in the response:
await unstable_cache(
() => api.products.get({ id }, { related: true }),
["product", id],
{
tags: [`product:${id}`],
},
)
// { type: "product", id: 42, related: [{ type: "product", id: 85 }, ...], ... }
Here it is incorrect to only add product:42
as a tag, since the result also contains data from other products.
We should also pass product:85
and other product tags from the response so we can correctly invalidate the response when related products change.
The way unstable_cache
works now, it is not possible to do this atomically.
Proposal
It would be useful if we could calculate the tags from the response in unstable_cache
so the tags can be set atomically:
In next something similar could look like:
await unstable_cache(
() => api.products.get({ id }, { related: true }),
["product", id],
{
tags(product) {
// return the related product's tags too
return [`product:${id}`, ...product.related.map(p => `product:${p.id}`)]
}
},
)
// { type: "product", id: 42, related: [{ type: "product", id: 85 }, ...], ... }
This will allow sites to cache responses way more agressively, since they can tag the cache entries with info about their content.
Being able to this is a major blocker for me to do more fine-grained caching.
Background
One example where a similar pattern is used is RTK Query, where providesTags
is allowed to be a function that takes the response as an argument and returns a list of tags.
Workaround
It does seem to be possible to add cache tags to an existing cache key (without invoking the cached function again), but that was only clear to me when I read the unstable_cache
code and I think it’s an implementation detail, not an API guarantee.
Additionally, this is not atomic, so it might happen that a cache tag gets revalidated using revalidateTag
between the first and second call to unstable_cache
in the above function will cause the cache entry to not be cleared properly.
function async workaround_cache(fn, key, options) {
const res = await unstable_cache(fn, key, {
tags: typeof options.tags === "function" ? [] : options.tags,
})
const tags = typeof options.tags === "function" ? await options.tags(res) : options.tags
// A second call does not call fn again, but because the key is the same, but it
// will add the tags to the existing cache entry.
// (!) DANGER: this is relying on an implementation detail
// (!) DANGER: this introduces a race condition
return await unstable_cache(fn, key, {
tags,
})
}
It would be good to add this to the unstable_cache
API. I’m happy to help and set up a PR if this seems like a good idea.