{"version":3,"file":"create-service-policy.mjs","sourceRoot":"","sources":["../src/create-service-policy.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n  BrokenCircuitError,\n  CircuitState,\n  EventEmitter as CockatielEventEmitter,\n  ConsecutiveBreaker,\n  ExponentialBackoff,\n  ConstantBackoff,\n  circuitBreaker,\n  handleAll,\n  handleWhen,\n  retry,\n  wrap,\n} from 'cockatiel';\nimport type {\n  CircuitBreakerPolicy,\n  Event as CockatielEvent,\n  FailureReason,\n  IBackoffFactory,\n  IPolicy,\n  Policy,\n  RetryPolicy,\n} from 'cockatiel';\n\nexport {\n  BrokenCircuitError,\n  CockatielEventEmitter,\n  CircuitState,\n  ConstantBackoff,\n  ExponentialBackoff,\n  handleAll,\n  handleWhen,\n};\n\nexport type { CockatielEvent, FailureReason as CockatielFailureReason };\n\n/**\n * The options for `createServicePolicy`.\n */\nexport type CreateServicePolicyOptions = {\n  /**\n   * The backoff strategy to use. Mainly useful for testing so that a constant\n   * backoff can be used when mocking timers. Defaults to an instance of\n   * ExponentialBackoff.\n   */\n  backoff?: IBackoffFactory<unknown>;\n  /**\n   * The length of time (in milliseconds) to pause retries of the action after\n   * the number of failures reaches `maxConsecutiveFailures`.\n   */\n  circuitBreakDuration?: number;\n  /**\n   * The length of time (in milliseconds) that governs when the service is\n   * regarded as degraded (affecting when `onDegraded` is called).\n   */\n  degradedThreshold?: number;\n  /**\n   * The maximum number of times that the service is allowed to fail before\n   * pausing further retries.\n   */\n  maxConsecutiveFailures?: number;\n  /**\n   * The maximum number of times that a failing service should be re-invoked\n   * before giving up.\n   */\n  maxRetries?: number;\n  /**\n   * The policy used to control when the service should be retried based on\n   * either the result of the service or an error that it throws. For instance,\n   * you could use this to retry only certain errors. See `handleWhen` and\n   * friends from Cockatiel for more.\n   */\n  retryFilterPolicy?: Policy;\n};\n\n/**\n * The service policy object.\n */\nexport type ServicePolicy = IPolicy & {\n  /**\n   * The Cockatiel circuit breaker policy that the service policy uses\n   * internally.\n   */\n  circuitBreakerPolicy: CircuitBreakerPolicy;\n  /**\n   * The amount of time to pause requests to the service if the number of\n   * maximum consecutive failures is reached.\n   */\n  circuitBreakDuration: number;\n  /**\n   * @returns The state of the underlying circuit.\n   */\n  getCircuitState: () => CircuitState;\n  /**\n   * If the circuit is open and ongoing requests are paused, returns the number\n   * of milliseconds before the requests will be attempted again. If the circuit\n   * is not open, returns null.\n   */\n  getRemainingCircuitOpenDuration: () => number | null;\n  /**\n   * Resets the internal circuit breaker policy (if it is open, it will now be\n   * closed).\n   */\n  reset: () => void;\n  /**\n   * The Cockatiel retry policy that the service policy uses internally.\n   */\n  retryPolicy: RetryPolicy;\n  /**\n   * A function which is called when the number of times that the service fails\n   * in a row meets the set maximum number of consecutive failures.\n   */\n  onBreak: CircuitBreakerPolicy['onBreak'];\n  /**\n   * A function which is called in two circumstances: 1) when the service\n   * succeeds before the maximum number of consecutive failures is reached, but\n   * takes more time than the `degradedThreshold` to run, or 2) if the service\n   * never succeeds before the retry policy gives up and before the maximum\n   * number of consecutive failures has been reached.\n   */\n  onDegraded: CockatielEvent<FailureReason<unknown> | void>;\n  /**\n   * A function which is called when the service succeeds for the first time,\n   * or when the service fails enough times to cause the circuit to break and\n   * then recovers.\n   */\n  onAvailable: CockatielEvent<void>;\n  /**\n   * A function which will be called by the retry policy each time the service\n   * fails and the policy kicks off a timer to re-run the service. This is\n   * primarily useful in tests where we are mocking timers.\n   */\n  onRetry: RetryPolicy['onRetry'];\n};\n\n/**\n * Parts of the circuit breaker's internal and external state as necessary in\n * order to compute the time remaining before the circuit will reopen.\n */\ntype InternalCircuitState =\n  | {\n      state: CircuitState.Open;\n      openedAt: number;\n    }\n  | { state: Exclude<CircuitState, CircuitState.Open> };\n\n/**\n * Availability statuses that the service can be in.\n *\n * Used to keep track of whether the `onAvailable` event should be fired.\n */\nconst AVAILABILITY_STATUSES = {\n  Available: 'available',\n  Degraded: 'degraded',\n  Unavailable: 'unavailable',\n  Unknown: 'unknown',\n} as const;\n\n/**\n * Availability statuses that the service can be in.\n *\n * Used to keep track of whether the `onAvailable` event should be fired.\n */\ntype AvailabilityStatus =\n  (typeof AVAILABILITY_STATUSES)[keyof typeof AVAILABILITY_STATUSES];\n\n/**\n * The maximum number of times that a failing service should be re-run before\n * giving up.\n */\nexport const DEFAULT_MAX_RETRIES = 3;\n\n/**\n * The maximum number of times that the service is allowed to fail before\n * pausing further retries. This is set to a value such that if given a\n * service that continually fails, the policy needs to be executed 3 times\n * before further retries are paused.\n */\nexport const DEFAULT_MAX_CONSECUTIVE_FAILURES = (1 + DEFAULT_MAX_RETRIES) * 3;\n\n/**\n * The default length of time (in milliseconds) to temporarily pause retries of\n * the service after enough consecutive failures.\n */\nexport const DEFAULT_CIRCUIT_BREAK_DURATION = 30 * 60 * 1000;\n\n/**\n * The default length of time (in milliseconds) that governs when the service is\n * regarded as degraded (affecting when `onDegraded` is called).\n */\nexport const DEFAULT_DEGRADED_THRESHOLD = 5_000;\n\nconst isServiceFailure = (error: unknown): boolean => {\n  if (\n    typeof error === 'object' &&\n    error !== null &&\n    'httpStatus' in error &&\n    typeof error.httpStatus === 'number'\n  ) {\n    return error.httpStatus >= 500;\n  }\n\n  // If the error is not an object, or doesn't have a numeric code property,\n  // consider it a service failure (e.g., network errors, timeouts, etc.)\n  return true;\n};\n\n/**\n * The circuit breaker policy inside of the Cockatiel library exposes some of\n * its state, but not all of it. Notably, the time that the circuit opened is\n * not publicly accessible. So we have to record this ourselves.\n *\n * This function therefore allows us to obtain the circuit breaker state that we\n * wish we could access.\n *\n * @param state - The public state of a circuit breaker policy.\n * @returns if the circuit is open, the state of the circuit breaker policy plus\n * the time that it opened, otherwise just the circuit state.\n */\nfunction getInternalCircuitState(state: CircuitState): InternalCircuitState {\n  if (state === CircuitState.Open) {\n    return { state, openedAt: Date.now() };\n  }\n  return { state };\n}\n\n/**\n * Constructs an object exposing an `execute` method which, given a function —\n * hereafter called the \"service\" — will retry that service with ever increasing\n * delays until it succeeds. If the policy detects too many consecutive\n * failures, it will block further retries until a designated time period has\n * passed; this particular behavior is primarily designed for services that wrap\n * API calls so as not to make needless HTTP requests when the API is down and\n * to be able to recover when the API comes back up. In addition, hooks allow\n * for responding to certain events, one of which can be used to detect when an\n * HTTP request is performing slowly.\n *\n * Internally, this function makes use of the retry and circuit breaker policies\n * from the [Cockatiel](https://www.npmjs.com/package/cockatiel) library; see\n * there for more.\n *\n * @param options - The options to this function. See\n * {@link CreateServicePolicyOptions}.\n * @returns The service policy.\n * @example\n * This function is designed to be used in the context of a service class like\n * this:\n * ``` ts\n * class Service {\n *   constructor() {\n *     this.#policy = createServicePolicy({\n *       maxRetries: 3,\n *       retryFilterPolicy: handleWhen((error) => {\n *         return error.message.includes('oops');\n *       }),\n *       maxConsecutiveFailures: 3,\n *       circuitBreakDuration: 5000,\n *       degradedThreshold: 2000,\n *       onBreak: () => {\n *         console.log('Circuit broke');\n *       },\n *       onDegraded: () => {\n *         console.log('Service is degraded');\n *       },\n *     });\n *   }\n *\n *   async fetch() {\n *     return await this.#policy.execute(async () => {\n *       const response = await fetch('https://some/url');\n *       return await response.json();\n *     });\n *   }\n * }\n * ```\n */\nexport function createServicePolicy(\n  options: CreateServicePolicyOptions = {},\n): ServicePolicy {\n  const {\n    maxRetries = DEFAULT_MAX_RETRIES,\n    retryFilterPolicy = handleAll,\n    maxConsecutiveFailures = DEFAULT_MAX_CONSECUTIVE_FAILURES,\n    circuitBreakDuration = DEFAULT_CIRCUIT_BREAK_DURATION,\n    degradedThreshold = DEFAULT_DEGRADED_THRESHOLD,\n    backoff = new ExponentialBackoff(),\n  } = options;\n\n  let availabilityStatus: AvailabilityStatus = AVAILABILITY_STATUSES.Unknown;\n\n  const retryPolicy = retry(retryFilterPolicy, {\n    // Note that although the option here is called \"max attempts\", it's really\n    // maximum number of *retries* (attempts past the initial attempt).\n    maxAttempts: maxRetries,\n    // Retries of the service will be executed following ever increasing delays,\n    // determined by a backoff formula.\n    backoff,\n  });\n  const onRetry = retryPolicy.onRetry.bind(retryPolicy);\n\n  const consecutiveBreaker = new ConsecutiveBreaker(maxConsecutiveFailures);\n  const circuitBreakerPolicy = circuitBreaker(handleWhen(isServiceFailure), {\n    // While the circuit is open, any additional invocations of the service\n    // passed to the policy (either via automatic retries or by manually\n    // executing the policy again) will result in a BrokenCircuitError. This\n    // will remain the case until `circuitBreakDuration` passes, after which the\n    // service will be allowed to run again. If the service succeeds, the\n    // circuit will close, otherwise it will remain open.\n    halfOpenAfter: circuitBreakDuration,\n    breaker: consecutiveBreaker,\n  });\n\n  let internalCircuitState: InternalCircuitState = getInternalCircuitState(\n    circuitBreakerPolicy.state,\n  );\n  circuitBreakerPolicy.onStateChange((state) => {\n    internalCircuitState = getInternalCircuitState(state);\n  });\n\n  circuitBreakerPolicy.onBreak(() => {\n    availabilityStatus = AVAILABILITY_STATUSES.Unavailable;\n  });\n  const onBreak = circuitBreakerPolicy.onBreak.bind(circuitBreakerPolicy);\n\n  const onDegradedEventEmitter =\n    new CockatielEventEmitter<FailureReason<unknown> | void>();\n  const onDegraded = onDegradedEventEmitter.addListener;\n\n  const onAvailableEventEmitter = new CockatielEventEmitter<void>();\n  const onAvailable = onAvailableEventEmitter.addListener;\n\n  retryPolicy.onGiveUp((data) => {\n    if (circuitBreakerPolicy.state === CircuitState.Closed) {\n      availabilityStatus = AVAILABILITY_STATUSES.Degraded;\n      onDegradedEventEmitter.emit(data);\n    }\n  });\n  retryPolicy.onSuccess(({ duration }) => {\n    if (circuitBreakerPolicy.state === CircuitState.Closed) {\n      if (duration > degradedThreshold) {\n        availabilityStatus = AVAILABILITY_STATUSES.Degraded;\n        onDegradedEventEmitter.emit();\n      } else if (availabilityStatus !== AVAILABILITY_STATUSES.Available) {\n        availabilityStatus = AVAILABILITY_STATUSES.Available;\n        onAvailableEventEmitter.emit();\n      }\n    }\n  });\n\n  // Every time the retry policy makes an attempt, it executes the circuit\n  // breaker policy, which executes the service.\n  //\n  // Calling:\n  //\n  //   policy.execute(() => {\n  //     // do what the service does\n  //   })\n  //\n  // is equivalent to:\n  //\n  //   retryPolicy.execute(() => {\n  //     circuitBreakerPolicy.execute(() => {\n  //       // do what the service does\n  //     });\n  //   });\n  //\n  // So if the retry policy succeeds or fails, it is because the circuit breaker\n  // policy succeeded or failed. And if there are any event listeners registered\n  // on the retry policy, by the time they are called, the state of the circuit\n  // breaker will have already changed.\n  const policy = wrap(retryPolicy, circuitBreakerPolicy);\n\n  const getRemainingCircuitOpenDuration = (): number | null => {\n    if (internalCircuitState.state === CircuitState.Open) {\n      return internalCircuitState.openedAt + circuitBreakDuration - Date.now();\n    }\n    return null;\n  };\n\n  const getCircuitState = (): CircuitState => {\n    return circuitBreakerPolicy.state;\n  };\n\n  const reset = (): void => {\n    // Set the state of the policy to \"isolated\" regardless of its current state\n    const { dispose } = circuitBreakerPolicy.isolate();\n    // Reset the state to \"closed\"\n    dispose();\n\n    // Reset the counter on the breaker as well\n    consecutiveBreaker.success();\n\n    // Re-initialize the availability status so that if the service is executed\n    // successfully, onAvailable listeners will be called again\n    availabilityStatus = AVAILABILITY_STATUSES.Unknown;\n  };\n\n  return {\n    ...policy,\n    circuitBreakerPolicy,\n    circuitBreakDuration,\n    getCircuitState,\n    getRemainingCircuitOpenDuration,\n    reset,\n    retryPolicy,\n    onBreak,\n    onDegraded,\n    onAvailable,\n    onRetry,\n  };\n}\n"]}