- 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.
200 lines
4.7 KiB
TypeScript
200 lines
4.7 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import {
|
|
delay,
|
|
retryWithBackoff,
|
|
withTimeout,
|
|
parallelLimit,
|
|
debounceAsync,
|
|
AsyncMutex,
|
|
CircuitBreaker
|
|
} from '../../../ts/core/utils/async-utils.js';
|
|
|
|
tap.test('delay should pause execution for specified milliseconds', async () => {
|
|
const startTime = Date.now();
|
|
await delay(100);
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
// Allow some tolerance for timing
|
|
expect(elapsed).toBeGreaterThan(90);
|
|
expect(elapsed).toBeLessThan(150);
|
|
});
|
|
|
|
tap.test('retryWithBackoff should retry failed operations', async () => {
|
|
let attempts = 0;
|
|
const operation = async () => {
|
|
attempts++;
|
|
if (attempts < 3) {
|
|
throw new Error('Test error');
|
|
}
|
|
return 'success';
|
|
};
|
|
|
|
const result = await retryWithBackoff(operation, {
|
|
maxAttempts: 3,
|
|
initialDelay: 10
|
|
});
|
|
|
|
expect(result).toEqual('success');
|
|
expect(attempts).toEqual(3);
|
|
});
|
|
|
|
tap.test('retryWithBackoff should throw after max attempts', async () => {
|
|
let attempts = 0;
|
|
const operation = async () => {
|
|
attempts++;
|
|
throw new Error('Always fails');
|
|
};
|
|
|
|
let error: Error | null = null;
|
|
try {
|
|
await retryWithBackoff(operation, {
|
|
maxAttempts: 2,
|
|
initialDelay: 10
|
|
});
|
|
} catch (e: any) {
|
|
error = e;
|
|
}
|
|
|
|
expect(error).not.toBeNull();
|
|
expect(error?.message).toEqual('Always fails');
|
|
expect(attempts).toEqual(2);
|
|
});
|
|
|
|
tap.test('withTimeout should complete operations within timeout', async () => {
|
|
const operation = async () => {
|
|
await delay(50);
|
|
return 'completed';
|
|
};
|
|
|
|
const result = await withTimeout(operation, 100);
|
|
expect(result).toEqual('completed');
|
|
});
|
|
|
|
tap.test('withTimeout should throw on timeout', async () => {
|
|
const operation = async () => {
|
|
await delay(200);
|
|
return 'never happens';
|
|
};
|
|
|
|
let error: Error | null = null;
|
|
try {
|
|
await withTimeout(operation, 50);
|
|
} catch (e: any) {
|
|
error = e;
|
|
}
|
|
|
|
expect(error).not.toBeNull();
|
|
expect(error?.message).toContain('timed out');
|
|
});
|
|
|
|
tap.test('parallelLimit should respect concurrency limit', async () => {
|
|
let concurrent = 0;
|
|
let maxConcurrent = 0;
|
|
|
|
const items = [1, 2, 3, 4, 5, 6];
|
|
const operation = async (item: number) => {
|
|
concurrent++;
|
|
maxConcurrent = Math.max(maxConcurrent, concurrent);
|
|
await delay(50);
|
|
concurrent--;
|
|
return item * 2;
|
|
};
|
|
|
|
const results = await parallelLimit(items, operation, 2);
|
|
|
|
expect(results).toEqual([2, 4, 6, 8, 10, 12]);
|
|
expect(maxConcurrent).toBeLessThan(3);
|
|
expect(maxConcurrent).toBeGreaterThan(0);
|
|
});
|
|
|
|
tap.test('debounceAsync should debounce function calls', async () => {
|
|
let callCount = 0;
|
|
const fn = async (value: string) => {
|
|
callCount++;
|
|
return value;
|
|
};
|
|
|
|
const debounced = debounceAsync(fn, 50);
|
|
|
|
// Make multiple calls quickly
|
|
debounced('a');
|
|
debounced('b');
|
|
debounced('c');
|
|
const result = await debounced('d');
|
|
|
|
// Wait a bit to ensure no more calls
|
|
await delay(100);
|
|
|
|
expect(result).toEqual('d');
|
|
expect(callCount).toEqual(1); // Only the last call should execute
|
|
});
|
|
|
|
tap.test('AsyncMutex should ensure exclusive access', async () => {
|
|
const mutex = new AsyncMutex();
|
|
const results: number[] = [];
|
|
|
|
const operation = async (value: number) => {
|
|
await mutex.runExclusive(async () => {
|
|
results.push(value);
|
|
await delay(10);
|
|
results.push(value * 10);
|
|
});
|
|
};
|
|
|
|
// Run operations concurrently
|
|
await Promise.all([
|
|
operation(1),
|
|
operation(2),
|
|
operation(3)
|
|
]);
|
|
|
|
// Results should show sequential execution
|
|
expect(results).toEqual([1, 10, 2, 20, 3, 30]);
|
|
});
|
|
|
|
tap.test('CircuitBreaker should open after failures', async () => {
|
|
const breaker = new CircuitBreaker({
|
|
failureThreshold: 2,
|
|
resetTimeout: 100
|
|
});
|
|
|
|
let attempt = 0;
|
|
const failingOperation = async () => {
|
|
attempt++;
|
|
throw new Error('Test failure');
|
|
};
|
|
|
|
// First two failures
|
|
for (let i = 0; i < 2; i++) {
|
|
try {
|
|
await breaker.execute(failingOperation);
|
|
} catch (e) {
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
expect(breaker.isOpen()).toBeTrue();
|
|
|
|
// Next attempt should fail immediately
|
|
let error: Error | null = null;
|
|
try {
|
|
await breaker.execute(failingOperation);
|
|
} catch (e: any) {
|
|
error = e;
|
|
}
|
|
|
|
expect(error?.message).toEqual('Circuit breaker is open');
|
|
expect(attempt).toEqual(2); // Operation not called when circuit is open
|
|
|
|
// Wait for reset timeout
|
|
await delay(150);
|
|
|
|
// Circuit should be half-open now, allowing one attempt
|
|
const successOperation = async () => 'success';
|
|
const result = await breaker.execute(successOperation);
|
|
|
|
expect(result).toEqual('success');
|
|
expect(breaker.getState()).toEqual('closed');
|
|
});
|
|
|
|
tap.start(); |