To cache an expensive external call, write its result to denkops.store with a ttlSeconds, and on the next request read the key back before you call out again. If it is present you skip the call; if it expired you fetch once and rewrite it. The store is a key-value store on a durable disk, so the cache survives redeploys, and TTL entries clean themselves up. Values are strings, so serialize with JSON.stringify.
How do I set a value that expires?
Pass ttlSeconds to set. That is the whole mechanism:
import denkops from "@denkopsai/sdk";
denkops.store.set("weather:London", JSON.stringify(data), { ttlSeconds: 600 });The entry lives for ten minutes. After that, get returns null and the key drops out of list, without a cleanup job on your side. Leave ttlSeconds off and the value stays until you overwrite or delete it.
const cached = denkops.store.get("weather:London");
if (cached !== null) {
const data = JSON.parse(cached);
}What does a read-through cache look like?
The pattern is: check the store, return on a hit, otherwise load, write with a TTL, and return. Wrap it once and reuse it:
import denkops from "@denkopsai/sdk";
async function cached<T>(key: string, ttlSeconds: number, load: () => Promise<T>): Promise<T> {
const hit = denkops.store.get(key);
if (hit !== null) return JSON.parse(hit) as T;
const fresh = await load();
denkops.store.set(key, JSON.stringify(fresh), { ttlSeconds });
return fresh;
}
// One slow call, then cheap for an hour.
const rates = await cached("fx:eur", 3600, () =>
fetch("https://api.example.com/fx?base=eur").then((r) => r.json()),
);The first request pays for the external call, and every request in the next hour reads a local string. The code versus I/O split in traces makes the win visible: a cache hit shows near-zero I/O time.
How should I name keys?
Namespace them with a prefix, because list filters by prefix and returns only live keys:
denkops.store.set("fx:eur", body, { ttlSeconds: 3600 });
denkops.store.set("fx:usd", body, { ttlSeconds: 3600 });
denkops.store.list("fx:"); // ["fx:eur", "fx:usd"], expired ones excludedA prefix keeps one feature's keys apart from another's, so you can enumerate or reason about a single cache without walking everything. Include the inputs that change the result in the key. weather:London and weather:Paris are different entries; a shared weather key would serve stale data for one city.
Cache or source of truth?
Decide this per key, because the store holds both and they want opposite settings.
- A cache is a copy you can lose. Always give it a
ttlSeconds, and always be correct whengetreturnsnull, that is a miss, not an error. Fall back to the source. - A source of truth is the only copy. A job's status, an agent's persisted memory, a user preference. Do not set a TTL, or you will silently delete real data.
The failure mode is mixing them: putting durable state under a TTL, or treating a cache miss as a bug. Keep the two kinds under different prefixes so the intent is obvious at the call site.
Why does this work reliably here?
Because the app runs in an always-on process, the store stays warm between requests and a background scheduled refresh can rewrite hot keys before they expire, turning every user-facing request into a hit. The same durable disk backs it across deploys, so a redeploy does not cold-start your cache. See the docs and features for the full store surface.