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 { 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( promise: Promise, ms: number, message = "Operation timed out" ): Promise { // `setTimeout` runs synchronously inside the Promise constructor, so // `timeoutId` is always assigned before `Promise.race` can observe it. let timeoutId: ReturnType = 0 as unknown as ReturnType< typeof setTimeout >; const timeoutPromise = new Promise((_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); }