alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
165 lines (164 loc) • 5.43 kB
JavaScript
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