UNPKG

@zerothrow/resilience

Version:

Production-grade resilience patterns for ZeroThrow

336 lines (327 loc) 8.45 kB
// src/types.ts var RetryExhaustedError = class extends Error { constructor(policyName, attempts, lastError, context) { super(`Retry exhausted after ${attempts} attempts`); this.policyName = policyName; this.attempts = attempts; this.lastError = lastError; this.context = context; this.name = "RetryExhaustedError"; } type = "retry-exhausted"; }; var CircuitOpenError = class extends Error { constructor(policyName, openedAt, failureCount, context) { super(`Circuit breaker is open`); this.policyName = policyName; this.openedAt = openedAt; this.failureCount = failureCount; this.context = context; this.name = "CircuitOpenError"; } type = "circuit-open"; }; var TimeoutError = class extends Error { constructor(policyName, timeout, elapsed, context) { super(`Operation timed out after ${elapsed}ms (limit: ${timeout}ms)`); this.policyName = policyName; this.timeout = timeout; this.elapsed = elapsed; this.context = context; this.name = "TimeoutError"; } type = "timeout"; }; // src/policies/retry.ts import { ZT as ZT2 } from "@zerothrow/core"; // src/policy.ts import { ZT } from "@zerothrow/core"; // src/clock.ts var SystemClock = class { now() { return /* @__PURE__ */ new Date(); } async sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } }; var TestClock = class { currentTime = Date.now(); sleepers = []; now() { return new Date(this.currentTime); } async sleep(ms) { const wakeTime = this.currentTime + ms; return new Promise((resolve) => { this.sleepers.push({ wakeTime, resolve }); this.sleepers.sort((a, b) => a.wakeTime - b.wakeTime); }); } advance(ms) { this.currentTime += ms; while (this.sleepers.length > 0) { const nextSleeper = this.sleepers[0]; if (nextSleeper && nextSleeper.wakeTime <= this.currentTime) { this.sleepers.shift(); nextSleeper.resolve(); } else { break; } } } setTime(time) { this.currentTime = typeof time === "number" ? time : time.getTime(); } }; // src/policy.ts var BasePolicy = class { constructor(name, clock = new SystemClock()) { this.name = name; this.clock = clock; } async runOperation(operation) { return ZT.tryAsync(operation); } }; // src/policies/retry.ts var RetryPolicy = class extends BasePolicy { constructor(count, options = {}, clock) { super("retry", clock); this.count = count; this.options = options; } retryCallback; async execute(operation) { let lastError; for (let attempt = 0; attempt <= this.count; attempt++) { const result = await this.runOperation(operation); if (result.ok) { return result; } lastError = result.error; if (this.options.handle && !this.options.handle(result.error)) { return result; } if (attempt < this.count) { const delayTime = this.calculateDelay(attempt + 1); if (this.retryCallback) { this.retryCallback(attempt + 1, lastError, delayTime); } await this.clock.sleep(delayTime); } } return ZT2.err(new RetryExhaustedError( this.name, this.count + 1, lastError )); } onRetry(callback) { this.retryCallback = callback; return this; } calculateDelay(attempt) { const { backoff = "constant", delay = 1e3, maxDelay = 3e4 } = this.options; let calculatedDelay; switch (backoff) { case "constant": calculatedDelay = delay; break; case "linear": calculatedDelay = delay * attempt; break; case "exponential": calculatedDelay = delay * Math.pow(2, attempt - 1); break; default: calculatedDelay = delay; } return Math.min(calculatedDelay, maxDelay); } }; // src/policies/circuit.ts import { ZT as ZT3 } from "@zerothrow/core"; var CircuitBreakerPolicy = class extends BasePolicy { constructor(options, clock) { super("circuit-breaker", clock); this.options = options; } state = "closed"; failures = 0; lastFailureTime; nextAllowedTime; stateChangeCallback; async execute(operation) { if (this.state === "open") { const now = this.clock.now().getTime(); if (this.nextAllowedTime && now >= this.nextAllowedTime) { this.setState("half-open"); } else { return ZT3.err(new CircuitOpenError( this.name, new Date(this.lastFailureTime || now), this.failures )); } } const result = await this.runOperation(operation); if (result.ok) { this.onSuccess(); return result; } else { this.onFailure(); const currentState = this.state; if (currentState === "open") { return ZT3.err(new CircuitOpenError( this.name, new Date(this.lastFailureTime || Date.now()), this.failures )); } return result; } } onSuccess() { if (this.state === "half-open") { this.reset(); this.options.onClose?.(); } } onFailure() { this.failures++; this.lastFailureTime = this.clock.now().getTime(); if (this.state === "half-open") { this.open(); } else if (this.failures >= this.options.threshold) { this.open(); } } open() { this.setState("open"); this.nextAllowedTime = this.clock.now().getTime() + this.options.duration; this.options.onOpen?.(); } reset() { this.setState("closed"); this.failures = 0; delete this.lastFailureTime; delete this.nextAllowedTime; } setState(newState) { if (this.state !== newState) { this.state = newState; this.stateChangeCallback?.(newState); } } onCircuitStateChange(callback) { this.stateChangeCallback = callback; return this; } }; // src/policies/timeout.ts import { ZT as ZT4 } from "@zerothrow/core"; var TimeoutPolicy = class extends BasePolicy { constructor(options, clock) { super("timeout", clock); this.options = options; } async execute(operation) { const startTime = this.clock.now().getTime(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { const elapsed = this.clock.now().getTime() - startTime; reject(new TimeoutError( this.name, this.options.timeout, elapsed )); }, this.options.timeout); }); try { const result = await Promise.race([ operation(), timeoutPromise ]); return ZT4.ok(result); } catch (error) { if (error instanceof TimeoutError) { return ZT4.err(error); } return ZT4.err(error instanceof Error ? error : new Error(String(error))); } } }; // src/compose.ts function wrap(outer, inner) { return { async execute(operation) { return outer.execute( () => ( // The inner policy executes the actual operation inner.execute(operation).then((result) => { if (!result.ok) { throw result.error; } return result.value; }) ) ); } }; } function compose(...policies) { if (policies.length === 0) { throw new Error("compose requires at least one policy"); } if (policies.length === 1) { return policies[0]; } return policies.reduce((acc, policy) => wrap(policy, acc)); } // src/policy-factory.ts var Policy = { /** * Creates a retry policy */ retry(count, options, clock) { return new RetryPolicy(count, options, clock); }, /** * Creates a circuit breaker policy */ circuitBreaker(options, clock) { return new CircuitBreakerPolicy(options, clock); }, /** * Creates a timeout policy */ timeout(options, clock) { const opts = typeof options === "number" ? { timeout: options } : options; return new TimeoutPolicy(opts, clock); }, /** * Wraps one policy with another * The outer policy executes first */ wrap, /** * Composes multiple policies from left to right * The leftmost policy is the outermost wrapper */ compose }; export { CircuitBreakerPolicy, CircuitOpenError, Policy as PolicyFactory, RetryExhaustedError, RetryPolicy, SystemClock, TestClock, TimeoutError, TimeoutPolicy, compose, wrap }; //# sourceMappingURL=index.js.map