Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | 7x 13x 13x 13x 13x 20x 20x 6x 20x 20x 20x 20x 276x 276x 276x 2x 2x 276x 276x 218x 276x 276x 272x 1x 271x 2x 269x 3x 266x 3x 11x 272x 272x 272x 289x 289x 218x 68x 68x 2x 66x 30x 36x 26x 26x 26x 36x 6x 4x 1x 3x 4x | /**
* Request Timeout Utilities
*
* ISMS Policy: SC-002 (Secure Coding), PE-001 (Performance Standards)
*
* Provides timeout handling for long-running operations to prevent
* resource exhaustion and ensure responsive API behavior.
*
* @see https://github.com/Hack23/ISMS-PUBLIC/blob/main/Secure_Development_Policy.md
*/
/**
* Typed configuration for timeout operations.
*
* Centralises timeout settings in a single object so they can be
* stored, passed, and validated without scattered magic numbers.
*/
export interface TimeoutConfig {
/** Timeout duration in milliseconds (must be > 0) */
timeoutMs: number;
/** Optional human-readable label for the timed operation */
operationName?: string;
/** Custom error message override for timeout errors */
errorMessage?: string;
}
/**
* Default timeout configurations for common operation types.
* These are compile-time constants; override by passing a custom {@link TimeoutConfig}
* at the call site rather than relying on environment variables.
*/
export const DEFAULT_TIMEOUTS = {
/** Standard EP API HTTP request (10 s) */
EP_API_REQUEST_MS: 10_000,
/** Short health-check probe (3 s) */
HEALTH_CHECK_MS: 3_000,
/** Retry delay base (1 s) */
RETRY_DELAY_MS: 1_000,
} as const;
/**
* Timeout error thrown when an operation exceeds its time limit
*
* @example
* ```typescript
* if (Date.now() - startTime > timeout) {
* throw new TimeoutError('Operation timed out after 10s');
* }
* ```
*/
export class TimeoutError extends Error {
/**
* Create a new timeout error
*
* @param message - Description of the timeout
* @param timeoutMs - The timeout duration in milliseconds
*/
constructor(
message: string,
public readonly timeoutMs?: number
) {
super(message);
this.name = 'TimeoutError';
Error.captureStackTrace(this, this.constructor);
}
}
/**
* Execute a promise with a timeout
*
* Races the provided promise against a timeout. If the timeout expires
* before the promise resolves, a TimeoutError is thrown.
*
* @template T - Type of the promise result
* @param promise - Promise to execute with timeout
* @param timeoutMs - Timeout in milliseconds
* @param errorMessage - Optional custom error message
* @returns Promise that resolves with the result or rejects with TimeoutError
*
* @throws {TimeoutError} If operation exceeds timeout
*
* @example
* ```typescript
* const result = await withTimeout(
* fetchFromAPI('/endpoint'),
* 5000,
* 'API request timed out'
* );
* ```
*
* @security
* - Prevents resource exhaustion from hanging operations
* - Ensures responsive API behavior
* - Timeout values should be tuned per operation
* @since 0.8.0
*/
export async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
errorMessage?: string
): Promise<T> {
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
// Create timeout promise that rejects after specified time
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => {
reject(
new TimeoutError(
errorMessage ?? `Operation timed out after ${String(timeoutMs)}ms`,
timeoutMs
)
);
}, timeoutMs);
});
// Race between the actual operation and timeout
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
// Always clear timeout to prevent memory leaks
Eif (timeoutHandle !== undefined) {
clearTimeout(timeoutHandle);
}
}
}
/**
* Wraps a promise with a timeout and optional AbortSignal support.
*
* For operations that support cancellation (like fetch), pass a function that
* accepts an AbortSignal. The signal will be aborted when the timeout fires,
* allowing the underlying operation to clean up resources.
*
* @template T - Type of the promise result
* @param fn - Function that returns a promise and optionally accepts an AbortSignal
* @param timeoutMs - Timeout in milliseconds
* @param errorMessage - Custom error message (optional)
* @returns Promise that resolves/rejects with the operation result or timeout
* @throws {TimeoutError} If the operation exceeds `timeoutMs`
*
* @example
* ```typescript
* // With AbortSignal support (for fetch, etc.)
* await withTimeoutAndAbort(
* (signal) => fetch(url, { signal }),
* 5000,
* 'API request timed out'
* );
* ```
*
* @security Aborts the underlying operation via `AbortController` when the
* timeout fires, preventing dangling fetch connections and resource leaks.
* Per ISMS Policy SC-002, all external network calls must be cancellable.
* @since 0.8.0
*/
export async function withTimeoutAndAbort<T>(
fn: (signal: AbortSignal) => Promise<T>,
timeoutMs: number,
errorMessage?: string
): Promise<T> {
const controller = new AbortController();
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
// Create timeout promise that rejects and aborts after specified time
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => {
controller.abort(); // Cancel the underlying operation
reject(
new TimeoutError(
errorMessage ?? `Operation timed out after ${String(timeoutMs)}ms`,
timeoutMs
)
);
}, timeoutMs);
});
// Race between the actual operation and timeout
try {
const result = await Promise.race([fn(controller.signal), timeoutPromise]);
return result;
} finally {
// Always clear timeout to prevent memory leaks
Eif (timeoutHandle !== undefined) {
clearTimeout(timeoutHandle);
}
}
}
/**
* Validate withRetry options
*
* @param maxRetries - Maximum retry count
* @param timeoutMs - Timeout duration
* @param retryDelayMs - Retry delay duration
* @param maxDelayMs - Maximum delay cap
* @throws {Error} If any option is invalid
*/
function validateRetryOptions(
maxRetries: number,
timeoutMs: number | undefined,
retryDelayMs: number,
maxDelayMs: number
): void {
if (maxRetries < 0) {
throw new Error('maxRetries must be non-negative');
}
if (timeoutMs !== undefined && timeoutMs <= 0) {
throw new Error('timeoutMs must be positive');
}
if (!Number.isFinite(retryDelayMs) || retryDelayMs <= 0) {
throw new Error('retryDelayMs must be positive');
}
if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0) {
throw new Error('maxDelayMs must be positive');
}
}
/**
* Execute a function with retry logic and timeout
*
* Retries the operation up to {@link options.maxRetries} times (for a total
* of maxRetries + 1 attempts including the initial call). Each retry has its
* own timeout (if timeoutMs is provided).
*
* By default, all non-{@link TimeoutError} failures are considered retryable.
* To restrict retries to transient failures only (for example, network
* errors or 5xx status codes), provide a {@link options.shouldRetry}
* predicate that returns true only for errors that should be retried.
*
* @template T - Type of the function result
* @param fn - Async function to execute
* @param options - Retry and timeout configuration
* @param options.maxRetries - Maximum number of retry attempts after the initial call
* @param options.timeoutMs - Optional per-attempt timeout in milliseconds (omit if fn handles timeout internally)
* @param options.retryDelayMs - Base delay between retry attempts in milliseconds (default: 1000)
* @param options.maxDelayMs - Maximum delay cap in milliseconds (default: 30000); prevents unbounded backoff growth
* @param options.timeoutErrorMessage - Custom error message for timeout errors
* @param options.shouldRetry - Predicate that decides if a failed attempt should be retried (default: retry all non-timeout errors)
* @returns Promise that resolves with the result
*
* @throws {TimeoutError} If any attempt exceeds timeout
* @throws {Error} If all retries are exhausted
*
* @example
* ```typescript
* // Retry up to 3 times (4 total attempts) on 5xx errors only with timeout
* const data = await withRetry(
* () => fetchFromAPI('/endpoint'),
* {
* maxRetries: 3,
* timeoutMs: 5000,
* retryDelayMs: 1000,
* shouldRetry: (error) => error.statusCode >= 500
* }
* );
*
* // Retry without additional timeout (fn handles timeout internally)
* const data2 = await withRetry(
* () => withTimeoutAndAbort(signal => fetch(url, { signal }), 5000),
* {
* maxRetries: 3,
* retryDelayMs: 1000,
* shouldRetry: (error) => error.statusCode >= 500
* }
* );
* ```
*
* @security
* - Prevents retry storms with exponential backoff
* - Respects timeout limits per attempt (when provided)
* - Configurable retry conditions for security
* @since 0.8.0
*/
export async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxRetries: number;
timeoutMs?: number;
retryDelayMs?: number;
maxDelayMs?: number;
timeoutErrorMessage?: string;
shouldRetry?: (error: unknown) => boolean;
}
): Promise<T> {
const {
maxRetries,
timeoutMs,
retryDelayMs = 1000,
maxDelayMs = 30_000,
timeoutErrorMessage,
shouldRetry = (): boolean => true
} = options;
// Input validation
validateRetryOptions(maxRetries, timeoutMs, retryDelayMs, maxDelayMs);
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// Add timeout to each attempt only if timeoutMs is provided
// If the function handles timeout internally (e.g., withTimeoutAndAbort), skip wrapping
const result = timeoutMs !== undefined
? await withTimeout(fn(), timeoutMs, timeoutErrorMessage)
: await fn();
return result;
} catch (error) {
lastError = error;
// Don't retry timeout errors
if (error instanceof TimeoutError) {
throw error;
}
// Check if we should retry this error
if (!shouldRetry(error)) {
throw error;
}
// Don't wait after the last attempt
if (attempt < maxRetries) {
// Exponential backoff with jitter and max cap to prevent thundering herd
const baseDelay = Math.min(retryDelayMs * Math.pow(2, attempt), maxDelayMs);
const jitter = baseDelay * 0.1 * Math.random();
await new Promise(resolve => setTimeout(resolve, baseDelay + jitter));
}
}
}
// All retries exhausted - this should always have a value since we attempt at least once
throw lastError;
}
/**
* Type guard to check if an error is a TimeoutError
*
* @param error - Error to check
* @returns true if error is a TimeoutError
*
* @example
* ```typescript
* try {
* await withTimeout(operation(), 5000);
* } catch (error) {
* if (isTimeoutError(error)) {
* console.error('Operation timed out:', error.timeoutMs);
* }
* }
* ```
*
* @since 0.8.0
*/
export function isTimeoutError(error: unknown): error is TimeoutError {
return error instanceof TimeoutError;
}
/**
* Execute a promise with timeout settings from a {@link TimeoutConfig}.
*
* Convenience wrapper around {@link withTimeout} for callers that
* already hold a `TimeoutConfig` object (e.g., from environment config).
*
* @template T - Type of the promise result
* @param promise - Promise to execute with timeout
* @param config - Timeout configuration object
* @returns Promise resolving with the result or rejecting with TimeoutError
*
* @throws {TimeoutError} If the operation exceeds `config.timeoutMs`
* @throws {Error} If `config.timeoutMs` is not positive
*
* @example
* ```typescript
* const config: TimeoutConfig = { timeoutMs: 5000, operationName: 'fetchMEPs' };
* const result = await withTimeoutConfig(fetchMEPs(), config);
* ```
*
* @since 0.8.0
*/
export async function withTimeoutConfig<T>(
promise: Promise<T>,
config: TimeoutConfig
): Promise<T> {
if (config.timeoutMs <= 0) {
throw new Error(`withTimeoutConfig: timeoutMs must be > 0, got ${String(config.timeoutMs)}`);
}
const message =
config.errorMessage ??
`${config.operationName ?? 'Operation'} timed out after ${String(config.timeoutMs)}ms`;
return withTimeout(promise, config.timeoutMs, message);
}
|