- Implemented async utilities including delay, retryWithBackoff, withTimeout, parallelLimit, debounceAsync, AsyncMutex, and CircuitBreaker. - Created tests for async utilities to ensure functionality and reliability. - Developed AsyncFileSystem class with methods for file and directory operations, including ensureDir, readFile, writeFile, remove, and more. - Added tests for filesystem utilities to validate file operations and error handling.
275 lines
6.3 KiB
TypeScript
275 lines
6.3 KiB
TypeScript
/**
|
|
* Async utility functions for SmartProxy
|
|
* Provides non-blocking alternatives to synchronous operations
|
|
*/
|
|
|
|
/**
|
|
* Delays execution for the specified number of milliseconds
|
|
* Non-blocking alternative to busy wait loops
|
|
* @param ms - Number of milliseconds to delay
|
|
* @returns Promise that resolves after the delay
|
|
*/
|
|
export async function delay(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Retry an async operation with exponential backoff
|
|
* @param fn - The async function to retry
|
|
* @param options - Retry options
|
|
* @returns The result of the function or throws the last error
|
|
*/
|
|
export async function retryWithBackoff<T>(
|
|
fn: () => Promise<T>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
initialDelay?: number;
|
|
maxDelay?: number;
|
|
factor?: number;
|
|
onRetry?: (attempt: number, error: Error) => void;
|
|
} = {}
|
|
): Promise<T> {
|
|
const {
|
|
maxAttempts = 3,
|
|
initialDelay = 100,
|
|
maxDelay = 10000,
|
|
factor = 2,
|
|
onRetry
|
|
} = options;
|
|
|
|
let lastError: Error | null = null;
|
|
let currentDelay = initialDelay;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error: any) {
|
|
lastError = error;
|
|
|
|
if (attempt === maxAttempts) {
|
|
throw error;
|
|
}
|
|
|
|
if (onRetry) {
|
|
onRetry(attempt, error);
|
|
}
|
|
|
|
await delay(currentDelay);
|
|
currentDelay = Math.min(currentDelay * factor, maxDelay);
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error('Retry failed');
|
|
}
|
|
|
|
/**
|
|
* Execute an async operation with a timeout
|
|
* @param fn - The async function to execute
|
|
* @param timeoutMs - Timeout in milliseconds
|
|
* @param timeoutError - Optional custom timeout error
|
|
* @returns The result of the function or throws timeout error
|
|
*/
|
|
export async function withTimeout<T>(
|
|
fn: () => Promise<T>,
|
|
timeoutMs: number,
|
|
timeoutError?: Error
|
|
): Promise<T> {
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
setTimeout(() => {
|
|
reject(timeoutError || new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
}, timeoutMs);
|
|
});
|
|
|
|
return Promise.race([fn(), timeoutPromise]);
|
|
}
|
|
|
|
/**
|
|
* Run multiple async operations in parallel with a concurrency limit
|
|
* @param items - Array of items to process
|
|
* @param fn - Async function to run for each item
|
|
* @param concurrency - Maximum number of concurrent operations
|
|
* @returns Array of results in the same order as input
|
|
*/
|
|
export async function parallelLimit<T, R>(
|
|
items: T[],
|
|
fn: (item: T, index: number) => Promise<R>,
|
|
concurrency: number
|
|
): Promise<R[]> {
|
|
const results: R[] = new Array(items.length);
|
|
const executing: Set<Promise<void>> = new Set();
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
const promise = fn(items[i], i).then(result => {
|
|
results[i] = result;
|
|
executing.delete(promise);
|
|
});
|
|
|
|
executing.add(promise);
|
|
|
|
if (executing.size >= concurrency) {
|
|
await Promise.race(executing);
|
|
}
|
|
}
|
|
|
|
await Promise.all(executing);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Debounce an async function
|
|
* @param fn - The async function to debounce
|
|
* @param delayMs - Delay in milliseconds
|
|
* @returns Debounced function with cancel method
|
|
*/
|
|
export function debounceAsync<T extends (...args: any[]) => Promise<any>>(
|
|
fn: T,
|
|
delayMs: number
|
|
): T & { cancel: () => void } {
|
|
let timeoutId: NodeJS.Timeout | null = null;
|
|
let lastPromise: Promise<any> | null = null;
|
|
|
|
const debounced = ((...args: Parameters<T>) => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
|
|
lastPromise = new Promise((resolve, reject) => {
|
|
timeoutId = setTimeout(async () => {
|
|
timeoutId = null;
|
|
try {
|
|
const result = await fn(...args);
|
|
resolve(result);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
}, delayMs);
|
|
});
|
|
|
|
return lastPromise;
|
|
}) as any;
|
|
|
|
debounced.cancel = () => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
};
|
|
|
|
return debounced as T & { cancel: () => void };
|
|
}
|
|
|
|
/**
|
|
* Create a mutex for ensuring exclusive access to a resource
|
|
*/
|
|
export class AsyncMutex {
|
|
private queue: Array<() => void> = [];
|
|
private locked = false;
|
|
|
|
async acquire(): Promise<() => void> {
|
|
if (!this.locked) {
|
|
this.locked = true;
|
|
return () => this.release();
|
|
}
|
|
|
|
return new Promise<() => void>(resolve => {
|
|
this.queue.push(() => {
|
|
resolve(() => this.release());
|
|
});
|
|
});
|
|
}
|
|
|
|
private release(): void {
|
|
const next = this.queue.shift();
|
|
if (next) {
|
|
next();
|
|
} else {
|
|
this.locked = false;
|
|
}
|
|
}
|
|
|
|
async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
|
|
const release = await this.acquire();
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Circuit breaker for protecting against cascading failures
|
|
*/
|
|
export class CircuitBreaker {
|
|
private failureCount = 0;
|
|
private lastFailureTime = 0;
|
|
private state: 'closed' | 'open' | 'half-open' = 'closed';
|
|
|
|
constructor(
|
|
private options: {
|
|
failureThreshold: number;
|
|
resetTimeout: number;
|
|
onStateChange?: (state: 'closed' | 'open' | 'half-open') => void;
|
|
}
|
|
) {}
|
|
|
|
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
if (this.state === 'open') {
|
|
if (Date.now() - this.lastFailureTime > this.options.resetTimeout) {
|
|
this.setState('half-open');
|
|
} else {
|
|
throw new Error('Circuit breaker is open');
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await fn();
|
|
this.onSuccess();
|
|
return result;
|
|
} catch (error) {
|
|
this.onFailure();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private onSuccess(): void {
|
|
this.failureCount = 0;
|
|
if (this.state !== 'closed') {
|
|
this.setState('closed');
|
|
}
|
|
}
|
|
|
|
private onFailure(): void {
|
|
this.failureCount++;
|
|
this.lastFailureTime = Date.now();
|
|
|
|
if (this.failureCount >= this.options.failureThreshold) {
|
|
this.setState('open');
|
|
}
|
|
}
|
|
|
|
private setState(state: 'closed' | 'open' | 'half-open'): void {
|
|
if (this.state !== state) {
|
|
this.state = state;
|
|
if (this.options.onStateChange) {
|
|
this.options.onStateChange(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
isOpen(): boolean {
|
|
return this.state === 'open';
|
|
}
|
|
|
|
getState(): 'closed' | 'open' | 'half-open' {
|
|
return this.state;
|
|
}
|
|
|
|
recordSuccess(): void {
|
|
this.onSuccess();
|
|
}
|
|
|
|
recordFailure(): void {
|
|
this.onFailure();
|
|
}
|
|
} |