Tag invalidation
Group KV keys under a tag and drop the whole group in one call. Every built-in adapter implements it, but the methods sit on the adapter rather than on ctx.kv, so reaching them takes one deliberate step.
Cache a rendered page under page:/posts/hello and its OG image under
og:/posts/hello, tag both post:123, and one call after an edit drops both.
That is the whole feature. It saves you from tracking, by hand, which derived
keys a record fed.
The three methods
They are optional members of KVAdapter. All three built-in adapters implement
all three.
| Method | What it does |
|---|---|
setWithTags(key, value, tags, ttl?) | Writes the value and files the key under each tag. |
invalidateByTag(tag) | Deletes every key filed under that tag, then the tag. |
invalidateByTags(tags) | The same for several tags at once. |
ttl is in seconds, like everywhere else in KV.
Reaching the adapter
KVService exposes get, set, delete, has and clear and nothing more,
so ctx.kv.setWithTags does not exist. The tag methods live one level down. The
reliable way to reach them is to keep the reference you built.
import { runtimeConfig } from "questpie/app";
import { redisKVAdapter } from "questpie/adapters/redis-kv";
// getRedis returns a connected client, as on the Key-value store page
export const kvAdapter = redisKVAdapter({ client: getRedis });
export default runtimeConfig({ kv: { adapter: kvAdapter } });import { kvAdapter } from "@/questpie/server/questpie.config";
await kvAdapter.setWithTags?.("page:/posts/hello", html, ["post:123"], 3600);
await kvAdapter.setWithTags?.("og:/posts/hello", ogImage, ["post:123"]);
// After an edit, one call drops both
await kvAdapter.invalidateByTag?.("post:123");app.config.kv?.adapter reads back that same instance, since app.config is
your raw config object. The ?. before each call is not defensive style,
TypeScript requires it because the three members are optional.
On the zero-config default there is no adapter to read
Omit kv.adapter and KVService constructs MemoryKVAdapter internally,
never writing it back. So app.config.kv?.adapter is undefined and every
?. tag call above silently does nothing, even though MemoryKVAdapter
implements all three methods. Construct the adapter yourself to use tags.
What each backend stores
On Redis and Cloudflare the index is a second set of keys beside your values,
written under tagIndexPrefix ("__questpie_tag:" by default) after
keyPrefix. MemoryKVAdapter takes no options at all, so it keeps its index
off to one side instead.
| Adapter | Tag index |
|---|---|
MemoryKVAdapter | A Map of tag to a Set of keys |
redisKVAdapter | One Redis SET per tag |
cloudflareKVAdapter | A JSON array per tag, since Cloudflare KV has no SET type |
delete() keeps the index honest on all three, dropping the key from every tag
that held it. On Redis that means a SCAN across the tag-index keys per delete,
which is worth knowing before you delete in a tight loop.
Tag indexes do not expire
No adapter puts a TTL on the index itself, and nothing prunes it when a value
expires. A tag keeps listing keys that are already gone, so invalidating it
still issues a delete for each one before dropping the tag. clear() removes
the index along with the values.
What tags do not do
defaultTtl does not apply. It is a KVService fallback, and setWithTags is
an adapter call that never passes through the service, so a setWithTags
without a ttl writes a value that never expires.
Neither is invalidation transactional. The deletes and the index cleanup go out as separate commands with nothing rolling them back, so a failure part way leaves some keys gone and some present. Treat a tag drop as a cache hint rather than a guarantee, and let the next read recompute.
Related
- Key-value store, the service, the adapters and the swap.
- Hooks, where to invalidate after a committed write.