The main orchestrator that manages an ordered stack of cache adapters.
import { CacheManager } from "@ziggurat-cache/core";new CacheManager(options: CacheManagerOptions)CacheManagerOptions:
| Property | Type | Default | Description |
|---|---|---|---|
layers |
CacheAdapter[] |
(required) | Ordered array of cache layers. L1 is index 0 (fastest). |
namespace |
string |
none | Prefix prepended to all keys as namespace:key. Useful for logical grouping. |
syncBackfill |
boolean |
false |
When true, waits for backfill to complete before returning. |
stampede |
StampedeConfig |
{ coalesce: true } |
Stampede protection configuration. |
events |
TypedEventEmitter<CacheEventMap> |
(auto-created) | Optional shared event emitter for observability. If omitted, an internal one is created. |
StampedeConfig:
| Property | Type | Default | Description |
|---|---|---|---|
coalesce |
boolean |
true |
Combine concurrent cache misses into a single factory call. |
Queries layers sequentially from L1 to Ln. Returns the first hit and backfills higher layers. Returns null on a complete miss.
const entry = await cache.get<User>("user:42");
if (entry) {
console.log(entry.value); // User
console.log(entry.expiresAt); // number | null
}Writes the value to all layers. TTL is in milliseconds. Omit for no expiration.
await cache.set("user:42", userData, 300_000);Removes the key from all layers.
await cache.delete("user:42");Returns the configured adapter layers in order (L1 at index 0). Each call returns a new array — mutations to the returned array do not affect the manager's internal state. The adapter references are the same objects passed to the constructor.
Use this to perform adapter-level operations like clear(), flushAll(), or keys() on individual layers:
const [l1, l2] = cache.getLayers();
// Clear a specific layer
await l1.clear();
// Clear all layers explicitly
for (const layer of cache.getLayers()) {
await layer.clear();
}
// Get keys from a specific layer
const keys = await l2.keys();
// Flush a specific layer
await l2.flushAll();Note: Adapters returned by
getLayers()do not apply the manager's namespace — they expose their raw API.
The primary API. Checks the cache first. On a miss, calls the factory, caches the result across all layers, and returns it. Includes stampede protection via request coalescing.
const user = await cache.wrap(
"user:42",
async () => db.users.findById(42),
300_000,
);Behavior:
- Check all layers sequentially (like
get). - If found, return
entry.value. - If not found and coalescing is enabled, check for an in-flight request for the same key. If one exists, attach to it.
- Otherwise, call the factory, store the result via
set, and return the value. - If the factory throws, the error propagates to all coalesced callers and the in-flight entry is cleaned up.
Alias for delete. Convenience method for developers coming from Redis-style APIs.
Batch get across layers with backfill. Missing keys are absent from the returned Map. Layers are queried sequentially with a shrinking remaining set — keys found in L1 are not queried in L2.
const entries = await cache.mget<number>(["a", "b", "missing"]);
entries.get("a")?.value; // number
entries.has("missing"); // falseBatch set across all layers with optional per-entry TTL. Writes to all layers in parallel.
await cache.mset([
{ key: "a", value: 1, ttlMs: 60_000 },
{ key: "b", value: 2 },
]);Batch delete across all layers.
await cache.mdel(["a", "b"]);Returns TTL information from the first layer with a hit. Returns a discriminated union:
const result = await cache.getTtl("key");
switch (result.kind) {
case "missing":
break; // key not found
case "permanent":
break; // key exists, no expiration
case "expiring": // key exists, expires in result.ttlMs ms
console.log(result.ttlMs);
break;
}Check existence across layers. Returns true on first hit.
Subscribe to cache events for observability. Returns an unsubscribe function. Events are emitted synchronously and have zero cost when no listeners are attached.
// Track hit/miss ratio
const unsub = cache.on("hit", (e) => {
console.log(`Hit on ${e.key} from layer ${e.layerName} in ${e.durationMs}ms`);
});
// Later: stop listening
unsub();Available events:
| Event | Key Fields | Emitted When |
|---|---|---|
hit |
key, layerName, layerIndex, durationMs |
get() finds a value in any layer |
miss |
key, durationMs |
get() exhausts all layers |
set |
key, ttlMs, durationMs |
set() writes to all layers |
delete |
key, durationMs |
delete() removes from all layers |
error |
key, operation, layerName, layerIndex, error |
Any layer throws during an operation |
backfill |
key, sourceLayerName, sourceLayerIndex, targetLayerNames |
A lower-layer hit triggers backfill |
wrap:hit |
key, durationMs |
wrap() finds a cached value |
wrap:miss |
key, durationMs, factoryDurationMs |
wrap() calls the factory |
wrap:coalesce |
key |
wrap() joins an in-flight request |
mget |
keys, hitCount, missCount, durationMs |
mget() completes |
mset |
keyCount, durationMs |
mset() completes |
mdel |
keyCount, durationMs |
mdel() completes |
All events include an optional namespace field when the manager has a namespace configured.
See Observability for OpenTelemetry integration.
Input type for mset with optional per-entry TTL.
interface CacheSetEntry<T> {
key: string;
value: T;
ttlMs?: number;
}Discriminated union returned by getTtl.
type TtlResult =
| { kind: "missing" }
| { kind: "permanent" }
| { kind: "expiring"; ttlMs: number };Abstract class that implements CacheAdapter with default implementations for all extended methods. New adapters should extend this class and only implement the 4 core methods: get, set, delete, clear.
import { BaseCacheAdapter } from "@ziggurat-cache/core";See Custom Adapters for a guide on building your own.
In-process cache adapter backed by node-cache.
import { MemoryAdapter } from "@ziggurat-cache/core";new MemoryAdapter(options?: MemoryAdapterOptions)MemoryAdapterOptions:
| Property | Type | Default | Description |
|---|---|---|---|
defaultTtlMs |
number |
none | Default TTL in milliseconds applied to all entries. Takes precedence over TTL passed via set/wrap. |
// Unbounded, no default TTL
const memory = new MemoryAdapter();
// 30-second default TTL
const memory = new MemoryAdapter({ defaultTtlMs: 30_000 });| Property | Type | Value |
|---|---|---|
name |
string |
"memory" |
Implements the full CacheAdapter interface including has, getTtl, keys, mget, mset, mdel, flushAll.
- Expired entries (past their TTL) are lazily cleaned up on
get. - Overrides
has,getTtl,keys, andflushAllusing nativenode-cachemethods for better performance.
The standard return type from adapter get methods.
interface CacheEntry<T> {
value: T;
expiresAt: number | null; // Unix timestamp in ms, or null for no expiration
}The contract every storage backend must implement.
interface CacheAdapter {
readonly name: string;
get<T>(key: string): Promise<CacheEntry<T> | null>;
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
has(key: string): Promise<boolean>;
getTtl(key: string): Promise<TtlResult>;
keys(): Promise<string[]>;
mget<T>(keys: readonly string[]): Promise<Map<string, CacheEntry<T>>>;
mset<T>(entries: readonly CacheSetEntry<T>[]): Promise<void>;
mdel(keys: readonly string[]): Promise<void>;
flushAll(): Promise<void>;
}See Custom Adapters for a guide on building your own. Extend BaseCacheAdapter for default implementations of all extended methods.
Cache adapter for Redis using ioredis. Stores values as JSON strings.
import { RedisAdapter } from "@ziggurat-cache/redis";new RedisAdapter(options: RedisAdapterOptions)RedisAdapterOptions:
| Property | Type | Default | Description |
|---|---|---|---|
client |
Redis (ioredis) |
(required) | A configured ioredis client instance. |
prefix |
string |
"" |
Key prefix for infrastructure-level isolation. All keys are stored as prefix + key. |
defaultTtlMs |
number |
none | Default TTL in milliseconds applied to all entries. Takes precedence over TTL passed via set/wrap. |
import Redis from "ioredis";
const adapter = new RedisAdapter({
client: new Redis("redis://localhost:6379"),
prefix: "myapp:",
defaultTtlMs: 600_000, // 10-minute default TTL
});| Property | Type | Value |
|---|---|---|
name |
string |
"redis" |
Implements the full CacheAdapter interface.
get: Fetches the key from Redis, parses the JSON, and checks expiration. Returnsnullfor missing or expired keys.set: Serializes the value as{ value, expiresAt }JSON. UsesPSETEXfor entries with TTL,SETfor entries without.delete: Deletes the prefixed key.clear: Scans for all keys matching the prefix pattern and deletes them using a pipeline. Pipeline command failures throwAggregateError.mget: Uses a pipeline for batch reads. Per-key read errors are skipped and the successful entries are returned — this meansmget()may return a partial result map rather than rejecting the entire batch.mset: Uses a pipeline for batch writes. Entries withttlMs <= 0are skipped. Pipeline command failures throwAggregateError.
See Redis Adapter for detailed usage.
NestJS dynamic module that provides a CacheManager via dependency injection.
import { ZigguratModule } from "@ziggurat-cache/nestjs";Synchronous registration. Creates the CacheManager immediately.
@Module({
imports: [
ZigguratModule.forRoot({
layers: [new MemoryAdapter()],
}),
],
})
export class AppModule {}The module is registered globally — the CacheManager is available for injection in any module without re-importing.
Asynchronous registration. Use when your cache configuration depends on other providers (e.g., ConfigService).
ZigguratModuleAsyncOptions:
| Property | Type | Description |
|---|---|---|
imports |
any[] |
Modules to import (e.g., ConfigModule). |
useFactory |
(...args) => CacheManagerOptions | Promise<CacheManagerOptions> |
Factory function that returns the options. |
inject |
any[] |
Providers to inject into the factory. |
@Module({
imports: [
ZigguratModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
layers: [
new MemoryAdapter({ defaultTtlMs: config.get("CACHE_DEFAULT_TTL") }),
new RedisAdapter({ client: new Redis(config.get("REDIS_URL")) }),
],
}),
}),
],
})
export class AppModule {}Method decorator that transparently wraps a service method with CacheManager.wrap().
import { Cached } from "@ziggurat-cache/nestjs";@Cached(options: CachedDecoratorOptions)CachedDecoratorOptions:
| Property | Type | Description |
|---|---|---|
key |
(...args: any[]) => string |
Function that receives the method's arguments and returns the cache key. |
ttlMs |
number |
(optional) TTL in milliseconds. |
@Injectable()
export class ProductService {
@Cached({
key: (id: string) => `product:${id}`,
ttlMs: 600_000,
})
async getProduct(id: string) {
return this.db.products.findById(id);
}
}The decorator injects the CacheManager automatically. Stampede protection (request coalescing) is included — concurrent calls for the same key share a single method invocation.
Injection token for the CacheManager instance. Use this for manual injection:
import { Inject } from "@nestjs/common";
import { CACHE_MANAGER } from "@ziggurat-cache/nestjs";
import type { CacheManager } from "@ziggurat-cache/core";
@Injectable()
export class MyService {
constructor(@Inject(CACHE_MANAGER) private cache: CacheManager) {}
async doSomething() {
return this.cache.wrap("key", () => this.expensiveWork());
}
}Connects a CacheManager's event system to OpenTelemetry metrics. Requires @opentelemetry/api as a peer dependency — bring your own OTel SDK and exporter.
import { instrumentCacheManager } from "@ziggurat-cache/otel";instrumentCacheManager(cacheManager: CacheManager, options?: InstrumentationOptions): () => voidInstrumentationOptions:
| Property | Type | Default | Description |
|---|---|---|---|
meterName |
string |
"ziggurat" |
Name passed to metrics.getMeter(). |
Returns a cleanup function that unsubscribes all listeners.
Counters:
| Metric Name | Attributes | Description |
|---|---|---|
ziggurat.cache.hit |
cache.layer |
Cache hits |
ziggurat.cache.miss |
Cache misses | |
ziggurat.cache.set |
Set operations | |
ziggurat.cache.delete |
Delete operations | |
ziggurat.cache.error |
cache.layer, cache.operation |
Layer errors |
ziggurat.cache.backfill |
cache.source_layer |
Backfill events |
ziggurat.cache.wrap.hit |
Wrap cache hits | |
ziggurat.cache.wrap.miss |
Wrap cache misses (factory called) | |
ziggurat.cache.wrap.coalesce |
Coalesced requests (stampede prevention) |
Histograms:
| Metric Name | Attributes | Unit | Description |
|---|---|---|---|
ziggurat.cache.duration |
cache.operation, cache.layer |
ms | Duration of cache operations |
ziggurat.cache.wrap.factory_duration |
ms | Duration of wrap factory calls |
import { CacheManager, MemoryAdapter } from "@ziggurat-cache/core";
import { instrumentCacheManager } from "@ziggurat-cache/otel";
const cache = new CacheManager({
layers: [new MemoryAdapter()],
});
// Start recording metrics
const cleanup = instrumentCacheManager(cache, { meterName: "my-app" });
// Use cache normally — metrics are recorded automatically
await cache.wrap("key", () => fetchData());
// On shutdown
cleanup();