Skip to content

@studnicky/resilience ​

Retry/backoff, circuit breaker, token bucket and sliding-window rate limiters, and a bounded dead-letter queue. Each primitive is independently usable and composable.

Install ​

bash
pnpm add @studnicky/resilience

Requires @studnicky:registry=https://npm.pkg.github.com in .npmrc.

Construct core runtime primitives through @studnicky/resilience/node in Node or @studnicky/resilience/browser in browsers. Retry uses @studnicky/resilience/retry/node or @studnicky/resilience/retry/browser; its schema-backed data and type-only contracts live at @studnicky/resilience/retry/entities and @studnicky/resilience/retry/interfaces. Core schema-backed data declarations live at @studnicky/resilience/entities, and core type-only contracts live at @studnicky/resilience/interfaces.

Northstar Books supplier boundary ​

Northstar Books protects catalogue and inventory-supplier calls at three different failure boundaries. A circuit breaker stops repeated requests to an unhealthy supplier; a token bucket limits the process-wide demand budget; and a dead-letter queue retains a rejected replenishment task for deliberate recovery. KeyedRateLimiter from @studnicky/resilience/keyed gives each supplier account or tenant an independent budget while bounded key retention prevents an unbounded registry.

Each primitive makes one guarantee only: breakers reject while their circuit is open, limiters admit only available local capacity, and queues retain only their bounded in-memory entries. These controls do not replace a supplier-wide or durable distributed rate limit; Northstar applies those at the supplier or persistence boundary.

Usage ​

Retry ​

Retry executes an operation until it succeeds, its retry budget is exhausted, or its caller-supplied error classifier rejects the failure. Import the executable primitive from @studnicky/resilience/retry/node or @studnicky/resilience/retry/browser; keep Retry configuration and collaborator contracts at @studnicky/resilience/retry/interfaces.

CircuitBreaker ​

Northstar's supplier catalogue lookup is healthy right up until it isn't — and hammering a dead supplier with retries only makes things worse. This example trips a breaker after three failures, confirms the very next call gets rejected instantly with CircuitBreakerOpenError instead of hitting the supplier again, then advances a virtual clock past the reset timeout to watch it probe, recover after two successful calls, and settle back to closed.

ts
import { CircuitBreaker, CircuitBreakerOpenError } from '../src/index.js';

// Deterministic clock so tests are instant with no real waits.
let now = 0;
class Clock {
  static now(): number { const result = now + 0; return result; }
}

const breaker = CircuitBreaker.create(
  {
    'failureThreshold': 3,
    'name': 'northstar-supplier-catalogue',
    'resetTimeoutMs': 1_000,
    'successThreshold': 2
  },
  { 'clock': Clock.now }
);

// --- CLOSED: a supplier catalogue lookup succeeds ---
await breaker.execute(() => { const result = Promise.resolve('catalogue-available'); return result; });
console.log('State after success:', breaker.state);

// --- Trip the breaker: three supplier failures protect checkout ---
class Fail {
  static boom(): Promise<never> { throw RuntimeError.create('supplier unavailable'); }
}
for (let i = 0; i < 3; i++) {
  await breaker.execute(Fail.boom).catch(() => { /* expected */ });
}
console.log('State after 3 failures:', breaker.state);

// --- OPEN: next call is fast-rejected with CircuitBreakerOpenError ---
await breaker.execute(() => { const result = Promise.resolve('supplier call must not run'); return result; }).catch((error) => {
  console.log('Open-circuit rejection:', error instanceof CircuitBreakerOpenError ? 'CircuitBreakerOpenError' : 'other');
});

// --- Advance past resetTimeoutMs → halfOpen on next call ---
now = 1_001;
await breaker.execute(() => { const result = Promise.resolve('supplier probe 1'); return result; });
console.log('State after probe 1:', breaker.state);

// --- 2 successes in halfOpen close the circuit (successThreshold: 2) ---
await breaker.execute(() => { const result = Promise.resolve('supplier probe 2'); return result; });
console.log('State after probe 2:', breaker.state);

// --- forceOpen / reset ---
breaker.forceOpen();
console.log('State after forceOpen:', breaker.state);
breaker.reset();
console.log('State after reset:', breaker.state);

TokenBucket ​

