Cache
The options, stores and errors of @dotpress/cache. For a guide, see Cache and memoization.
defineMemo()
- Keys: the arguments, compared by value: strings, numbers, booleans,
null,undefined, bigints, dates, arrays and plain objects, whatever the order of their properties. - Single flight: calls made while a value loads share the load. A failed load fails the calls in progress, and is not kept: the next call loads again.
- Values are the objects returned by
load, not copies: a caller that mutates one changes it for the others. batchreceives the keys without duplicates, and returns aMapby key (missing keys giveundefined), or an array in the order of the keys, where anErrorfails its key only. An array of the wrong length fails every key with aMemoBatchError. Keys requested before the batch runs join it, awaits included; keys requested once it started go to the next one.
The memo
createMemo() creates a memo: one per request, event message or job, never one per process.
defineCacheEntry()
Keys are prefix:name:vVersion:key, like app:user:v1:42. Keys longer than the store accepts are hashed (SHA-256). Durations are a number of seconds, or a string: '500ms', '30s', '10m', '1h', '7d'.
Without a schema, getOrSet returns the type of the loader, and get returns unknown: defineCacheEntry<[id: string], User>({ … }) declares the type.
createCache()
Values
Values are serialized with the serializer. undefined is not cached; null is. With a schema, a value read that no longer matches (written by an older version of the app) is a miss, not a crash. Values are checked when written too, as they will read back: JSON turns dates into strings, so z.date() fails and the value is not cached, with a warning, while z.coerce.date() works. getOrSet returns the value as it reads back: a hit and a miss return the same thing. The memory store serializes too, so that a caller cannot change a cached object.
Resilience
Values stay in the store for ttl plus the longer of grace and staleWhileRevalidate. Without a grace period, a failing loader fails the call.
Tags
Each tag has a random token in the store (prefix:_tag:name, kept 30 days or longer). A value records the tokens of its tags when written; invalidateTags() replaces them. Reading a value with tags reads their tokens too: one more round trip, grouped with getMany when the store has it.
- A value whose token changed or vanished is expired:
getreturnsundefined, andgetOrSetreloads it. With agraceperiod, it can still be served when the loader fails, never bystaleWhileRevalidate: the data changed. - When the tokens cannot be read, values with tags count as invalidated, and new values are not cached.
Events
onEvent(event) receives { type, reason?, entry, key }: type is hit, miss or stale, and reason (for a stale value) is revalidate, error, timeout or backoff. The same events are logged at the debug level.
Store failures
By default, the cache fails open: a failed read is a miss (the loader runs), a failed write is skipped. Every failure is logged, warn for reads and writes, error for deletions and invalidations (they may leave outdated values), and passed to onError.
Stores
Redis and Valkey
- The store uses the client of the app, which connects and closes it: node-redis (
redis, v4 or later) or ioredis. - It works with Redis, Valkey, managed services (ElastiCache, Memorystore…) and servers of the same protocol (Dragonfly, KeyDB…).
- Commands:
GET,MGET(tags),SET … PX,DEL, and forlock,SET … NX PXand a Lua script that releases a lock with its token only. - Cluster clients (
createCluster(),new Cluster()) are refused for now: use a standalone server, or the primary of a replicated one. - A Redis used as a cache only can evict the least recently used keys (
maxmemory-policy allkeys-lru); a Redis shared with BullMQ needsnoeviction.
Upstash
The store calls the REST API of Upstash Redis with fetch: no dependency, no connection to open or close. It runs on Cloudflare Workers, Vercel, Deno, Bun and Node.js. Each operation is one HTTPS request. Errors of the API (401, 400 ERR …) are store failures. A request is limited to 10 MB on the free and pay-as-you-go plans. The fetch option replaces the global fetch.
The @upstash/redis SDK is not needed: by default, it applies JSON.parse to every reply, which would change the cached values.
Cloudflare Workers KV
kv is the binding, or a function that returns it. Workers KV suits values read often and changed rarely: configuration, catalogs, rendered pages.
cacheTtl (30 seconds at least) keeps the values read in the cache of the location for longer: faster reads, but changes are seen even later. The tokens of tags are read in bulk, 100 keys per read.
Custom stores
A store implements CacheStore: name, get(key), set(key, value, ttl) (strings, ttl in milliseconds) and delete(key), and optionally getMany(keys) (one round trip for the tokens of tags), lock(key, ttl) and unlock(key, token) (the lock option), and maxKeyLength (in bytes).
cachePlugin()
The plugin, named cache, provides ctx.memo: a new memo for each request, created on first use.
@dotpress/cache/als
With AsyncLocalStorage: Node.js, Bun, Deno, and Cloudflare Workers with the nodejs_compat flag.
Outside a scope, currentMemo() and the memoized() functions throw an explicit error: there is no global memo.
Errors
The error classes extend CacheError.