UNPKG

alepha

Version:

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

178 lines (153 loc) 4.64 kB
import { $context, $inject, createMiddleware, type Middleware, Primitive, type PrimitiveArgs, } from "alepha"; import type { DurationLike } from "alepha/datetime"; import type { RetryBackoffOptions } from "../providers/RetryProvider.ts"; import { RetryProvider } from "../providers/RetryProvider.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 }) => { ... }, * }); * ``` */ export const $retry = (options?: RetryMiddlewareOptions): Middleware => { const { alepha } = $context(); const retryProvider = alepha.inject(RetryProvider); let appAbortController: AbortController | undefined; alepha.events.on("stop", () => { appAbortController?.abort(); }); return createMiddleware({ name: "$retry", options: options as unknown as Record<string, unknown>, handler: ({ next }) => { return async (...args) => { appAbortController ??= new AbortController(); return retryProvider.retry( { ...options, handler: next, additionalSignal: appAbortController.signal, }, ...args, ); }; }, }); }; // --------------------------------------------------------------------------------------------------------------------- export interface RetryPrimitiveOptions<T extends (...args: any[]) => any> { /** * The function to retry. */ handler: T; /** * The maximum number of attempts. * * @default 3 */ max?: number; /** * The backoff strategy for delays between retries. * Can be a fixed number (in ms) or a configuration object for exponential backoff. * * @default { initial: 200, factor: 2, jitter: true } */ backoff?: number | RetryBackoffOptions; /** * An overall time limit for all retry attempts combined. * * e.g., `[5, 'seconds']` */ maxDuration?: DurationLike; /** * A function that determines if a retry should be attempted based on the error. * * @default (error) => true (retries on any error) */ when?: (error: Error) => boolean; /** * A custom callback for when a retry attempt fails. * This is called before the delay. */ onError?: (error: Error, attempt: number, ...args: Parameters<T>) => void; /** * An AbortSignal to allow for external cancellation of the retry loop. */ signal?: AbortSignal; } /** * Options for $retry in middleware mode (no handler). */ export interface RetryMiddlewareOptions { /** * The maximum number of attempts. * * @default 3 */ max?: number; /** * The backoff strategy for delays between retries. * * @default { initial: 200, factor: 2, jitter: true } */ backoff?: number | RetryBackoffOptions; /** * An overall time limit for all retry attempts combined. */ maxDuration?: DurationLike; /** * A function that determines if a retry should be attempted based on the error. * * @default (error) => true (retries on any error) */ when?: (error: Error) => boolean; /** * A custom callback for when a retry attempt fails. */ onError?: (error: Error, attempt: number) => void; /** * An AbortSignal to allow for external cancellation of the retry loop. */ signal?: AbortSignal; } // --------------------------------------------------------------------------------------------------------------------- export class RetryPrimitive< T extends (...args: any[]) => any, > extends Primitive<RetryPrimitiveOptions<T>> { protected readonly retryProvider = $inject(RetryProvider); protected appAbortController?: AbortController; constructor(args: PrimitiveArgs<RetryPrimitiveOptions<T>>) { super(args); this.alepha.events.on("stop", () => { this.appAbortController?.abort(); }); } async run(...args: Parameters<T>): Promise<ReturnType<T>> { // Nov 25: Cloudflare does not like 'new AbortController' outside main handler, we can't pre-create it in the constructor. this.appAbortController ??= new AbortController(); return this.retryProvider.retry( { ...this.options, additionalSignal: this.appAbortController.signal, }, ...args, ); } } export interface RetryPrimitiveFn<T extends (...args: any[]) => any> extends RetryPrimitive<T> { (...args: Parameters<T>): Promise<ReturnType<T>>; }