Primitives
If QueryCache is an automatic transmission, primitives are a manual. You get direct control over cache keys, tags, TTLs, and the whole machinery. Most people don’t need this—QueryCache handles the common cases beautifully—but sometimes you want to cache something weird, or you’re integrating with existing code that already has its own key scheme, or you just really like knowing exactly what’s happening under the hood.
No judgment here. Some of us still use vim.
The basics
Section titled “The basics”import { createPrimitives, MemoryAdapter } from '@t87s/core';
const p = createPrimitives({ adapter: new MemoryAdapter(), defaultTtl: '30s', defaultGrace: '2m',});
// Query: fetch if not cached, cache the resultawait p.query({ key: 'users:123', tags: [['users', '123']], fn: () => db.users.findById('123'),});
// Set: manually cache a valueawait p.set('users:settings', settings, { tags: [['users', 'settings']], ttl: '10m',});
// Get: read without recomputingconst user = await p.get('users:123');
// Invalidate: mark tags as staleawait p.invalidate([['users', '123']]);
// Delete: remove from cache entirelyawait p.del('users:settings');from t87s import create_primitivesfrom t87s.adapters import AsyncMemoryAdapter
p = create_primitives( adapter=AsyncMemoryAdapter(), default_ttl="30s", default_grace="2m",)
# Query: fetch if not cached, cache the resultawait p.query( key="users:123", tags=[("users", "123")], fn=lambda: db.users.find_by_id("123"),)
# Set: manually cache a valueawait p.set( "users:settings", settings, tags=[("users", "settings")], ttl="10m",)
# Get: read without recomputinguser = await p.get("users:123")
# Invalidate: mark tags as staleawait p.invalidate([("users", "123")])
# Delete: remove from cache entirelyawait p.delete("users:settings")When to use primitives
Section titled “When to use primitives”You want custom cache keys. QueryCache derives keys from method names and arguments, which is convenient until you need a specific key format for debugging or migration purposes.
You’re caching ad-hoc data. Maybe you computed an expensive aggregate and want to stash it without defining a whole query for it. p.set() is your friend.
You’re mixing with existing infrastructure. If you already have cache keys from another system, primitives let you keep using them while still benefiting from t87s’s tag-based invalidation.
You enjoy the suffering. Look, I’m not going to pretend that manually managing cache keys is fun. But some of us find a certain satisfaction in it, like people who sharpen their own knives or build their own keyboards. You know who you are.
Tags are still the point
Section titled “Tags are still the point”Even when using primitives, tags are what make invalidation work. The key is just an address—the tag is what tells t87s which entries are related to each other.
When you call p.invalidate([['users', '123']]), every cache entry tagged with ['users', '123'] (or any prefix of it) gets marked stale. That’s the whole trick: you don’t have to remember every cache key you’ve ever used. You just invalidate the tag that describes what changed, and t87s figures out the rest.
If you find yourself invalidating by key instead of by tag, you might be missing the point. Not that I’ve ever done that myself. More than twice.