Cache stores
The CacheStore interface, the Redis store, and an in-process store for tests.
Use this page to connect the cache plugin to Redis or to write a store for another key-value service.
Write a custom store
store takes any object that implements CacheStore. redis() is one; you can write your own for another key-value service, or an in-process store for tests. The interface is part of the experimental surface and can change in a patch release.
interface CacheStore {
get(keys: readonly string[]): Promise<ReadonlyArray<string | null>>;
set(entries: readonly CacheStoreEntry[]): Promise<void>;
delete(keys: readonly string[]): Promise<void>;
}
type CacheStoreEntry = { key: string; ttl: number; value: string };| Method | Called for | Must |
|---|---|---|
get(keys) | every cached read, once, with the entry key followed by its dependency version keys | resolve to one value per key, in the same order, with null for a missing or expired key. Return values exactly as stored. |
set(entries) | storing a result, initializing missing versions before SQL, and replacing versions after writes, $cache.invalidate(), and $cache.clear() | write each value under its key, overwriting any previous value, and expire it after ttl seconds. |
delete(keys) | $cache.invalidate({ keys }), to remove custom-key entries | remove the keys. Keys that do not exist are not an error. |
What the plugin relies on:
- TTLs are whole seconds.
ttlis always a positive integer: durations are converted to seconds and rounded up. Entry TTLs never exceedversionTtl, and version keys are written withversionTtl. - Values are opaque strings. Each entry is a JSON header with its versions followed by the serialized result. Store and return it unchanged; the size is bounded by
maxSize. - No cross-key atomicity. A batch passed to
setdoes not have to be written atomically, and the plugin never asks for a transaction or compare-and-set. Each key does need its value and TTL written in one step (likeSET key value EX ttl), so a key never exists without an expiry. Version values are random tokens, so concurrent writers to the same key need no coordination: the last one wins. - Eviction is safe. The store may drop any key at any time. A missing entry is a miss, and a missing version gets a fresh token before the query runs, so eviction never revives a stale entry.
- One shared store per deployment. Invalidations reach other app instances only through the store. A store that lives in process memory only sees writes made by its own process.
- Failures reject. Throw or return a rejected promise when an operation fails; do not swallow errors. The plugin wraps the failure as a
CACHE_STORE_ERROR: during reads and writes it reachesonErrorand the call falls back to the database, while$cache.invalidate()and$cache.clear()throw it. A store that hides a failedsetcan hide a failed invalidation.
A minimal in-process store, suitable for tests or a single process:
import { better } from 'better-drizzle';
import { cache, type CacheStore } from 'better-drizzle/cache';
const entries = new Map<string, { expiresAt: number; value: string }>();
export const memory: CacheStore = {
async get(keys) {
const now = Date.now();
return keys.map((key) => {
const entry = entries.get(key);
if (!entry) return null;
if (entry.expiresAt <= now) {
entries.delete(key);
return null;
}
return entry.value;
});
},
async set(items) {
const now = Date.now();
for (const { key, ttl, value } of items)
entries.set(key, { expiresAt: now + ttl * 1000, value });
},
async delete(keys) {
for (const key of keys) entries.delete(key);
},
};
const client = better(db, { plugins: [cache({ store: memory, ttl: '5m' })] });The Redis store
redis(options) returns a CacheStore. RedisStoreOptions takes exactly one of:
client: aRedisClientLike, which is any object with Bun'ssend(command, args), ioredis'call(command, ...args), or node-redis'sendCommand(args). Any other object throwsCACHE_INVALID_OPTIONS.command: a function that runs one command given as a string array, such as['SET', key, value, 'EX', '60'], and resolves to its reply.
plus an optional cluster flag (detected for ioredis Cluster clients). get sends one MGET, or one GET per key in cluster mode; set sends one SET key value EX ttl per entry, concurrently; delete sends one DEL, or one per key in cluster mode. Replies other than null are converted to strings. The store never opens or closes connections.