86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import type { Clock } from "@suaveplan/types";
|
|
|
|
/**
|
|
* Default clock used by the package when callers do not inject one.
|
|
* Reading through this indirection (rather than calling `Date.now()` inline)
|
|
* keeps every timing path test-controllable via the `Clock` injection.
|
|
*/
|
|
export const defaultClock: Clock = () => Date.now();
|
|
|
|
/**
|
|
* Generates a unique workflow or step identifier.
|
|
* Uses a combination of timestamp and random characters.
|
|
*/
|
|
export function generateId(prefix = "wf", clock: Clock = defaultClock): string {
|
|
const timestamp = clock().toString(36);
|
|
const random = Math.random().toString(36).substring(2, 8);
|
|
return `${prefix}_${timestamp}_${random}`;
|
|
}
|
|
|
|
/** Returns current timestamp via the supplied clock. */
|
|
export function now(clock: Clock = defaultClock): number {
|
|
return clock();
|
|
}
|
|
|
|
/** Returns a promise that resolves after the specified delay. */
|
|
export function delay(ms: number): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wraps a promise with a timeout. Rejects if the promise
|
|
* does not resolve within the specified duration.
|
|
*/
|
|
export function withTimeout<T>(
|
|
promise: Promise<T>,
|
|
ms: number,
|
|
message = "Operation timed out"
|
|
): Promise<T> {
|
|
// `setTimeout` runs synchronously inside the Promise constructor, so
|
|
// `timeoutId` is always assigned before `Promise.race` can observe it.
|
|
let timeoutId: ReturnType<typeof setTimeout> = 0 as unknown as ReturnType<
|
|
typeof setTimeout
|
|
>;
|
|
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
timeoutId = setTimeout(() => {
|
|
reject(new Error(message));
|
|
}, ms);
|
|
});
|
|
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
clearTimeout(timeoutId);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculates the delay for a retry attempt using the specified strategy.
|
|
*
|
|
* @param attempt - Current attempt number (1-based).
|
|
* @param strategy - 'fixed', 'linear', or 'exponential'.
|
|
* @param baseDelay - Base delay in milliseconds.
|
|
* @param multiplier - Multiplier for exponential backoff.
|
|
* @param maxDelay - Maximum delay cap in milliseconds.
|
|
*/
|
|
export function calculateRetryDelay(
|
|
attempt: number,
|
|
strategy: "fixed" | "linear" | "exponential",
|
|
baseDelay: number,
|
|
multiplier = 2,
|
|
maxDelay = 30000
|
|
): number {
|
|
let computed: number;
|
|
switch (strategy) {
|
|
case "fixed":
|
|
computed = baseDelay;
|
|
break;
|
|
case "linear":
|
|
computed = baseDelay * attempt;
|
|
break;
|
|
case "exponential":
|
|
computed = baseDelay * multiplier ** (attempt - 1);
|
|
break;
|
|
}
|
|
return Math.min(computed, maxDelay);
|
|
}
|