UNPKG

@hyperlane-xyz/utils

Version:

General utilities and types for the Hyperlane network

334 lines 13.9 kB
import { expect } from 'chai'; import { concurrentMap, fetchWithTimeout, mapAllSettled, pollAsync, raceWithContext, retryAsync, runWithTimeout, sleep, timeout, } from './async.js'; describe('Async Utilities', () => { describe('sleep', () => { it('should resolve after sleep duration', async () => { const start = Date.now(); await sleep(100); const duration = Date.now() - start; expect(duration).to.be.at.least(95); expect(duration).to.be.lessThan(200); }); }); describe('timeout', () => { it('should timeout a promise', async () => { const promise = new Promise((resolve) => setTimeout(resolve, 200)); try { await timeout(promise, 100); throw new Error('Expected timeout error'); } catch (error) { expect(error.message).to.equal('Timeout reached'); } }); it('should clear timer when promise resolves', async () => { const origSetTimeout = global.setTimeout; const origClearTimeout = global.clearTimeout; let timerCleared = false; // Intercept setTimeout/clearTimeout to track cleanup global.setTimeout = ((fn, ms) => { const id = origSetTimeout(fn, ms); global.clearTimeout = ((clearId) => { if (clearId === id) timerCleared = true; origClearTimeout(clearId); }); return id; }); await timeout(Promise.resolve('ok'), 60_000); global.setTimeout = origSetTimeout; global.clearTimeout = origClearTimeout; expect(timerCleared).to.equal(true); }); it('should clear timer when promise rejects', async () => { const origSetTimeout = global.setTimeout; const origClearTimeout = global.clearTimeout; let timerCleared = false; global.setTimeout = ((fn, ms) => { const id = origSetTimeout(fn, ms); global.clearTimeout = ((clearId) => { if (clearId === id) timerCleared = true; origClearTimeout(clearId); }); return id; }); try { await timeout(Promise.reject(new Error('fail')), 60_000); } catch { // expected } global.setTimeout = origSetTimeout; global.clearTimeout = origClearTimeout; expect(timerCleared).to.equal(true); }); }); describe('runWithTimeout', () => { it('should run a callback with a timeout', async () => { const result = await runWithTimeout(100, async () => { await sleep(50); return 'success'; }); expect(result).to.equal('success'); }); }); describe('fetchWithTimeout', () => { it('should fetch with timeout', async () => { // Mock fetch for testing global.fetch = async () => { await sleep(50); return new Response('ok'); }; const response = await fetchWithTimeout('https://example.com', {}, 100); expect(await response.text()).to.equal('ok'); }); }); describe('retryAsync', () => { it('should retry until success', async () => { let attempt = 0; const runner = async () => { attempt++; if (attempt < 3) throw new Error('fail'); return 'success'; }; const result = await retryAsync(runner, 5, 10); expect(result).to.equal('success'); }); it('should retry `attempts` times at most', async () => { let attempt = 0; const runner = async () => { attempt++; throw new Error('fail'); }; try { await retryAsync(runner, 5, 10); throw new Error('Expected error to be thrown'); } catch (error) { expect(error.message).to.equal('fail'); expect(attempt).to.equal(5); } }); it('should immediately throw error if isRecoverable is false', async () => { let attempts = 0; const runner = async () => { attempts++; const error = new Error('non-recoverable error'); error.isRecoverable = false; throw error; }; try { await retryAsync(runner, 5, 10); throw new Error('Expected error to be thrown'); } catch (error) { expect(error.message).to.equal('non-recoverable error'); expect(error.isRecoverable).to.equal(false); expect(attempts).to.equal(1); } }); it('should continue retrying if isRecoverable is not set', async () => { let attempt = 0; const runner = async () => { attempt++; if (attempt < 3) throw new Error('recoverable error'); return 'success'; }; const result = await retryAsync(runner, 5, 10); expect(result).to.equal('success'); expect(attempt).to.equal(3); }); it('should continue retrying if isRecoverable is true', async () => { let attempt = 0; const runner = async () => { attempt++; if (attempt < 3) { const error = new Error('recoverable error'); error.isRecoverable = true; throw error; } return 'success'; }; const result = await retryAsync(runner, 5, 10); expect(result).to.equal('success'); expect(attempt).to.equal(3); }); it('should execute at least once even with 0 attempts', async () => { let attempts = 0; const runner = async () => { attempts++; return 'success'; }; const result = await retryAsync(runner, 0, 10); expect(result).to.equal('success'); expect(attempts).to.equal(1); }); it('should execute at least once even with negative attempts', async () => { let attempts = 0; const runner = async () => { attempts++; return 'success'; }; const result = await retryAsync(runner, -5, 10); expect(result).to.equal('success'); expect(attempts).to.equal(1); }); }); describe('pollAsync', () => { it('should poll async function until success', async () => { let attempt = 0; const runner = async () => { attempt++; if (attempt < 3) throw new Error('fail'); return 'success'; }; const result = await pollAsync(runner, 10, 5); expect(result).to.equal('success'); }); it('should fail after reaching max retries', async () => { let attempt = 0; const runner = async () => { attempt++; throw new Error('fail'); }; try { await pollAsync(runner, 10, 3); // Set maxAttempts to 3 throw new Error('Expected pollAsync to throw an error'); } catch (error) { expect(attempt).to.equal(3); // Ensure it attempted 3 times expect(error.message).to.equal('fail'); } }); }); describe('raceWithContext', () => { it('should race with context', async () => { const promises = [ sleep(50).then(() => 'first'), sleep(100).then(() => 'second'), ]; const result = await raceWithContext(promises); expect(result.resolved).to.equal('first'); expect(result.index).to.equal(0); }); }); describe('concurrentMap', () => { it('should map concurrently with correct results', async () => { const xs = [1, 2, 3, 4, 5, 6]; const mapFn = async (val) => { await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate async work return val * 2; }; const result = await concurrentMap(2, xs, mapFn); expect(result).to.deep.equal([2, 4, 6, 8, 10, 12]); }); it('should respect concurrency limit', async () => { const xs = [1, 2, 3, 4, 5, 6]; const concurrency = 2; let activeTasks = 0; let maxActiveTasks = 0; const mapFn = async (val) => { activeTasks++; maxActiveTasks = Math.max(maxActiveTasks, activeTasks); await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate async work activeTasks--; return val * 2; }; await concurrentMap(concurrency, xs, mapFn); expect(maxActiveTasks).to.equal(concurrency); }); }); }); describe('mapAllSettled', () => { it('should return all fulfilled results when all promises succeed', async () => { const items = ['a', 'b', 'c']; const { fulfilled, rejected } = await mapAllSettled(items, async (item) => item.toUpperCase(), (item) => item); expect(fulfilled.size).to.equal(3); expect(fulfilled.get('a')).to.equal('A'); expect(fulfilled.get('b')).to.equal('B'); expect(fulfilled.get('c')).to.equal('C'); expect(rejected.size).to.equal(0); }); it('should return all rejected results when all promises fail', async () => { const items = ['a', 'b', 'c']; const { fulfilled, rejected } = await mapAllSettled(items, async (item) => { throw new Error(`Failed: ${item}`); }, (item) => item); expect(fulfilled.size).to.equal(0); expect(rejected.size).to.equal(3); expect(rejected.get('a')?.message).to.equal('Failed: a'); expect(rejected.get('b')?.message).to.equal('Failed: b'); expect(rejected.get('c')?.message).to.equal('Failed: c'); }); it('should handle mixed success and failure', async () => { const items = [1, 2, 3, 4, 5]; const { fulfilled, rejected } = await mapAllSettled(items, async (item) => { if (item % 2 === 0) { throw new Error(`Even number: ${item}`); } return item * 10; }, (item) => item); expect(fulfilled.size).to.equal(3); expect(fulfilled.get(1)).to.equal(10); expect(fulfilled.get(3)).to.equal(30); expect(fulfilled.get(5)).to.equal(50); expect(rejected.size).to.equal(2); expect(rejected.get(2)?.message).to.equal('Even number: 2'); expect(rejected.get(4)?.message).to.equal('Even number: 4'); }); it('should use index as key when keyFn is not provided', async () => { const items = ['a', 'b', 'c']; const { fulfilled, rejected } = await mapAllSettled(items, async (item) => item.toUpperCase()); expect(fulfilled.size).to.equal(3); expect(fulfilled.get(0)).to.equal('A'); expect(fulfilled.get(1)).to.equal('B'); expect(fulfilled.get(2)).to.equal('C'); expect(rejected.size).to.equal(0); }); it('should convert non-Error rejection reasons to Error objects', async () => { const items = ['a']; const { rejected } = await mapAllSettled(items, async () => { throw 'string error'; }, (item) => item); expect(rejected.size).to.equal(1); expect(rejected.get('a')).to.be.instanceOf(Error); expect(rejected.get('a')?.message).to.equal('string error'); }); it('should handle empty array', async () => { const items = []; const { fulfilled, rejected } = await mapAllSettled(items, async (item) => item.toUpperCase(), (item) => item); expect(fulfilled.size).to.equal(0); expect(rejected.size).to.equal(0); }); it('should pass index to mapFn', async () => { const items = ['a', 'b', 'c']; const { fulfilled } = await mapAllSettled(items, async (item, index) => `${item}-${index}`, (item) => item); expect(fulfilled.get('a')).to.equal('a-0'); expect(fulfilled.get('b')).to.equal('b-1'); expect(fulfilled.get('c')).to.equal('c-2'); }); it('should pass index to keyFn', async () => { const items = ['a', 'b', 'c']; const { fulfilled } = await mapAllSettled(items, async (item) => item.toUpperCase(), (_item, index) => `key-${index}`); expect(fulfilled.get('key-0')).to.equal('A'); expect(fulfilled.get('key-1')).to.equal('B'); expect(fulfilled.get('key-2')).to.equal('C'); }); it('should process items in parallel', async () => { const items = [1, 2, 3]; const startTime = Date.now(); await mapAllSettled(items, async () => { await sleep(50); return 'done'; }, (item) => item); const duration = Date.now() - startTime; // If run in parallel, should take ~50ms, not ~150ms // Using 150ms threshold to avoid CI flakiness from timing jitter expect(duration).to.be.lessThan(150); }); }); //# sourceMappingURL=async.test.js.map