/** * kosha-discovery — Resilience primitives. * * Provides a {@link CircuitBreaker} for per-provider fault isolation, * a {@link HealthTracker} that manages breakers for all known providers, * and a {@link StaleCachePolicy} that implements stale-while-revalidate * semantics on top of {@link KoshaCache}. * * The module is self-contained with no side-effects on import. * @module */ import type { KoshaCache } from "./cache.js"; /** Possible states of a {@link CircuitBreaker}. */ export type CircuitState = "closed" | "open" | "half-open"; /** Configuration options for a {@link CircuitBreaker} instance. */ export interface CircuitBreakerOptions { /** * Number of consecutive failures before the circuit opens. * @default 3 */ failureThreshold?: number; /** * Time (in milliseconds) the circuit stays open before transitioning * to `half-open` to allow a probe request. * @default 60_000 */ resetTimeoutMs?: number; /** * Number of consecutive successes in `half-open` state required to * close the circuit again. * @default 1 */ halfOpenSuccessThreshold?: number; /** * Upper bound on the adaptive `resetTimeoutMs`. Each time the circuit * re-opens without successfully closing, the cooldown doubles up to this * cap, so a chronically broken provider is probed less often. * @default 3_600_000 (1 hour) */ maxResetTimeoutMs?: number; } /** * A point-in-time health snapshot for a single provider circuit. */ export interface ProviderHealth { /** Provider slug this health record belongs to. */ providerId: string; /** Current circuit state. */ state: CircuitState; /** Number of consecutive failures recorded since last reset. */ failureCount: number; /** Unix timestamp (ms) of the most recent recorded failure, or 0. */ lastFailureTime: number; /** Error message from the most recent failure, if available. */ lastError?: string; /** Unix timestamp (ms) of the most recent recorded success, or 0. */ lastSuccessTime: number; } /** * Wrapper returned by {@link StaleCachePolicy.getWithStale}. */ export interface StaleResult { /** The cached payload. */ data: T; /** True when the cache entry has exceeded its TTL. */ stale: boolean; /** Milliseconds elapsed since the entry was written. */ age: number; /** Unix timestamp (ms) when the entry was originally cached. */ cachedAt: number; } /** * Per-provider circuit breaker with three states. * * - **closed** — Normal operation. All requests are allowed. Consecutive * failures are counted; once the {@link CircuitBreakerOptions.failureThreshold} * is reached the circuit transitions to `open`. * - **open** — Requests are rejected immediately (canExecute returns false). * After {@link CircuitBreakerOptions.resetTimeoutMs} has elapsed the circuit * transitions to `half-open` so a single probe request can be attempted. * - **half-open** — Exactly one request is let through. A success closes the * circuit; a failure re-opens it and resets the timeout. * * @example * const cb = new CircuitBreaker("anthropic", { failureThreshold: 5 }); * if (cb.canExecute()) { * try { * const result = await callApi(); * cb.onSuccess(); * } catch (err) { * cb.onFailure(err.message); * } * } */ export declare class CircuitBreaker { readonly providerId: string; private options; private state; private failureCount; private successCount; private lastFailureTime; private lastSuccessTime; private lastError?; /** Consecutive open cycles since the last successful close. Used to * exponentially extend the open-state cooldown so a chronically broken * provider gets probed less often. Resets to 0 on a closed transition. */ private openCycles; /** Resolved threshold: consecutive failures before opening. */ private readonly failureThreshold; /** Resolved open-state duration before allowing a probe. */ private readonly resetTimeoutMs; /** Resolved success count in half-open needed to close. */ private readonly halfOpenSuccessThreshold; /** Resolved upper bound on the adaptive open-state cooldown. */ private readonly maxResetTimeoutMs; constructor(providerId: string, options?: CircuitBreakerOptions); /** Current adaptive open-state cooldown, in ms. Public for diagnostics. */ currentResetTimeoutMs(): number; /** * Check whether a request should be allowed through. * * - `closed` → always true. * - `open` → false, unless the reset timeout has elapsed, in which case * the circuit transitions to `half-open` and returns true for the probe. * - `half-open` → true (the probe request is already in flight). */ canExecute(): boolean; /** * Record a successful API call. * * In `half-open` state, once enough successes accumulate (per * {@link CircuitBreakerOptions.halfOpenSuccessThreshold}) the circuit closes. * In `closed` state the failure counter is reset. */ onSuccess(): void; /** * Record a failed API call. * * In `closed` state, increments the failure counter and opens the circuit * when the threshold is reached. In `half-open` state, immediately * re-opens the circuit. * * @param error - Optional error message to store for diagnostics. */ onFailure(error?: string): void; /** * Return a point-in-time health snapshot for this provider's circuit. */ health(): ProviderHealth; /** * Force the circuit back to `closed` state, resetting all counters. * Useful for manual recovery or test teardown. */ reset(): void; private transitionToOpen; private transitionToClosed; } /** * Manages {@link CircuitBreaker} instances for all tracked providers. * * Breakers are created lazily on first access via {@link breaker}. * * @example * const tracker = new HealthTracker(); * const cb = tracker.breaker("anthropic"); * if (cb.canExecute()) { ... } */ export declare class HealthTracker { private breakers; /** * Retrieve the {@link CircuitBreaker} for the given provider, creating * one with default options if it does not yet exist. * * @param providerId - Provider slug (e.g. `"anthropic"`). * @param options - Options forwarded to a newly created breaker only. */ breaker(providerId: string, options?: CircuitBreakerOptions): CircuitBreaker; /** * Return health snapshots for every tracked provider, sorted by provider ID. */ healthReport(): ProviderHealth[]; /** * Return provider IDs whose circuit is `closed` or `half-open` * (i.e. requests are currently being allowed through). */ availableProviders(): string[]; /** * Return provider IDs whose circuit is `open` * (i.e. requests are currently being rejected). */ downProviders(): string[]; /** * Reset all tracked circuit breakers to `closed` state. */ resetAll(): void; } /** * Utility that wraps {@link KoshaCache} reads to implement * stale-while-revalidate semantics. * * Unlike the registry's normal cache path (which returns `null` for expired * entries), this policy always returns whatever was cached along with a * `stale` flag. This lets callers serve the old data immediately while * triggering a background refresh. * * Returns `null` only when the cache has never held a value for the key. * * @example * const result = await StaleCachePolicy.getWithStale(cache, "provider_anthropic"); * if (result) { * serveToClient(result.data); // always fast * if (result.stale) triggerBackgroundRefresh(); * } */ export declare class StaleCachePolicy { /** * Fetch a cached value regardless of its expiry, annotating the result * with staleness metadata. * * @param cache - The {@link KoshaCache} instance to read from. * @param key - Cache key to look up. * @returns A {@link StaleResult} when any cached value exists, or `null` * when the key has never been written. */ static getWithStale(cache: KoshaCache, key: string): Promise | null>; } //# sourceMappingURL=resilience.d.ts.map