UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

165 lines (164 loc) 5.43 kB
import { $context, $inject, $module, AlephaError, Primitive, createMiddleware } from "alepha"; import { DateTimeProvider } from "alepha/datetime"; import { $logger } from "alepha/logger"; //#region ../../src/retry/errors/RetryCancelError.ts var RetryCancelError = class extends AlephaError { constructor() { super("Retry operation was cancelled."); this.name = "RetryCancelError"; } }; //#endregion //#region ../../src/retry/errors/RetryTimeoutError.ts var RetryTimeoutError = class extends AlephaError { constructor(duration) { super(`Retry operation timed out after ${duration}ms.`); this.name = "RetryTimeoutError"; } }; //#endregion //#region ../../src/retry/providers/RetryProvider.ts /** * Service for executing functions with automatic retry logic. * Supports exponential backoff, max duration, conditional retries, and cancellation. */ var RetryProvider = class { log = $logger(); dateTime = $inject(DateTimeProvider); /** * Execute a function with automatic retry logic. */ async retry(options, ...args) { const maxAttempts = options.max ?? 3; const when = options.when ?? (() => true); const { handler, onError } = options; let lastError; const startTime = this.dateTime.nowMillis(); const maxDurationMs = options.maxDuration ? this.dateTime.duration(options.maxDuration).asMilliseconds() : Infinity; const signals = [options.signal, options.additionalSignal].filter(Boolean); const onAbort = () => { lastError = new RetryCancelError(); }; for (const signal of signals) signal?.addEventListener("abort", onAbort); const waitSignals = [options.signal, options.additionalSignal].filter(Boolean); const combinedSignal = waitSignals.length > 0 ? AbortSignal.any(waitSignals) : void 0; try { for (let attempt = 1; attempt <= maxAttempts; attempt++) { if (signals.some((signal) => signal?.aborted)) throw new RetryCancelError(); if (this.dateTime.nowMillis() - startTime >= maxDurationMs) throw new RetryTimeoutError(maxDurationMs); try { const result = await handler(...args); if (this.dateTime.nowMillis() - startTime >= maxDurationMs) throw new RetryTimeoutError(maxDurationMs); return result; } catch (err) { lastError = err; if (this.dateTime.nowMillis() - startTime >= maxDurationMs) throw new RetryTimeoutError(maxDurationMs); this.log.warn("Retry attempt failed", { attempt, maxAttempts, remainingAttempts: maxAttempts - attempt, error: lastError.message, errorName: lastError.name }); if (!(err instanceof Error) || !when(err)) throw err; if (onError) onError(err, attempt, ...args); if (attempt >= maxAttempts) break; const delay = this.calculateBackoff(attempt, options.backoff); if (delay > 0) await this.dateTime.wait(delay, { signal: combinedSignal }); if (this.dateTime.nowMillis() - startTime >= maxDurationMs) throw new RetryTimeoutError(maxDurationMs); } } } finally { for (const signal of signals) signal?.removeEventListener("abort", onAbort); } throw lastError; } /** * Calculate the backoff delay for a given attempt. */ calculateBackoff(attempt, options) { if (typeof options === "number") return options; const initial = options?.initial ?? 200; const factor = options?.factor ?? 2; const max = options?.max ?? 1e4; const useJitter = options?.jitter !== false; const exponential = initial * factor ** (attempt - 1); let delay = Math.min(exponential, max); if (useJitter) delay = delay * (1 + Math.random() * .5); return Math.floor(delay); } }; //#endregion //#region ../../src/retry/primitives/$retry.ts /** * Retry middleware for `use` arrays in `$action`, `$job`, `$page`, `$pipeline`. * * Retries the handler on failure with configurable backoff, max attempts, and * conditional retry via `when`. Aborts on application shutdown. * * ```ts * processOrder = $action({ * use: [$retry({ max: 3, backoff: { initial: 500 } })], * handler: async ({ body }) => { ... }, * }); * ``` */ const $retry = (options) => { const { alepha } = $context(); const retryProvider = alepha.inject(RetryProvider); let appAbortController; alepha.events.on("stop", () => { appAbortController?.abort(); }); return createMiddleware({ name: "$retry", options, handler: ({ next }) => { return async (...args) => { appAbortController ??= new AbortController(); return retryProvider.retry({ ...options, handler: next, additionalSignal: appAbortController.signal }, ...args); }; } }); }; var RetryPrimitive = class extends Primitive { retryProvider = $inject(RetryProvider); appAbortController; constructor(args) { super(args); this.alepha.events.on("stop", () => { this.appAbortController?.abort(); }); } async run(...args) { this.appAbortController ??= new AbortController(); return this.retryProvider.retry({ ...this.options, additionalSignal: this.appAbortController.signal }, ...args); } }; //#endregion //#region ../../src/retry/index.ts /** * Automatic retry with backoff. * * **Features:** * - Retry configuration * - Exponential backoff * - Max retry limits * - Custom retry predicates * * @module alepha.retry */ const AlephaRetry = $module({ name: "alepha.retry", services: [RetryProvider] }); //#endregion export { $retry, AlephaRetry, RetryCancelError, RetryPrimitive, RetryProvider, RetryTimeoutError }; //# sourceMappingURL=index.js.map