Cache
LRU + TTL cache with operator-supplied namespacing, drop-silent key validation on hot-path observability, and pluggable backends that share semantics across single-process and clustered nodes.
Three first-class backends ship in the box:
- "memory" (default) — Map + LRU eviction (maxEntries) + bytes eviction (maxBytes) + periodic sweep. Single-process accuracy. - "cluster" — _blamejs_cache table via cluster-storage. One table serves every CacheInstance via "
A { get, set, del, clear, size, close } operator-supplied object is the custom-backend escape hatch (Memcached, in-memory harnesses, anything else with the same async surface).
Hot-path validation policy:
- create() opts → throw at boot (config-time) - key arg on get/set/del → throw at call site (programming bug) - per-call ttlMs override → throw at call site (silent footgun if accepted) - audit / observability → drop silent (hot-path sink) - method-after-close → throw BAD_STATE
Security defaults that are NOT opt-in:
- auditClear: true mass purges are operator-action shaped - auditFailures: true backend errors are signal - hot-path get/set/hit/miss/eviction → observability only (the audit chain would drown at any reasonable QPS)
Returned CacheInstance shape:
get(key) → value | undefined set(key, value, opts?) → void (opts: { ttlMs, tags, seal }) del(key) → boolean has(key) → boolean (does NOT bump LRU recency) clear(opts?) → number (opts: { req, context }) size() → number bytes() → number (memory backend only) wrap(key, fn, opts?) → fn's return (opts: { ttlMs, singleFlight }) invalidateTag(tag, opts?) → number (opts: { req, context }) getTags(key) → string[] | null close() → void
Stale-while-revalidate, single-flight wrap (concurrent calls collapse to one compute), tag-based bulk invalidation (memory + cluster), and cross-node invalidation via b.pubsub are all built in — operator opts in via the staleWhileRevalidate / invalidationPubsub opts.
What is NOT in the box: maxBytes on the cluster backend (would require an aggregate query per set; operator prunes the shared table on their own schedule) and per-entry exact slidingTtl on the cluster backend (sliding extends by the cache's defaultTtlMs; operators with mixed-TTL writes wanting strict per-entry sliding use the memory backend or extend at the application layer).
b.cache.create(opts) #
{
namespace: string, // required; collision domain; must not contain ':'
backend: "memory" | "cluster" | "redis" | object, // default "memory"
ttlMs: number | Infinity, // default C.TIME.minutes(5)
maxEntries: number | Infinity, // memory backend cap; default 10000
maxBytes: number | Infinity, // memory backend cap; default Infinity
sizeOf: function(value) -> number, // memory bytes accounting override
sweepIntervalMs: number, // expired-entry sweep cadence; default C.TIME.minutes(1); minimum 1000
staleWhileRevalidate: boolean, // wrap() serves stale + refreshes in background; default false
slidingTtl: boolean, // bump expiresAt on hit; default false
auditFailures: boolean, // emit audit on backend errors; default true
auditClear: boolean, // emit audit on clear / invalidateTag; default true
audit: { emit } | b.audit, // audit sink override
observability: { event } | b.observability, // metrics sink override
clock: function() -> number, // Date.now() override (testing)
invalidationPubsub: b.pubsub instance, // cross-node del/clear/tag mirroring
redisUrl: string, // backend === "redis" only; required there
redisPassword: string, // backend === "redis" only
redisUsername: string, // backend === "redis" only
redisTls: boolean, // backend === "redis" only
redisCa: string | Buffer, // backend === "redis" only; PEM CA bundle
redisServername: string, // backend === "redis" only; SNI override
redisConnectTimeoutMs: number, // backend === "redis" only
redisCommandTimeoutMs: number, // backend === "redis" only
redisMaxReconnectAttempts: number, // backend === "redis" only
}
Build a CacheInstance bound to a namespace. The instance owns its sweep timer, its backend connection, its single-flight inflight map, and (when invalidationPubsub is supplied) a pubsub subscription that mirrors del / clear / invalidateTag events across nodes. Multiple instances coexist — a "session.user" memory cache and a "billing.invoice" cluster cache share neither keys nor tags. close() releases everything.
The backend opt picks the storage tier: "memory" (default, single-process LRU+TTL), "cluster" (shared SQL table for multi-node coherence), "redis" (when redisUrl is supplied; native EXPIRE-based TTL), or an operator-supplied object with { get, set, del, clear, size, close } for any other store. Backends are interchangeable from the caller's perspective — await cache.get(key) returns the same shape regardless.
var b = require("@blamejs/core");
var C = b.constants;
// Simple set/get against the default memory backend.
var sessions = b.cache.create({
namespace: "session.user",
ttlMs: C.TIME.minutes(5),
maxEntries: 10000,
});
await sessions.set("u-42", { uid: "u-42", role: "admin" });
var hit = await sessions.get("u-42");
// → { uid: "u-42", role: "admin" }
var b = require("@blamejs/core");
var C = b.constants;
// wrap() pattern with per-call TTL override + single-flight.
// Concurrent callers collapse to one DB read; subsequent reads
// serve from cache for 10 minutes.
var profiles = b.cache.create({
namespace: "billing.profile",
ttlMs: C.TIME.minutes(2),
});
var profile = await profiles.wrap(
"u-42",
function () { return { uid: "u-42", plan: "pro" }; },
{ ttlMs: C.TIME.minutes(10) }
);
// → { uid: "u-42", plan: "pro" }
var b = require("@blamejs/core");
var C = b.constants;
// Cluster-shared cache: every node sees the same entries via the
// _blamejs_cache table. Tag-based bulk invalidation purges across
// every namespace member in one call.
var inventory = b.cache.create({
namespace: "catalog.item",
backend: "cluster",
ttlMs: C.TIME.minutes(15),
});
await inventory.set("sku-1001", { qty: 42 }, { tags: ["warehouse:east"] });
var purged = await inventory.invalidateTag("warehouse:east");
// → 1
b.cache.update(key, mutatorFn, opts?) #
{
ttlMs: number | Infinity, // lifetime of the written value; default the instance ttlMs
seal: boolean, // cluster backend only — seal the value at rest
}
Atomic read-modify-write. Reads the current value, calls mutatorFn(current | null), and commits the result in one operation so a concurrent writer cannot clobber the change (lost update) — the race that makes a plain get → mutate → set unsafe for counters, sets, and quorum state. The memory backend is atomic by single-thread; the cluster backend uses a transaction with compare-and-set + retry.
mutatorFn returns one of: { value } to commit the new value, { abort: data } to leave the entry untouched and surface data to the caller, or { delete: true } to remove the entry. A committing decision may also set the written value's lifetime — { value, ttlMs } (a duration the backend resolves against its own clock) or { value, expiresAt } (an absolute time) — when the new value's own state decides how long it should live; otherwise the call ttlMs applies. The call resolves to { updated: true, value }, { updated: true, deleted: true }, or { aborted: data }.
await counters.update("hits", function (n) {
return { value: (n || 0) + 1 };
});
Last updated 2026-08-08T16:39:49.652Z by seeder.