@socketsecurity/lib
Version:
Core utilities and infrastructure for Socket.dev security tools
458 lines (457 loc) • 15.6 kB
TypeScript
/**
* 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>;