{"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  IBackoffFactory,\n  IPolicy,\n  Policy,\n  RetryPolicy,\n} from 'cockatiel';\n\nexport {\n  BrokenCircuitError,\n  CircuitState,\n  ConstantBackoff,\n  ExponentialBackoff,\n  handleAll,\n  handleWhen,\n};\n\nexport type { CockatielEvent };\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 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<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 * 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) => {\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 * 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  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 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: new ConsecutiveBreaker(maxConsecutiveFailures),\n  });\n  const onBreak = circuitBreakerPolicy.onBreak.bind(circuitBreakerPolicy);\n\n  const onDegradedEventEmitter = new CockatielEventEmitter<void>();\n  retryPolicy.onGiveUp(() => {\n    if (circuitBreakerPolicy.state === CircuitState.Closed) {\n      onDegradedEventEmitter.emit();\n    }\n  });\n  retryPolicy.onSuccess(({ duration }) => {\n    if (\n      circuitBreakerPolicy.state === CircuitState.Closed &&\n      duration > degradedThreshold\n    ) {\n      onDegradedEventEmitter.emit();\n    }\n  });\n  const onDegraded = onDegradedEventEmitter.addListener;\n\n  // Every time the retry policy makes an attempt, it executes the circuit\n  // breaker policy, which executes the service.\n  const policy = wrap(retryPolicy, circuitBreakerPolicy);\n\n  return {\n    ...policy,\n    circuitBreakerPolicy,\n    retryPolicy,\n    onBreak,\n    onDegraded,\n    onRetry,\n  };\n}\n"]}