Storage
Every cached function and handler reads and writes through a single global storage instance. By default that's an in-memory Map, but you can replace it with any backend that implements the minimal StorageInterface.
The Storage Interface
A storage only needs two methods, get and set. Both may be synchronous or return a promise:
interface StorageInterface {
get<T = unknown>(key: string): T | null | Promise<T | null>;
set<T = unknown>(key: string, value: T, opts?: { ttl?: number }): void | Promise<void>;
}
get(key)returns the stored value, ornullwhen the key is missing.set(key, value, opts)stores a value.opts.ttlis a lifetime in seconds (optional).- Setting a nullish value (
nullorundefined) viasetdeletes the entry. This is how invalidation reaches your backend, so make sure yoursethandles it.
Custom Storage
Use setStorage to swap in your own backend. Here it is backed by Redis:
import { setStorage } from "ocache";
import type { StorageInterface } from "ocache";
const redisStorage: StorageInterface = {
async get(key) {
const raw = await redis.get(key);
return raw === null ? null : JSON.parse(raw);
},
async set(key, value, opts) {
// A nullish value means "delete this entry" (used by invalidation).
if (value === null || value === undefined) {
await redis.del(key);
return;
}
// opts.ttl is in seconds — Redis' EX option expects seconds too.
await redis.set(key, JSON.stringify(value), opts?.ttl ? { EX: opts.ttl } : undefined);
},
};
setStorage(redisStorage);
Because the interface is so small, unstorage drivers (filesystem, Cloudflare KV, S3, Redis, and many more) drop in with a thin adapter:
import { setStorage } from "ocache";
import { createStorage } from "unstorage";
import redisDriver from "unstorage/drivers/redis";
const storage = createStorage({ driver: redisDriver({/* ... */}) });
setStorage({
get: (key) => storage.getItem(key),
set: (key, value, opts) =>
value === null || value === undefined
? storage.removeItem(key)
: storage.setItem(key, value, { ttl: opts?.ttl }),
});
setStorage replaces the storage globally — every cached function and handler uses it. Call it once at startup, before your first cached call.Memory Storage & LRU
The built-in storage is created with createMemoryStorage. It keeps at most 10 000 entries by default, evicting the least-recently-used entries once the ceiling is exceeded (LRU). Reading or writing a key marks it as most-recently-used.
Pass maxSize to change the ceiling, or set it to Infinity (or 0) to disable eviction and grow unbounded:
import { createMemoryStorage, setStorage } from "ocache";
// Change the entry ceiling.
setStorage(createMemoryStorage({ maxSize: 50_000 }));
// Opt out of the ceiling entirely.
setStorage(createMemoryStorage({ maxSize: Infinity }));
Memory storage also honors the per-entry TTL: expired entries return null and are cleaned up automatically.
Multi-tier Caching
The base option prefixes cache keys. Pass an array of prefixes to enable multi-tier caching:
- On read, each prefix is tried in order and the first hit wins.
- On write, the entry is stored under every prefix.
import { defineCachedFunction } from "ocache";
const cachedFetch = defineCachedFunction(
async (url: string) => {
const res = await fetch(url);
return res.json();
},
{
maxAge: 60,
base: ["/local", "/remote"], // read "/local" first, then "/remote"; write both
},
);
This suits layered setups (e.g. a fast local tier in front of a shared remote tier): reads prefer the nearest tier while every tier stays populated on writes. A single string (the default is "/cache") keeps everything in one tier.
Invalidation and expiration operate across all tiers — see Invalidation & Expiration.