A token bucket is really just a budget that refills itself over time — spend it with consume(), or queue up and wait for more with waitForToken(). This example drains a 3-token burst capacity immediately, watches it slowly refill as a virtual clock advances, then drains it again and aborts an in-flight waitForToken() call with an AbortSignal, confirming the wait itself is cancellable, not just the token math.

ts
import { TokenBucket, TokenBucketExhaustedError } from '../src/index.js';

// Deterministic clock: start at t=0, advance manually.
let now = 0;
class Clock {
  static now(): number { const result = now + 0; return result; }
}

// 2 tokens/s, burst capacity 3 → starts full (3 tokens).
const bucket = TokenBucket.create({ 'burstSize': 3, 'clock': Clock.now, 'requestsPerSecond': 2 });
console.log('Initial available tokens:', bucket.available);

// --- consume() drains tokens ---
const firstAdmission = bucket.consume();
bucket.consume();
bucket.consume();
console.log('First admission:', firstAdmission);
console.log('Available after 3 consumes:', bucket.available);

// --- Advance 500 ms → 1 new token (2 tokens/s × 0.5 s = 1) ---
now = 500;
console.log('Available at t=500ms:', bucket.available);
bucket.consume();

// --- Advance to 1500 ms → 2 tokens refilled, capped at burstSize=3 ---
now = 1_500;
console.log('Available at t=1500ms:', bucket.available);

// --- waitForToken with abort signal cancels correctly ---
// Drain remaining tokens so waitForToken has to block, then abort concurrently.
bucket.consume();
bucket.consume();
const controller = new AbortController();
const abortError = RuntimeError.create('cancelled');
// Abort after a microtask so waitForToken is already suspended in the Promise.
const waitPromise = bucket.waitForToken({ 'signal': controller.signal, 'tokens': 1 });
queueMicrotask(() => { controller.abort(abortError); });
const abortRejected = await waitPromise.then(() => { const result = false; return result; }).catch(() => { const result = true; return result; });
console.log('waitForToken aborted:', abortRejected);

KeyedRateLimiter ​

One slow-moving supplier account shouldn't be able to starve every other supplier's request budget — KeyedRateLimiter gives each key its own independent TokenBucket by default. This example tracks three suppliers against a registry capped at two keys: the first two get their own budgets, exhausting one doesn't touch the other, and adding a third evicts the least-recently-used supplier's budget — all visible through telemetry hooks firing on creation, eviction, exhaustion, and every successful token grab. A second scenario swaps in a hand-written FixedAllowance strategy to show the limiter works with any object matching RateLimiterStrategyInterface, not just TokenBucket.

ts
import type { RateLimitConsumptionEntity } from '../src/entities/RateLimitConsumptionEntity.js';
import type { RateLimitConsumptionInterface } from '../src/interfaces/RateLimitConsumptionInterface.js';
import type { RateLimiterStrategyInterface } from '../src/keyed/index.js';
import type { KeyedRateLimiterCreateConfigInterface } from '../src/keyed/interfaces/index.js';

import { KeyedRateLimiter } from '../src/keyed/index.js';
import { TokenBucketExhaustedError } from '../src/TokenBucketExhaustedError.js';

const telemetryEvents: string[] = [];

class TelemetryKeyedRateLimiter extends KeyedRateLimiter {
  static build(config: KeyedRateLimiterCreateConfigInterface): TelemetryKeyedRateLimiter {
    return new TelemetryKeyedRateLimiter(super.createDefaultDependencies(config));
  }
  protected override onKeyCreated(key: string): void {
    console.log(`[keyed-rate-limiter] key created key=${key}`);
    telemetryEvents.push(`created:${key}`);
  }

  protected override onKeyEvicted(key: string): void {
    console.log(`[keyed-rate-limiter] key evicted key=${key}`);
    telemetryEvents.push(`evicted:${key}`);
  }

  protected override onLimitExceeded(key: string): void {
    console.log(`[keyed-rate-limiter] limit exceeded key=${key}`);
    telemetryEvents.push(`exceeded:${key}`);
  }

