Integrations
Emit state transitions over nestjs-diagnostics, mirror them onto @nestjs/event-emitter, and make breaker keys tenant-aware through nestjs-context — all soft-detected and optional.
Resilience plugs into the rest of the ecosystem through event sinks and a context accessor. Every integration is optional and soft-detected — nothing is a hard dependency.
Events
Every state transition is a ResilienceEvent handed to an EventSink:
type ResilienceEventType =
| 'circuit-opened' | 'circuit-closed' | 'circuit-half-open'
| 'short-circuited' | 'failover' | 'timeout' | 'retry';
interface ResilienceEvent {
type: ResilienceEventType;
key?: string; // the breaker key, when applicable
[extra: string]: unknown; // e.g. failover carries target / index / error
}
type EventSink = (event: ResilienceEvent) => void;Pass onEvent to any policy, or let the module wire the sinks for you. Combine several with combineSinks:
import { circuitBreaker, combineSinks, diagnosticsSink } from '@dudousxd/nestjs-resilience';
circuitBreaker({
key: 'payments',
store,
threshold: 5,
cooldownMs: 30_000,
onEvent: combineSinks(diagnosticsSink(), (event) => logger.warn(`resilience: ${event.type} ${event.key ?? ''}`)),
});Diagnostics
diagnosticsSink() routes each event to @dudousxd/nestjs-diagnostics on the aviary:resilience:<type> channel — so Telescope, OpenTelemetry, or any subscriber can observe circuit and failover activity. It's a no-op when diagnostics isn't installed.
ResilienceModule.forRoot({ emit: true }); // default — installs diagnosticsSink for youThe module wires this automatically; you only call diagnosticsSink() by hand when composing onEvent on a standalone policy.
Telescope dashboard
For a first-class view in Telescope — instead of generic diagnostic blobs — add @dudousxd/nestjs-resilience-telescope. It records every transition as a resilience entry and contributes a Resilience dashboard (open circuits, recent failovers, most-tripped circuits, and a table of recent transitions):
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
import { nestjsResilienceTelescope } from '@dudousxd/nestjs-resilience-telescope';
TelescopeModule.forRoot({ extensions: [nestjsResilienceTelescope()] });It subscribes to the aviary:resilience:* channels, so it needs nothing beyond the default emit: true — and costs nothing until resilience emits.
Reacting in-app
To run logic when a transition happens — invalidate a cache when a circuit opens, page on repeated failovers — subscribe to the diagnostics channel directly. This needs nothing beyond diagnostics (already present whenever emit: true is on, the default), so there's no extra event library to install:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { getChannel, type DiagnosticEvent } from '@dudousxd/nestjs-diagnostics';
import type { ResilienceEvent } from '@dudousxd/nestjs-resilience';
@Injectable()
export class CircuitReactions implements OnModuleInit {
onModuleInit() {
getChannel('resilience', 'circuit-opened').subscribe((msg) => {
const event = (msg as DiagnosticEvent).payload as ResilienceEvent;
this.cache.invalidate(event.key);
});
}
}There's one channel per ResilienceEventType under aviary:resilience:<type> — subscribe to each you care about (circuit-opened, short-circuited, failover, …).
Prefer raw onEvent sinks over the diagnostics channel? Pass any EventSink (or combineSinks(...)) to a policy — see Events above. Either way you stay on one event stream that observability tools read too.
Tenant-aware circuit keys
In a multi-tenant app you usually want a circuit per tenant — one tenant's failing provider shouldn't trip the breaker for everyone. tenantSuffix() reads the current tenant from @dudousxd/nestjs-context (via a shared Symbol, returning undefined when context isn't present), so you can fold it into the key:
import { circuitBreaker, tenantSuffix } from '@dudousxd/nestjs-resilience';
circuitBreaker({
key: `payments:${tenantSuffix() ?? 'global'}`,
store,
threshold: 5,
cooldownMs: 30_000,
});Because the key carries the tenant, a distributed store keeps each tenant's circuit isolated and fleet-wide at the same time.
Stores
Share circuit-breaker state across instances with a pluggable ResilienceStore — in-memory by default, or Redis / Postgres / SQLite adapters that coordinate the half-open probe atomically.
Testing
Drive time deterministically with FakeClock so timeouts, backoff and cooldowns are instant and reproducible — and validate a custom store against the shared contract suite.