@nifrajs/cache
v4.0.0
Published
Typed KV cache for nifra - TTL, stale-while-revalidate, tag invalidation, and single-flight stampede protection on a pluggable store (in-memory default; bring CF KV / Redis). Dependency-free, runs on Bun/Node/Deno/Workers.
Maintainers
Readme
@nifrajs/cache
Typed KV cache for nifra - TTL, stale-while-revalidate, tag invalidation, and single-flight stampede protection on a pluggable store (in-memory by default; bring CF KV / Redis for a shared cache). Dependency-free; runs on Bun/Node/Deno/Workers.
import { createCache } from "@nifrajs/cache"
const cache = createCache({ defaultTtlMs: 30_000 })
// Cache-aside in a loader - one DB hit per key per TTL, even under a stampede:
export async function loader({ params }) {
const user = await cache.wrap(
`user:${params.id}`,
() => db.users.find(params.id),
{ ttlMs: 30_000, swrMs: 60_000, tags: [`user:${params.id}`] },
)
return { user }
}
// On write, drop everything tagged for that user:
await cache.invalidateTag(`user:${id}`)Semantics
wrap(key, loader, opts)- returns the cached value, or runsloader, stores, and returns it.- Fresh (
now < staleAt): the cached value, no loader call. - Stale-but-live (
staleAt ≤ now < expiresAt, i.e. withinswrMs): the stale value is returned immediately while a background refresh runs (deduped). Latency stays flat; data self-heals. - Miss/expired: awaits
loader. Concurrent misses for the same key share one call (no stampede). - A throwing
loaderis not cached (and in the background path goes toonError, never rejects the caller).
- Fresh (
set/get/has/delete,invalidateTag(tag),clear().- TTL:
ttlMs(→ stale) + optionalswrMs(→ served-stale window) + optionaltags.
Stores
The default MemoryCache is in-process with incremental expiry, a tag index, and a 10,000-entry LRU
cap. Tune it with new MemoryCache({ maxEntries: 2_000 }); maxEntries: 0 is the explicit unbounded
opt-in. For a cache shared across instances, implement CacheStore
(get / set / delete / invalidateTag / clear) over CF KV, Redis, etc.:
const cache = createCache({ store: new RedisCacheStore(redis) })On Cloudflare Workers the in-memory cache is per-isolate and short-lived - back it with CF KV (or
the Cache API) via a CacheStore for anything that should survive across requests/instances.
Observing operations and tracing
observer receives one event per operation after it settles: { op, outcome, startedAt, durationMs,
key, tag, tagCount, context }. outcome is hit, stale or miss for reads, ok for writes and a
finished background refresh (op: "revalidate"), error when the store or loader threw. An observer
that throws is swallowed; it never changes a result. Without an observer the cache reads no extra clock.
cache.for(c) binds a request (or job) context, and every event carries it. That is how
cacheTracing() from @nifrajs/otel/cache turns each operation into a child span of c.trace:
import { cacheTracing } from "@nifrajs/otel/cache"
const cache = createCache({ observer: cacheTracing({ exporter }) })
app.use(tracing({ exporter })).get("/orders/:user", (c) =>
cache.for(c).wrap(`orders:${c.params.user}`, () => loadOrders(c.params.user)), // span "cache wrap"
)The raw key goes to the in-process observer only. cacheTracing exports the key prefix (the token
before the first :, when it is a short lowercase name) unless keyAttribute says otherwise
("none", or a function that owns the redaction). A stale read's background refresh is its own trace,
linked to the request span. for(c) needs a beacon, an observer, or both; the beacon is enforced
whenever it is set.
API
createCache(options?)→Cache-{ store?, defaultTtlMs?, now?, onError?, observer?, beacon?, capabilities? }.cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })·get·has·set·delete·invalidateTag·clear.cache.for(context)→ the same surface bound to a request or job context.MemoryCache({ maxEntries?, now? })- the default store; implementCacheStorefor your own.
For AI agents
Start with LLM.md - this package's contract card (the exports you call + its footguns),
one cheap read instead of the whole corpus. For the wider framework: the repo's
AGENTS.md is the copy-paste quick reference, and
llms-full.txt is the full machine-readable corpus. Run nifra check as the
done-gate, or nifra mcp to give the agent live project tools.