  protected override onTokenAcquired(
    key: string,
    result: RateLimitConsumptionEntity.Type
  ): void {
    console.log(
      `[keyed-rate-limiter] token acquired key=${key} consumed=${result.consumedTokens} remaining=${result.remainingTokens}`
    );
    telemetryEvents.push(`acquired:${key}:${result.consumedTokens}:${result.remainingTokens}`);
  }
}

const limiter = TelemetryKeyedRateLimiter.build({
  'burstSize': 2,
  'clock': () => {
    const epoch = new Date(0);
    const result = epoch.getTime();
    return result;
  },
  'maximumKeys': 2,
  'requestsPerSecond': 1
});

// Two suppliers have independent request budgets.
limiter.consume('supplier:paper-trail');
limiter.consume('supplier:paper-trail');

try {
  limiter.consume('supplier:paper-trail'); // exhausted
} catch (error) {
  if (!(error instanceof TokenBucketExhaustedError)) { throw error; }
}

limiter.consume('supplier:archive-house'); // unaffected by user-a's exhaustion

// maximumKeys: 2 — a third supplier evicts the least-recently-used budget.
limiter.consume('supplier:rare-leaf');

console.log('Events:', telemetryEvents);

// The generic extension point: any object matching RateLimiterStrategyInterface
// slots in without a second wrapper class — no import of, or coupling to,
// TokenBucket required.
class FixedAllowance implements RateLimiterStrategyInterface {
  #remaining: number;
  constructor(allowance: number) { this.#remaining = allowance; }
  consume(tokens = 1): RateLimitConsumptionInterface {
    if (this.#remaining < tokens) { throw RuntimeError.create('exhausted'); }
    this.#remaining -= tokens;
    return { 'consumedTokens': tokens, 'remainingTokens': this.#remaining };
  }
  waitForToken(
    options?: { 'signal'?: AbortSignal; 'tokens'?: number }
  ): Promise<RateLimitConsumptionInterface> {
    const result = Promise.resolve(this.consume(options?.tokens ?? 1));
    return result;
  }
}

const genericLimiter = KeyedRateLimiter.create<FixedAllowance>({
  'factory': (_key) => {return new FixedAllowance(3);}
});

genericLimiter.consume('supplier:advance-reader-copy', 3);

SlidingWindowLimiter ​

Use a sliding window when a limit is a fixed number of requests over a rolling interval. Select log for exact accounting or counter for a constant-space approximation. Both consume and waitForToken return the same RateLimitConsumptionInterface admission shape as TokenBucket.

typescript
import { SlidingWindowExhaustedError, SlidingWindowLimiter } from "@studnicky/resilience/node";

const limiter = SlidingWindowLimiter.create({
  algorithm: "log",
  limit: 100,
  windowMs: 60_000,
});

try {
  const admission = limiter.consume();
  console.log(admission.remainingTokens);
  await sendRequest();
} catch (error) {
  if (error instanceof SlidingWindowExhaustedError) {
    // The request does not fit in the current rolling window.
  }
}

DeadLetterQueue ​

When an order's replenishment task fails, Northstar doesn't want it lost, just set aside for deliberate recovery. DeadLetterQueue is a fixed-capacity FIFO queue drained with an async generator rather than a callback — the example below enqueues a couple of failed jobs and drains them, proves capacity rejects a queue that's full, then feeds a closed queue into DeadLetterQueueRetryGenerator to re-yield its entries with a configurable pause between each, finishing with a queue that's aborted before anything is ever drained from it.

DeadLetterQueueRetryGenerator: timed re-delivery ​

ts
import {
  DeadLetterQueue,
  DeadLetterQueueClosedError,
  DeadLetterQueueFullError,
  DeadLetterQueueRetryGenerator
} from '../src/index.js';

await (async function runDeadLetterQueueExample(): Promise<void> {
  // --- Basic enqueue and drain ---
  const deadLetterQueue = DeadLetterQueue.create<string>({ 'capacity': 5 });

  deadLetterQueue.enqueue('job-1', 'timeout');
  deadLetterQueue.enqueue('job-2', 'network error', RuntimeError.create('ECONNREFUSED'));
  console.log('Queue size after 2 enqueues:', deadLetterQueue.size);

  // Close before draining so the generator terminates instead of waiting.
  deadLetterQueue.close();

  const collected: string[] = [];
  for await (const entry of deadLetterQueue.drain()) {
    collected.push(entry.item);
  }
  console.log('Drained items:', collected);
  console.log('Queue size after drain:', deadLetterQueue.size);

  // --- Capacity enforcement ---
  const bounded = DeadLetterQueue.create<number>({ 'capacity': 2 });
  bounded.enqueue(1, 'err');
  bounded.enqueue(2, 'err');
  console.log('Bounded queue size:', bounded.size);

  // --- DeadLetterQueueRetryGenerator re-yields entries with a pause ---
  const retryDeadLetterQueue = DeadLetterQueue.create<string>();
  retryDeadLetterQueue.enqueue('retry-job-1', 'failed');
  retryDeadLetterQueue.enqueue('retry-job-2', 'failed');
  retryDeadLetterQueue.close();

  const generator = DeadLetterQueueRetryGenerator.create({ 'deadLetterQueue': retryDeadLetterQueue, 'intervalMs': 0 });
  const retried: string[] = [];
  for await (const entry of generator.generate()) {
    retried.push(entry.item);
  }
  console.log('Retried items:', retried);

  // --- AbortSignal aborts the queue on construction ---
  const controller = new AbortController();
  controller.abort();
  const abortedDeadLetterQueue = DeadLetterQueue.create<string>({ 'signal': controller.signal });
  const abortedEntries: string[] = [];
  for await (const entry of abortedDeadLetterQueue.drain()) {
    abortedEntries.push(entry.item);
  }
  console.log('Aborted drain count:', abortedEntries.length);

  assert.deepEqual(collected, ['job-1', 'job-2']);
  assert.equal(deadLetterQueue.size, 0);
  assert.throws(() => { deadLetterQueue.enqueue('job-3', 'late'); }, DeadLetterQueueClosedError);
  assert.throws(() => { bounded.enqueue(3, 'overflow'); }, DeadLetterQueueFullError);
  assert.deepEqual(retried, ['retry-job-1', 'retry-job-2']);
  assert.equal(abortedEntries.length, 0);

  console.log('dead-letter-queue: all assertions passed');
})();

Lifecycle hooks ​

Subclass a primitive when the application needs lifecycle telemetry. Hooks are protected synchronous methods with no-op defaults. Keep an override fast and non-throwing; the documented operation result and error behavior remain the consumer contract.

CircuitBreaker hooks ​

HookWhen it firesArgs
onSuccess()A protected call resolves.—
onFailure(error)A classified circuit failure is recorded.error: Error
onTrip()The circuit reaches its failure threshold.—
onOpen()The circuit enters the open state.—
onHalfOpen()The reset timeout opens a probe window.—
onClose()The circuit enters the closed state.—
onReject()A call is rejected while the circuit is open.—

TokenBucket hooks ​

HookWhen it firesArgs
onTokenAcquired(count)Tokens are deducted by consume() or waitForToken().count: number
onTokenDepleted()consume() cannot admit the requested tokens.—
onRefill(added)Elapsed time adds tokens.added: number

SlidingWindowLimiter hooks ​

HookWhen it firesArgs
onAllow(count)consume() admits the requested count.count: number
onReject(count)consume() rejects the requested count.count: number
onWindowRoll()The active rolling window advances.—

DeadLetterQueue hooks ​

HookWhen it firesArgs
onEnqueue(item)An item is added to the queue.item: T
onDequeue(item)An item is yielded from drain().item: T
onOverflow()enqueue() reaches capacity.—
onClose()close() completes.—
onAbort()abort() completes.—

DeadLetterQueueRetryGenerator hooks ​

HookWhen it firesArgs
onYield(entry)generate() yields an entry.entry: DeadLetterQueueEntryInterface<T>
onWait(intervalMs)generate() begins an inter-entry delay.intervalMs: number
onDone()generate() finishes.—
ts
import {
  CircuitBreaker,
  type CircuitBreakerCollaboratorsInterface,
  CircuitBreakerOpenError,
  DeadLetterQueue,
  type DeadLetterQueueOptionsInterface
} from '../src/index.js';

// --- Observed CircuitBreaker ---
class TracedBreaker extends CircuitBreaker {
  constructor(config: unknown, collaborators: CircuitBreakerCollaboratorsInterface = {}) { super(config, collaborators); }
  protected override onSuccess(): void { console.log('[resilience:cb] onSuccess — circuit closed and call succeeded'); }
  protected override onFailure(error: Error): void { console.log(`[resilience:cb] onFailure — error=${error.message}`); }
  protected override onTrip(): void { console.log('[resilience:cb] onTrip — failure threshold reached, circuit OPEN'); }
  protected override onOpen(): void { console.log('[resilience:cb] onOpen — circuit is now open'); }
  protected override onHalfOpen(): void { console.log('[resilience:cb] onHalfOpen — probing after timeout'); }
  protected override onClose(): void { console.log('[resilience:cb] onClose — circuit CLOSED, service healthy'); }
  protected override onReject(): void { console.log('[resilience:cb] onReject — call short-circuited (circuit open)'); }
}

// --- Observed DeadLetterQueue ---
class TracedDeadLetterQueue<T> extends DeadLetterQueue<T> {
  constructor(options?: DeadLetterQueueOptionsInterface) { super(options); }
  protected override onEnqueue(item: T): void { console.log(`[resilience:dlq] onEnqueue — item=${String(item)}`); }
  protected override onDequeue(item: T): void { console.log(`[resilience:dlq] onDequeue — item=${String(item)}`); }
  protected override onOverflow(): void { console.log('[resilience:dlq] onOverflow — queue full, item dropped'); }
  protected override onClose(): void { console.log('[resilience:dlq] onClose — queue sealed'); }
  protected override onAbort(): void { console.log('[resilience:dlq] onAbort — queue aborted'); }
}

const events: string[] = [];

// Scenario: breaker trips open → half-open → closes; rejected calls go to DLQ
let time = 0;
class Clock {
  static now(): number { const result = time + 0; return result; }
}

const circuitBreaker = new TracedBreaker(
  { 'failureThreshold': 2, 'resetTimeoutMs': 100, 'successThreshold': 1 },
  { 'clock': Clock.now }
);
const deadLetterQueue = new TracedDeadLetterQueue<string>({ 'capacity': 10 });

// Trip the breaker open with 2 failures
console.log('\n--- Tripping open ---');
try { await circuitBreaker.execute(() => { throw RuntimeError.create('service down'); }); } catch { deadLetterQueue.enqueue('msg-1', 'service-failure'); events.push('failure-1'); }
try { await circuitBreaker.execute(() => { throw RuntimeError.create('service down'); }); } catch { deadLetterQueue.enqueue('msg-2', 'service-failure'); events.push('failure-2'); }

// Call while open — should be rejected and sent to DLQ
console.log('\n--- Rejecting while open ---');
try {
  await circuitBreaker.execute(() => { const result = Promise.resolve('ok'); return result; });
} catch (error) {
  if (error instanceof CircuitBreakerOpenError) {
    deadLetterQueue.enqueue('msg-3', 'circuit-open');
    events.push('rejected');
  }
}

// Advance clock past resetTimeoutMs → half-open
console.log('\n--- Half-open probe ---');
time = 100;
await circuitBreaker.execute(() => { const result = Promise.resolve('ok'); return result; }); // half-open → closed
events.push('recovered');

// Drain the DLQ
console.log('\n--- Draining DLQ ---');
deadLetterQueue.close();
for await (const entry of deadLetterQueue.drain()) {
  events.push(`drain:${entry.item}`);
}

The base class never calls any logger or metrics library. All hooks are no-ops by default.

Try it ​

This is the same breaker-plus-DLQ pattern a lot of Northstar's supplier code actually needs: when a call fails, send the failed work to a dead-letter queue instead of losing it. Watch the full scenario play out — two failures trip the breaker through onFailure, onTrip, and onOpen; a call made while it's open gets rejected via onReject; advancing a virtual clock into the half-open probe window and succeeding fires onHalfOpen, onSuccess, and onClose; and finally draining the dead-letter queue emits onDequeue for every item it had been holding onto.

Loading example…

Run this to compare the two sliding-window accounting strategies side by side: the exact log algorithm admits weighted requests until the limit, rejects the next one, then succeeds again once the window fully elapses and stale entries are pruned; the approximate counter algorithm tracks the same budget in constant memory, rejecting at the limit and recovering as its blended estimate decays. A third scenario exhausts a single-slot limiter and calls waitForToken(), which resolves only once the decaying estimate drops back under the limit.

Loading example…

Run this to see the per-key isolation and eviction from the KeyedRateLimiter example above happen live: two suppliers draw down independent budgets, exhausting one doesn't touch the other, and adding a third supplier past the two-key limit evicts the least-recently-used budget — each step logged through the telemetry hooks as it happens.

Loading example…

What it is ​

@studnicky/resilience is a collection of independently composable retry, circuit-breaker, rate-limiting, and bounded dead-letter primitives. Each primitive makes a local control-flow or capacity guarantee; the package does not provide a supplier integration, durable queue, distributed limiter, or recovery product.

What it is for ​

Northstar Books composes these primitives at supplier and inventory boundaries: it retries a transient catalogue fetch, stops calls to an unhealthy vendor, budgets local demand, gives each vendor account its own allowance, and retains a bounded failed task for a recovery flow it owns. Node and browser are runtime alternatives. entities validate data at a boundary, interfaces define contracts, and the keyed and retry subpackages expose focused primitives rather than duplicate applications.

Northstar Books examples ​

  • Resilience lifecycle hooks solves the “a supplier fails repeatedly while a replenishment task needs deliberate recovery” problem. It observes a circuit opening, rejecting calls, closing after a successful probe, and draining failed work, proving that Northstar receives lifecycle evidence without changing primitive outcomes.
  • Sliding-window rate limiting solves the “do not exceed a vendor’s rolling catalogue-query allowance” problem. It records admission and rejection in a rolling time window, proving that Northstar can select exact or bounded-space accounting for its own supplier rule.
  • Per-key token buckets with LRU eviction solves the “one busy supplier account must not consume every catalogue-import slot” problem. It gives keys independent capacity and evicts inactive keys, proving that Northstar can bound the local registry while isolating demand.

Public entrypoints ​

Import pathUse it when
@studnicky/resilience/nodeNorthstar applies core resilience primitives in a Node supplier or inventory service.
@studnicky/resilience/browserNorthstar applies the browser-safe core primitives in a bookseller interface.
@studnicky/resilience/entitiesNorthstar validates core limiter, breaker, queue, and event data at a boundary.
@studnicky/resilience/interfacesNorthstar types its own clocks, classifiers, queue entries, and primitive collaborators.
@studnicky/resilience/keyedNorthstar gives each supplier account or tenant an independently bounded local rate-limiting strategy.
@studnicky/resilience/keyed/entitiesNorthstar validates keyed-limiter configuration and state data.
@studnicky/resilience/keyed/interfacesNorthstar implements a keyed rate-limiting strategy contract without coupling to a provider.
@studnicky/resilience/retry/nodeNorthstar executes a Node-side supplier operation under its chosen retry and backoff policy.
@studnicky/resilience/retry/browserNorthstar executes a browser-side operation under its chosen retry and backoff policy.
@studnicky/resilience/retry/entitiesNorthstar validates retry data before a retry boundary consumes it.
@studnicky/resilience/retry/interfacesNorthstar supplies retry configuration and collaborators through type contracts.

Exports ​

SymbolPurposeImport path
CircuitBreakerThree-state async circuit breaker.@studnicky/resilience/node
CircuitBreakerOpenErrorSignals a call rejected by an open circuit.@studnicky/resilience/node
CircuitBreakerCollaboratorsInterfaceTyped clock and error-classifier collaborators CircuitBreaker.create accepts alongside schema-validated config.@studnicky/resilience/interfaces
DeadLetterQueue<T>Bounded FIFO queue with async-generator drain.@studnicky/resilience/node
DeadLetterQueueAbortedErrorSignals enqueue after queue abort.@studnicky/resilience/node
DeadLetterQueueClosedErrorSignals enqueue after queue close.@studnicky/resilience/node
DeadLetterQueueFullErrorSignals enqueue at queue capacity.@studnicky/resilience/node
DeadLetterQueueOptionsInterfaceCaller-supplied queue options, including clock and abort signal.@studnicky/resilience/interfaces
DeadLetterQueueRetryGenerator<T>Re-yields queue entries after a configurable pause.@studnicky/resilience/node
DeadLetterQueueRetryGeneratorOptionsInterface<T>Caller-supplied retry-generator options with a live queue.@studnicky/resilience/interfaces
ResilienceConfigErrorSignals invalid resilience configuration.@studnicky/resilience/node
RetryExecutes an operation under caller-supplied retry and backoff policy.@studnicky/resilience/retry/node
RetryConfigInterfaceCaller-supplied retry configuration and collaborators.@studnicky/resilience/retry/interfaces
ResilienceErrorBase error for the package.@studnicky/resilience/node
TokenBucketToken-bucket rate limiter.@studnicky/resilience/node
TokenBucketExhaustedErrorSignals insufficient available tokens.@studnicky/resilience/node
TokenBucketOptionsInterfaceCaller-supplied token-bucket options, including clock.@studnicky/resilience/interfaces
SlidingWindowLimiterEnforces an exact or approximate sliding-window limit.@studnicky/resilience/node
SlidingWindowExhaustedErrorSignals that a request exceeds available window capacity.@studnicky/resilience/node
SlidingWindowLimiterConfigErrorSignals invalid sliding-window limiter configuration.@studnicky/resilience/node
SlidingWindowLimiterErrorBase error for sliding-window limiter failures.@studnicky/resilience/node
SlidingWindowLimiterOptionsInterfaceCaller-supplied sliding-window limiter options, including algorithm and clock.@studnicky/resilience/interfaces
KeyedRateLimiterPer-key rate limiter with an injectable strategy.@studnicky/resilience/keyed
KeyedRateLimiterBoundaryErrorSignals invalid request, strategy, or strategy result boundaries.@studnicky/resilience/keyed
KeyedRateLimiterConfigErrorSignals invalid keyed limiter configuration.@studnicky/resilience/keyed
KeyedRateLimiterErrorBase error for keyed limiter failures.@studnicky/resilience/keyed
RateLimiterStrategyInterfaceStructural strategy contract.@studnicky/resilience/keyed/interfaces
RateLimitConsumptionEntitySchema-derived admission result with consumedTokens and remainingTokens.@studnicky/resilience/entities
SlidingWindowLimiterOptionsEntitySchema-derived sliding-window limiter options.@studnicky/resilience/entities

Entities ​

@studnicky/resilience/entities exports all schema-backed configuration, state, event, and effect declarations. Each entity namespace provides Schema, Type, and validate.

typescript
import { CircuitBreakerOptionsEntity } from "@studnicky/resilience/entities";

Interfaces ​

@studnicky/resilience/interfaces exports type-only event, effect, queue-entry, and option contracts.

typescript
import type { DeadLetterQueueEntryInterface } from "@studnicky/resilience/interfaces";

CircuitBreaker ​

MemberSignatureDescription
execute<T>(fn: () => Promise<T>) => Promise<T>Runs fn; throws CircuitBreakerOpenError when open
stateget state(): CircuitStateEntity.TypeCurrent circuit state
reset() => voidRestores the closed state and clears failure counters
forceOpen() => voidForces circuit open

TokenBucket ​

MemberSignatureDescription
consume(tokens?: unknown) => RateLimitConsumptionInterfaceConsumes a positive finite token count and returns the admission; throws TokenBucketExhaustedError if insufficient
waitForToken(options?: { tokens?: unknown; signal?: AbortSignal }) => Promise<RateLimitConsumptionInterface>Waits until a positive finite token count is available, then returns the admission
availablenumberCurrent token count (triggers a refill calculation)

SlidingWindowLimiter ​

MemberSignatureDescription
consume(tokens?: number) => RateLimitConsumptionInterfaceConsumes a positive count when it fits within the rolling window.
waitForToken(options?: { tokens?: number; signal?: AbortSignal }) => Promise<RateLimitConsumptionInterface>Waits until the requested count fits within the rolling window.

DeadLetterQueue<T> ​

MemberSignatureDescription
enqueue(item, reason, error?) => voidAdds item; throws on full, closed, or aborted
drain() => AsyncGenerator<DeadLetterQueueEntryInterface<T>>Yields all entries; suspends when queue is empty
close() => voidSignals drain to stop after the current entries
abort() => voidImmediately stops drain
sizeget size(): numberCurrent entry count

Source on GitHub