@wrnexus/cache
Memory and distributed caching with coordination and invalidation.
Install the package
After WorkRoot approves private registry access, install the release-aligned package:
bun add @wrnexus/cache@0.8.7Request preview access. Never put registry tokens in source control.
Bounded in-memory/tag caching and HTTP response caching for WRNexusJS. Supports request deduplication, tag invalidation, ETags, fresh/stale states, and optional detached stale revalidation.
import { connectCacheInvalidation, TagCache, responseCache } from "@wrnexus/cache";
const cache = new TagCache({ ttlMs: 60_000, staleWhileRevalidateMs: 300_000 });
export default responseCache({ cache, tags: ["products"] });
TagCache bounds entries with LRU-style eviction, deduplicates concurrent loaders, and prevents an invalidated in-flight loader from repopulating stale data. Use lookup() when fresh/stale state matters, or getOrLoad() for stampede-safe loading.
For multi-instance applications, connect the cache to any compatible pub/sub bus (including @wrnexus/pubsub). Namespaces isolate applications sharing the same broker. Local invalidation happens first and the returned promise confirms cross-instance publication; failures remain visible to the caller.
import { connectCacheInvalidation, TagCache } from "@wrnexus/cache";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
const cache = new TagCache({ maxEntries: 10_000 });
const bus = createPubSub(redisDriver(process.env.REDIS_URL));
const invalidation = connectCacheInvalidation(cache, bus, {
namespace: "storefront-production",
onError: (error) => logger.error("cache invalidation failed", { error }),
});
await invalidation.invalidateTag("products");
await invalidation.delete("product:42");
// Unsubscribes this cache only; the shared bus remains owned by the app.
invalidation.close();
await bus.close();
Framework cache layers
CacheCoordinator keeps the four cache lifetimes explicit:
coordinator.request()creates request-only deduplication.coordinator.datacaches loader/query results.coordinator.componentcaches reusable rendered fragments.coordinator.pagecaches complete safe documents.
All cross-request layers are bounded, tag-aware, stale-while-revalidate capable, stampede-safe, and expose withLock() for exclusive per-key work. inspect() returns metadata without cached values. Development applications expose that inspection through the Cache panel and GET /__wrnexus/cache.
Pages and components can opt in declaratively:
cache {
scope = "page"
strategy = "stale-while-revalidate"
ttl = "5m"
stale = "10m"
tags = ["catalog", "marketing"]
vary = ["tenant", "language"]
}
Omit scope to cache named loader data. Use scope = "page" for full-page caching. Component policies cache their rendered fragment. Authenticated user and tenant identities are always included automatically; page caches also vary by language, theme, and accent. Add header names or cookie:name entries for other application-specific variation. Pages containing CSRF forms are never stored in the full-page cache.
Complete TypeScript API
Generated from the exact installed package declarations.
import { Context, Middleware } from '@wrnexus/core';
interface CacheEntry<V> {
value: V;
createdAt: number;
expiresAt: number;
staleUntil: number;
tags: string[];
}
type CacheLookup<V> = {
state: "miss";
} | {
state: "fresh" | "stale";
entry: CacheEntry<V>;
};
interface CacheSetOptions {
ttlMs?: number;
staleWhileRevalidateMs?: number;
tags?: string[];
}
interface TagCacheOptions {
ttlMs?: number;
staleWhileRevalidateMs?: number;
maxEntries?: number;
clock?: () => number;
onEvent?: (event: CacheEvent) => void;
}
interface CacheEvent {
operation: "hit" | "stale" | "miss" | "set" | "delete" | "invalidate" | "clear" | "load";
key?: string;
tags?: string[];
at: number;
}
interface CacheSnapshotEntry {
key: string;
state: "fresh" | "stale";
createdAt: number;
expiresAt: number;
staleUntil: number;
tags: string[];
}
declare class TagCache<V = unknown> {
private entries;
private tagIndex;
private pending;
private locks;
private revisions;
private readonly ttlMs;
private readonly staleMs;
private readonly maxEntries;
private readonly clock;
private readonly onEvent?;
constructor(options?: TagCacheOptions);
private emit;
lookup(key: string): CacheLookup<V>;
get(key: string): V | undefined;
set(key: string, value: V, options?: CacheSetOptions): void;
private store;
getOrLoad(key: string, loader: () => V | Promise<V>, options?: CacheSetOptions): Promise<V>;
/** Serialize arbitrary cache-adjacent work for a key without storing its result. */
withLock<T>(key: string, task: () => T | Promise<T>): Promise<T>;
delete(key: string): boolean;
private removeEntry;
invalidateTag(tag: string): number;
invalidateTags(tags: Iterable<string>): number;
clear(): void;
get size(): number;
snapshot(): CacheSnapshotEntry[];
private revision;
private bump;
}
type CacheLayerName = "data" | "component" | "page";
interface CacheInspection {
layers: Record<CacheLayerName, ReturnType<TagCache<unknown>["snapshot"]>>;
recentEvents: Array<CacheEvent & {
layer: CacheLayerName;
}>;
}
interface CacheCoordinatorOptions extends Omit<TagCacheOptions, "onEvent"> {
eventLimit?: number;
onEvent?: (event: CacheEvent & {
layer: CacheLayerName;
}) => void;
}
/** A request-lifetime cache: deduplicates work without leaking values between requests. */
declare class RequestCache {
private pending;
getOrLoad<V>(key: string, loader: () => V | Promise<V>): Promise<V>;
clear(): void;
}
/** Owns the three cross-request cache layers and creates isolated request caches. */
declare class CacheCoordinator {
readonly data: TagCache<unknown>;
readonly component: TagCache<unknown>;
readonly page: TagCache<unknown>;
private readonly events;
private readonly eventLimit;
constructor(options?: CacheCoordinatorOptions);
request(): RequestCache;
layer(name: CacheLayerName): TagCache<unknown>;
getOrLoad<V>(layer: CacheLayerName, key: string, loader: () => V | Promise<V>, options?: CacheSetOptions): Promise<V>;
invalidateTags(tags: Iterable<string>): number;
inspect(): CacheInspection;
clear(): void;
}
interface CachedResponse {
status: number;
statusText: string;
headers: [string, string][];
body: Uint8Array;
etag: string;
}
interface ResponseCacheOptions extends CacheSetOptions {
cache?: TagCache<CachedResponse>;
key?: (ctx: Context) => string;
vary?: string[];
shouldCache?: (ctx: Context, response: Response) => boolean;
/**
* Optional detached revalidator used for stale-while-revalidate. Middleware
* `next()` is deliberately never called after a response has been returned,
* because many middleware pipelines are single-use.
*/
revalidate?: (ctx: Context) => Promise<Response>;
onRevalidateError?: (error: unknown, ctx: Context) => void;
}
declare function responseCache(options?: ResponseCacheOptions): Middleware;
interface CacheInvalidationBus {
publish(topic: string, message: unknown): void | Promise<void>;
subscribe(pattern: string, handler: (message: unknown) => void | Promise<void>): () => void;
}
interface DistributedInvalidationOptions {
namespace?: string;
instanceId?: string;
onError?: (error: unknown) => void;
}
interface DistributedInvalidation {
invalidateTag(tag: string): Promise<number>;
invalidateTags(tags: Iterable<string>): Promise<number>;
delete(key: string): Promise<boolean>;
clear(): Promise<void>;
close(): void;
}
/**
* Propagate cache invalidations over any structurally compatible pub/sub bus.
* The bus is intentionally not closed because applications commonly share it.
*/
declare function connectCacheInvalidation<V>(cache: TagCache<V>, bus: CacheInvalidationBus, options?: DistributedInvalidationOptions): DistributedInvalidation;
export { CacheCoordinator, type CacheCoordinatorOptions, type CacheEntry, type CacheEvent, type CacheInspection, type CacheInvalidationBus, type CacheLayerName, type CacheLookup, type CacheSetOptions, type CacheSnapshotEntry, type CachedResponse, type DistributedInvalidation, type DistributedInvalidationOptions, RequestCache, type ResponseCacheOptions, TagCache, type TagCacheOptions, connectCacheInvalidation, responseCache };
Examples
Copy-ready examples from the installed package documentation.
Bounded in-memory/tag caching and HTTP response caching for WRNexusJS. Supports request deduplication, tag invalidation, ETags, fresh/stale states, and optional detached stale revalidation.
import { connectCacheInvalidation, TagCache, responseCache } from "@wrnexus/cache";
const cache = new TagCache({ ttlMs: 60_000, staleWhileRevalidateMs: 300_000 });
export default responseCache({ cache, tags: ["products"] });cross-instance publication; failures remain visible to the caller.
import { connectCacheInvalidation, TagCache } from "@wrnexus/cache";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
const cache = new TagCache({ maxEntries: 10_000 });
const bus = createPubSub(redisDriver(process.env.REDIS_URL));
const invalidation = connectCacheInvalidation(cache, bus, {
namespace: "storefront-production",
onError: (error) => logger.error("cache invalidation failed", { error }),
});
await invalidation.invalidateTag("products");
await invalidation.delete("product:42");
// Unsubscribes this cache only; the shared bus remains owned by the app.
invalidation.close();
await bus.close();Pages and components can opt in declaratively
cache {
scope = "page"
strategy = "stale-while-revalidate"
ttl = "5m"
stale = "10m"
tags = ["catalog", "marketing"]
vary = ["tenant", "language"]
}