UNPKG

@socketsecurity/lib

Version:

Core utilities and infrastructure for Socket.dev security tools

458 lines (457 loc) 15.6 kB
/** * Configuration options for retry behavior with exponential backoff. * * Controls how failed operations are retried, including timing, backoff strategy, * and callback hooks for observing or modifying retry behavior. */ export interface RetryOptions { /** * Arguments to pass to the callback function on each attempt. * * @default [] */ args?: unknown[] | undefined; /** * Multiplier for exponential backoff (e.g., 2 doubles delay each retry). * Each retry waits `baseDelayMs * (backoffFactor ** attemptNumber)`. * * @default 2 * @example * // With backoffFactor: 2, baseDelayMs: 100 * // Retry 1: 100ms * // Retry 2: 200ms * // Retry 3: 400ms */ backoffFactor?: number | undefined; /** * Initial delay before the first retry (in milliseconds). * This is the base value for exponential backoff calculations. * * @default 200 */ baseDelayMs?: number | undefined; // REMOVED: Deprecated `factor` option // Migration: Use `backoffFactor` instead /** * Whether to apply randomness to spread out retries and avoid thundering herd. * When `true`, adds random delay between 0 and current delay value. * * @default true * @example * // With jitter: true, delay: 100ms * // Actual wait: 100ms + random(0-100ms) = 100-200ms */ jitter?: boolean | undefined; /** * Upper limit for any backoff delay (in milliseconds). * Prevents exponential backoff from growing unbounded. * * @default 10000 */ maxDelayMs?: number | undefined; // REMOVED: Deprecated `maxTimeout` option // Migration: Use `maxDelayMs` instead // REMOVED: Deprecated `minTimeout` option // Migration: Use `baseDelayMs` instead /** * Callback invoked on each retry attempt. * Can observe errors, customize delays, or cancel retries. * * @param attempt - The current attempt number (1-based: 1, 2, 3, ...) * @param error - The error that triggered this retry * @param delay - The calculated delay in milliseconds before next retry * @returns `false` to cancel retries (if `onRetryCancelOnFalse` is `true`), * a number to override the delay, or `undefined` to use calculated delay * * @example * // Log each retry * onRetry: (attempt, error, delay) => { * console.log(`Retry ${attempt} after ${delay}ms: ${error}`) * } * * @example * // Cancel retries for specific errors * onRetry: (attempt, error) => { * if (error instanceof ValidationError) return false * } * * @example * // Use custom delay * onRetry: (attempt) => attempt * 1000 // 1s, 2s, 3s, ... */ onRetry?: ((attempt: number, error: unknown, delay: number) => boolean | number | undefined) | undefined; /** * Whether `onRetry` can cancel retries by returning `false`. * When `true`, returning `false` from `onRetry` stops retry attempts. * * @default false */ onRetryCancelOnFalse?: boolean | undefined; /** * Whether errors thrown by `onRetry` should propagate. * When `true`, exceptions in `onRetry` terminate the retry loop. * When `false`, exceptions in `onRetry` are silently caught. * * @default false */ onRetryRethrow?: boolean | undefined; /** * Number of retry attempts (0 = no retries, only initial attempt). * The callback is executed `retries + 1` times total (initial + retries). * * @default 0 * @example * // retries: 0 -> 1 total attempt (no retries) * // retries: 3 -> 4 total attempts (1 initial + 3 retries) */ retries?: number | undefined; /** * AbortSignal to support cancellation of retry operations. * When aborted, immediately stops retrying and returns `undefined`. * * @default process abort signal * @example * const controller = new AbortController() * pRetry(fn, { signal: controller.signal }) * // Later: controller.abort() to cancel */ signal?: AbortSignal | undefined; } /** * Configuration options for iteration functions with concurrency control. * * Controls how array operations are parallelized and retried. */ export interface IterationOptions { /** * The number of concurrent executions performed at one time. * Higher values increase parallelism but may overwhelm resources. * * @default 1 * @example * // Process 5 items at a time * await pEach(items, processItem, { concurrency: 5 }) */ concurrency?: number | undefined; /** * Retry configuration as a number (retry count) or full options object. * Applied to each individual item's callback execution. * * @default 0 (no retries) * @example * // Simple: retry each item up to 3 times * await pEach(items, fetchItem, { retries: 3 }) * * @example * // Advanced: custom backoff for each item * await pEach(items, fetchItem, { * retries: { * retries: 3, * baseDelayMs: 1000, * backoffFactor: 2 * } * }) */ retries?: number | RetryOptions | undefined; /** * AbortSignal to support cancellation of the entire iteration. * When aborted, stops processing remaining items. * * @default process abort signal */ signal?: AbortSignal | undefined; } /** * Normalize options for iteration functions. * * Converts various option formats into a consistent structure with defaults applied. * Handles number shorthand for concurrency and ensures minimum values. * * @param options - Concurrency as number, or full options object, or undefined * @returns Normalized options with concurrency, retries, and signal * * @example * // Number shorthand for concurrency * normalizeIterationOptions(5) * // => { concurrency: 5, retries: {...}, signal: AbortSignal } * * @example * // Full options * normalizeIterationOptions({ concurrency: 3, retries: 2 }) * // => { concurrency: 3, retries: {...}, signal: AbortSignal } */ /*@__NO_SIDE_EFFECTS__*/ export declare function normalizeIterationOptions(options?: number | IterationOptions | undefined): { concurrency: number; retries: RetryOptions; signal: AbortSignal; }; /** * Normalize options for retry functionality. * * Converts various retry option formats into a complete configuration with all defaults. * Handles legacy property names (`factor`, `minTimeout`, `maxTimeout`) and merges them * with modern equivalents. * * @param options - Retry count as number, or full options object, or undefined * @returns Normalized retry options with all properties set * * @example * // Number shorthand * normalizeRetryOptions(3) * // => { retries: 3, baseDelayMs: 200, backoffFactor: 2, ... } * * @example * // Full options with defaults filled in * normalizeRetryOptions({ retries: 5, baseDelayMs: 500 }) * // => { retries: 5, baseDelayMs: 500, backoffFactor: 2, jitter: true, ... } */ /*@__NO_SIDE_EFFECTS__*/ export declare function normalizeRetryOptions(options?: number | RetryOptions | undefined): RetryOptions; /** * Resolve retry options from various input formats. * * Converts shorthand and partial options into a base configuration that can be * further normalized. This is an internal helper for option processing. * * @param options - Retry count as number, or partial options object, or undefined * @returns Resolved retry options with defaults for basic properties * * @example * resolveRetryOptions(3) * // => { retries: 3, minTimeout: 200, maxTimeout: 10000, factor: 2 } * * @example * resolveRetryOptions({ retries: 5, maxTimeout: 5000 }) * // => { retries: 5, minTimeout: 200, maxTimeout: 5000, factor: 2 } */ /*@__NO_SIDE_EFFECTS__*/ export declare function resolveRetryOptions(options?: number | RetryOptions | undefined): RetryOptions; /** * Execute an async function for each array element with concurrency control. * * Processes array items in parallel batches (chunks) with configurable concurrency. * Each item's callback can be retried independently on failure. Similar to * `Promise.all(array.map(fn))` but with controlled parallelism. * * @template T - The type of array elements * @param array - The array to iterate over * @param callbackFn - Async function to execute for each item * @param options - Concurrency as number, or full iteration options, or undefined * @returns Promise that resolves when all items are processed * * @example * // Process items serially (concurrency: 1) * await pEach(urls, async (url) => { * await fetch(url) * }) * * @example * // Process 5 items at a time * await pEach(files, async (file) => { * await processFile(file) * }, 5) * * @example * // With retries and cancellation * const controller = new AbortController() * await pEach(tasks, async (task) => { * await executeTask(task) * }, { * concurrency: 3, * retries: 2, * signal: controller.signal * }) */ /*@__NO_SIDE_EFFECTS__*/ export declare function pEach<T>(array: T[], callbackFn: (item: T) => Promise<unknown>, options?: number | IterationOptions | undefined): Promise<void>; /** * Filter an array asynchronously with concurrency control. * * Tests each element with an async predicate function, processing items in parallel * batches. Returns a new array with only items that pass the test. Similar to * `array.filter()` but for async predicates with controlled concurrency. * * @template T - The type of array elements * @param array - The array to filter * @param callbackFn - Async predicate function returning true to keep item * @param options - Concurrency as number, or full iteration options, or undefined * @returns Promise resolving to filtered array * * @example * // Filter serially * const activeUsers = await pFilter(users, async (user) => { * return await isUserActive(user.id) * }) * * @example * // Filter with concurrency * const validFiles = await pFilter(filePaths, async (path) => { * try { * await fs.access(path) * return true * } catch { * return false * } * }, 10) * * @example * // With retries for flaky checks * const reachable = await pFilter(endpoints, async (url) => { * const response = await fetch(url) * return response.ok * }, { * concurrency: 5, * retries: 2 * }) */ /*@__NO_SIDE_EFFECTS__*/ export declare function pFilter<T>(array: T[], callbackFn: (item: T) => Promise<boolean>, options?: number | IterationOptions | undefined): Promise<T[]>; /** * Process array in chunks with an async callback. * * Divides the array into fixed-size chunks and processes each chunk sequentially * with the callback. Useful for batch operations like bulk database inserts or * API calls with payload size limits. * * @template T - The type of array elements * @param array - The array to process in chunks * @param callbackFn - Async function to execute for each chunk * @param options - Chunk size and retry options * @returns Promise that resolves when all chunks are processed * * @example * // Insert records in batches of 100 * await pEachChunk(records, async (chunk) => { * await db.batchInsert(chunk) * }, { chunkSize: 100 }) * * @example * // Upload files in batches with retries * await pEachChunk(files, async (batch) => { * await uploadBatch(batch) * }, { * chunkSize: 50, * retries: 3, * baseDelayMs: 1000 * }) * * @example * // Process with cancellation support * const controller = new AbortController() * await pEachChunk(items, async (chunk) => { * await processChunk(chunk) * }, { * chunkSize: 25, * signal: controller.signal * }) */ /*@__NO_SIDE_EFFECTS__*/ export declare function pEachChunk<T>(array: T[], callbackFn: (chunk: T[]) => Promise<unknown>, options?: (RetryOptions & { chunkSize?: number | undefined; }) | undefined): Promise<void>; /** * Filter chunked arrays with an async predicate. * * Internal helper for `pFilter`. Processes pre-chunked arrays, applying the * predicate to each element within each chunk with retry support. * * @template T - The type of array elements * @param chunks - Pre-chunked array (array of arrays) * @param callbackFn - Async predicate function * @param options - Retry count as number, or full retry options, or undefined * @returns Promise resolving to array of filtered chunks * * @example * const chunks = [[1, 2], [3, 4], [5, 6]] * const filtered = await pFilterChunk(chunks, async (n) => n % 2 === 0) * // => [[2], [4], [6]] */ /*@__NO_SIDE_EFFECTS__*/ export declare function pFilterChunk<T>(chunks: T[][], callbackFn: (value: T) => Promise<boolean>, options?: number | RetryOptions | undefined): Promise<T[][]>; /** * Retry an async function with exponential backoff. * * Attempts to execute a function multiple times with increasing delays between attempts. * Implements exponential backoff with optional jitter to prevent thundering herd problems. * Supports custom retry logic via `onRetry` callback. * * The delay calculation follows: `min(baseDelayMs * (backoffFactor ** attempt), maxDelayMs)` * With jitter: adds random value between 0 and calculated delay. * * @template T - The return type of the callback function * @param callbackFn - Async function to retry * @param options - Retry count as number, or full retry options, or undefined * @returns Promise resolving to callback result, or `undefined` if aborted * * @throws {Error} The last error if all retry attempts fail * * @example * // Simple retry: 3 attempts with default backoff * const data = await pRetry(async () => { * return await fetchData() * }, 3) * * @example * // Custom backoff strategy * const result = await pRetry(async () => { * return await unreliableOperation() * }, { * retries: 5, * baseDelayMs: 1000, // Start at 1 second * backoffFactor: 2, // Double each time * maxDelayMs: 30000, // Cap at 30 seconds * jitter: true // Add randomness * }) * // Delays: ~1s, ~2s, ~4s, ~8s, ~16s (each ± random jitter) * * @example * // With custom retry logic * const data = await pRetry(async () => { * return await apiCall() * }, { * retries: 3, * onRetry: (attempt, error, delay) => { * console.log(`Attempt ${attempt} failed: ${error}`) * console.log(`Waiting ${delay}ms before retry...`) * * // Cancel retries for client errors (4xx) * if (error.statusCode >= 400 && error.statusCode < 500) { * return false * } * * // Use longer delay for rate limit errors * if (error.statusCode === 429) { * return 60000 // Wait 1 minute * } * }, * onRetryCancelOnFalse: true * }) * * @example * // With cancellation support * const controller = new AbortController() * setTimeout(() => controller.abort(), 5000) // Cancel after 5s * * const result = await pRetry(async ({ signal }) => { * return await longRunningTask(signal) * }, { * retries: 10, * signal: controller.signal * }) * // Returns undefined if aborted * * @example * // Pass arguments to callback * const result = await pRetry( * async (url, options) => { * return await fetch(url, options) * }, * { * retries: 3, * args: ['https://api.example.com', { method: 'POST' }] * } * ) */ /*@__NO_SIDE_EFFECTS__*/ export declare function pRetry<T>(callbackFn: (...args: unknown[]) => Promise<T>, options?: number | RetryOptions | undefined): Promise<T | undefined>;