Tags, Queries, Invalidations
Tags are the first step in caching. They’re a way for you to give logical names to your database and API operations.
For example, if you have a user with ID 123, you might tag it as ['user', '123']. If that user has posts, you might tag those as ['user', '123', 'posts'].
tl;dr - tags are simple names for the things you care about.
const tags = defineTags({ user: (id: string) => ['user', id], userPosts: (id: string) => ['user', id, 'posts'],});tags = define_tags({ "user": lambda id: ("user", id), "user_posts": lambda id: ("user", id, "posts"),})Queries
Section titled “Queries”Queries are read-only interactions with a database, API, blockchain, file system, or any otherwise persnickety entity. We attach tags to queries to say “instead of doing that expensive operation every time, when we see this tag again, use the cached result.”
// "get user 123, and remember it by this tag"const getUser = t87s.query((id: string) => ({ tags: [tags.user(id)], fn: () => db.users.findById(id),}));# "get user 123, and remember it by this tag"@t87s.querydef get_user(id: str) -> QueryConfig[User]: return QueryConfig( tags=[tags["user"](id)], fn=lambda: db.users.find_by_id(id), )Invalidations
Section titled “Invalidations”Nothing in this world is permanent (except impermanence itself), and that’s why you need invalidations. They tell the caching engine that data is stale and needs to be refetched.
// "user 123 changed, forget what you knew"const updateUser = t87s.mutation(async (id, data) => { const user = await db.users.update(id, data); return { result: user, invalidates: [tags.user(id)], };});# "user 123 changed, forget what you knew"@t87s.mutationdef update_user(id: str, data: dict) -> MutationResult[User]: user = db.users.update(id, data) return MutationResult( result=user, invalidates=[tags["user"](id)], )The caching trifecta
Section titled “The caching trifecta”So there you have it:
- Tags give operations names
- Queries use tags to know when we can hit the speedy cache instead of the slow operation
- Invalidations tell the cache that data is stale and needs to be refreshed
Here’s how they work together:
// First call: cache miss, fetches from DBconst user = await getUser('123');
// Second call: cache hit, instantconst userAgain = await getUser('123');
// Update triggers invalidationawait updateUser('123', { name: 'New Name' });
// Next call: cache miss (was invalidated), fetches fresh dataconst freshUser = await getUser('123');# First call: cache miss, fetches from DBuser = get_user("123")
# Second call: cache hit, instantuser_again = get_user("123")
# Update triggers invalidationupdate_user("123", {"name": "New Name"})
# Next call: cache miss (was invalidated), fetches fresh datafresh_user = get_user("123")Why arrays?
Section titled “Why arrays?”You might have noticed tags are arrays like ['user', '123'] instead of strings like 'user:123'. This enables something we call “hierarchical invalidation”, which sounds as pretentious as it is. When you invalidate ['user', '123'], it also invalidates ['user', '123', 'posts'] and ['user', '123', 'settings'].
See Prefix Matching for the details.