Cache
One cache API. Memory or disk.
createCache() gives you a driver-agnostic store — get/set/has/delete/flush,
a remember() read-through helper, TTL expiry, and tag invalidation. The zero-config
in-memory driver is perfect for a single process; switch to the file driver to survive a restart —
same interface, one config line.
Setup #
createCache lives in the server entry. Call it with no arguments for the in-memory
driver (the zero-config default), or pass a driver discriminant. Values round-trip through JSON,
so anything serializable is fair game.
import { createCache } from '@apex-stack/core/server'
const cache = createCache() // memory driver — the default
const disk = createCache({ driver: 'file', dir: '.apex/cache' })
Read & write #
The whole surface is async. set takes an optional third argument — TTL in
seconds; omit it and the entry never expires. A miss returns
undefined.
await cache.set('user:1', { id: 1, name: 'Ada' })
await cache.get('user:1') // → { id: 1, name: 'Ada' }
await cache.get('missing') // → undefined
await cache.has('user:1') // → true
await cache.set('otp:9', '123456', 60) // expires 60s from now
await cache.delete('user:1') // drop one key
await cache.flush() // drop everything
An expired entry returns undefined and is evicted on the next read — no background
sweeper. The clock is injectable (createCache({ driver: 'memory', now })), so cache
expiry is deterministic under test.
remember() #
The read-through pattern in one call: on a miss, run the factory and cache its result under the given TTL; on a hit, skip the factory entirely. The factory runs exactly once until the value expires.
const report = await cache.remember('report:q3', 300, async () => {
return await computeExpensiveReport() // only runs on a miss
})
Tag invalidation #
Group related keys under one or more tags via cache.tags([...]), then flush the whole
group in one call. Flushing a tag drops every key registered under it and leaves other tags
untouched — a key tagged under several tags is dropped when any of them is flushed.
await cache.tags(['users']).set('user:1', { id: 1 })
await cache.tags(['users']).set('user:2', { id: 2 })
await cache.tags(['posts']).set('post:1', { id: 1 })
await cache.tags(['users']).flush() // user:1 + user:2 gone; post:1 survives
// remember() works through a tag view too — the key is registered under the tag.
await cache.tags(['reports']).remember('report:q3', 60, async () => compute())
File driver #
The file driver writes each entry as a JSON file under a base directory, so the cache survives a
process restart — a fresh createCache({ driver: 'file', dir }) over the same directory
reads the persisted data. TTL, delete, and flush behave exactly as the
memory driver; expired files are evicted on read.
const cache = createCache({ driver: 'file', dir: '.apex/cache' })
await cache.set('k', { hello: 'world' })
// A new instance over the same dir sees the persisted value.
const reopened = createCache({ driver: 'file', dir: '.apex/cache' })
await reopened.get('k') // → { hello: 'world' }
Both drivers are pure and clock-injectable — the same store powers rate limiting and idempotency (see Auth → Hardening).