Circuit Breaker With Half Open State

A typed circuit breaker that trips after consecutive failures, probes recovery in half open state, and resets after a success streak.

TypescriptData Fetching

Code

export type CircuitState = "closed" | "open" | "half-open";

export interface CircuitBreakerOptions {
  failureThreshold: number;
  resetTimeoutMs: number;
  successThreshold: number;
  now?: () => number;
}

export class CircuitBreaker {
  private state: CircuitState = "closed";
  private failures = 0;
  private successes = 0;
  private openedAt = 0;
  private readonly now: () => number;

  private readonly options: CircuitBreakerOptions;

  constructor(options: CircuitBreakerOptions) {
    this.options = options;
    this.now = options.now ?? (() => Date.now());
  }

  getState(): CircuitState {
    if (this.state === "open" && this.now() - this.openedAt >= this.options.resetTimeoutMs) {
      this.state = "half-open";
      this.successes = 0;
    }
    return this.state;
  }

  async call<T>(fn: () => Promise<T>): Promise<T> {
    const state = this.getState();
    if (state === "open") {
      throw new Error("circuit breaker is open");
    }
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  private onSuccess(): void {
    this.failures = 0;
    if (this.getState() === "half-open") {
      this.successes += 1;
      if (this.successes >= this.options.successThreshold) {
        this.state = "closed";
        this.successes = 0;
      }
    }
  }

  private onFailure(): void {
    this.failures += 1;
    if (this.getState() === "half-open") {
      this.state = "open";
      this.openedAt = this.now();
      this.successes = 0;
      return;
    }
    if (this.failures >= this.options.failureThreshold) {
      this.state = "open";
      this.openedAt = this.now();
    }
  }
}