cached()
cached() is a higher-order function that wraps any async function with a cache. Declare the caching strategy once; every subsequent call uses the cache automatically.
import { cached } from "@warlock.js/cache";
const getUser = cached( (id: number) => db.users.find(id), "user", "1h",);
await getUser(42); // cache miss → runs DB query, caches, returnsawait getUser(42); // cache hit → no DB callawait getUser.invalidate(42); // drop "user.42" from the cacheawait getUser(42); // miss againUses remember() under the hood — inherits stampede protection for free.
When to use cached vs remember
Section titled “When to use cached vs remember”| Task | Use |
|------|-----|
| One-shot “get this value or compute it” | cache.remember() |
| Turn an existing function into a cached version you’ll call many times | cached() |
Three shapes
Section titled “Three shapes”All three put fn first and return the same wrapped function type.
1. Shorthand — prefix only
Section titled “1. Shorthand — prefix only”Driver’s default TTL. The wrapped function’s arguments are auto-appended to the prefix to derive the cache key.
const getFeatured = cached( () => db.products.findFeatured(), "featured-products",);// cache key: "featured-products"
const getUser = cached( (id: number) => db.users.find(id), "user",);// getUser(42) → cache key "user.42"2. Shorthand — prefix + TTL
Section titled “2. Shorthand — prefix + TTL”Same as #1, with an explicit TTL. Accepts number of seconds or a duration string.
const getUser = cached( (id: number) => db.users.find(id), "user", "1h",);
const getOrder = cached( (userId: number, orderId: string) => db.orders.find(userId, orderId), "orders", "10m",);// getOrder(42, "abc") → cache key "orders.42.abc"3. Options form — full control
Section titled “3. Options form — full control”Custom key function, tags, per-call driver override.
type Filters = { category: string; sort: string; page: number };
const searchProducts = cached( (filters: Filters) => db.products.search(filters), { key: (filters) => `products.search.${filters.category}.${filters.sort}`, ttl: "15m", tags: ["products"], driver: "redis", },);Use the options form when:
- The cache key shouldn’t include every argument (drop
pagefrom the key above so paginated calls share a cache entry). - You want to attach tags for bulk invalidation.
- You need to route this wrapper’s calls to a non-default driver.
Auto-key rules (shorthand only)
Section titled “Auto-key rules (shorthand only)”When you use the positional cached(fn, prefix) / cached(fn, prefix, ttl) form, keys are derived automatically:
| Args shape | Key |
|------------|-----|
| No args | prefix |
| Any args | ${prefix}.${hash} — the first 16 hex chars of the sha1 of a stable JSON encoding of the args |
| Map, Set, functions, symbols, class instances, circular refs | CacheConfigurationError |
The hash keeps distinct args on distinct keys: profile("1.private", "") and profile("1", "private"), or { q: "x" } and ("q", "x"), never share an entry. Object keys are sorted, Date becomes an ISO string, undefined and null are distinct, and bigint is encoded as a decimal string.
Examples:
getUser() // "user"getUser(42) // "user.<16 hex chars>"searchBy({ q: "hello" }) // "search.<16 hex chars>"The exact hash is not part of the contract. Don’t build keys by hand; use .invalidate(...args).
:::caution Upgrading to 5.20
The auto-key format changed (it was dot-joined args). Entries written by earlier versions are never read again, so every cached() call misses once and repopulates. Old entries expire by TTL, or flush() clears them.
:::
Caveats
Section titled “Caveats”- Order matters.
getOrder(42, "abc")andgetOrder("abc", 42)produce different keys. Date→ ISO string. TwoDateobjects at the same millisecond share a key — correct.Map,Setand class instances throw. They can’t be encoded stably. Use the options form with a customkeyfn..invalidate()uses the configureddriver, the same one the wrapper reads and writes.
When the auto-key isn’t right, use the options form. key can project whatever subset of the args actually identifies the cached value.
Helpers on the wrapped function
Section titled “Helpers on the wrapped function”.invalidate(...args)
Section titled “.invalidate(...args)”Drop the cache entry for a specific argument combination — uses the same key function the wrapper uses internally, so callers don’t re-derive keys by hand.
const getUser = cached((id: number) => db.users.find(id), "user", "1h");
await getUser(42); // populates "user.42"await getUser.invalidate(42); // drops "user.42":::info Planned for v2.1
.refresh() (force-recompute) and .peek() (read without populating) are not shipping yet. If you need them, file it under domains/cache/backlog.md.
:::
Real-world examples
Section titled “Real-world examples”Memoize a DB lookup
Section titled “Memoize a DB lookup”const getUser = cached( (id: number) => db.users.findById(id), "user", "1h",);
// Call wherever you need a userconst user = await getUser(42);
// Invalidate after a writeawait db.users.update(42, patch);await getUser.invalidate(42);Bulk invalidation via tags
Section titled “Bulk invalidation via tags”const getUser = cached( (id: number) => db.users.findById(id), { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] },);
const getPosts = cached( (userId: number) => db.posts.findByUser(userId), { key: (userId) => `posts.by-user.${userId}`, ttl: "30m", tags: ["users", "posts"] },);
// After any user-table change, drop every wrapper's cache in one goawait cache.tags(["users"]).invalidate();Project a subset of args into the key
Section titled “Project a subset of args into the key”type Filters = { category: string; sort: string; page: number; pageSize: number };
// Key ignores `page` so paginated calls for the same filters share an entry.// (Only do this if your source function returns ALL pages — otherwise pages// would collide. Use this pattern thoughtfully.)const getCategoryMeta = cached( (filters: Filters) => db.categories.meta(filters.category), { key: (f) => `category.meta.${f.category}`, ttl: "1h", },);Cache a remote API call with tags
Section titled “Cache a remote API call with tags”const getWeather = cached( (city: string) => fetch(`https://api.weather.com/${city}`).then((r) => r.json()), { key: (city) => `weather.${city.toLowerCase()}`, ttl: "10m", tags: ["weather"] },);
// Refresh everyone after a scheduled synccron.every("10m", () => cache.tags(["weather"]).invalidate());Stampede behavior
Section titled “Stampede behavior”Inherited from remember(). When 100 concurrent calls hit the same wrapper with the same args on a cold cache, the underlying function runs once and the other 99 share its promise. Works within a single Node process; cross-process stampede safety still needs a distributed lock.
const getter = cached(expensiveQuery, "report", "1h");
// Underlying expensiveQuery runs exactly onceawait Promise.all(Array.from({ length: 100 }, () => getter(42)));Troubleshooting
Section titled “Troubleshooting”CacheConfigurationError: could not derive an auto-key from args— your args include something that can’t be encoded stably (circular ref,Map,Set, function, class instance). Switch to the options form with a customkeyfn that picks JSON-safe fields.- Two calls with the same args hit the source function — driver default TTL is
Infinityonly if you haven’t set one. Checkoptions.<driver>.ttl, or pass a TTL via the shorthand. .invalidate()doesn’t seem to work — verify you’re calling it with the same args shape the wrapper was called with. The key function needs matching inputs to produce the same key.
Related Documentation
Section titled “Related Documentation”- Stampede Prevention —
remember(), the primitive under the hood - Set Options — the shape of the options object
cachedforwards toset() - Cache Tags — tag-based invalidation
- Errors —
CacheConfigurationErrorand when it fires