blob: 5887f40769c1165903d66d5eb6c9eaa61f8337db [file] [log] [blame]
import { expect } from 'chai';
import {
asyncFilter, asyncFind, asyncForEach, asyncMap,
} from './async';
describe('async utilities', () => {
describe('asyncFind', () => {
it('handles empty inputs', async () => {
const visitedItems: number[] = [];
const finderFn = async (item: any) => {
await simulateAsyncOp();
visitedItems.push(item);
return true;
};
// Raw array.
expect(await asyncFind([], finderFn)).to.be.null;
expect(visitedItems).to.be.empty;
// Array wrapped in promise.
expect(await asyncFind(wrapInPromise([]), finderFn)).to.be.null;
expect(visitedItems).to.be.empty;
});
it('finds it', async () => {
let visitedItems: string[];
let visitedIndices: number[];
const finderFn = async (item: string, index: number) => {
await simulateAsyncOp();
visitedItems.push(item);
visitedIndices.push(index);
return item.startsWith('g');
};
const input = ['alpha', 'beta', 'gamma', 'delta'];
// Raw array.
visitedItems = [];
visitedIndices = [];
expect(await asyncFind(input, finderFn)).to.equal('gamma');
expect(visitedItems).to.deep.equal(['alpha', 'beta', 'gamma']);
expect(visitedIndices).to.deep.equal([0, 1, 2]);
// Array wrapped in promise.
visitedItems = [];
visitedIndices = [];
expect(await asyncFind(wrapInPromise(input), finderFn)).to.equal('gamma');
expect(visitedItems).to.deep.equal(['alpha', 'beta', 'gamma']);
expect(visitedIndices).to.deep.equal([0, 1, 2]);
});
it('does not find it', async () => {
let visitedItems: string[];
let visitedIndices: number[];
const finderFn = async (item: string, index: number) => {
await simulateAsyncOp();
visitedItems.push(item);
visitedIndices.push(index);
return false; // Never finds it.
};
const input = ['alpha', 'beta', 'gamma', 'delta'];
// Raw array.
visitedItems = [];
visitedIndices = [];
expect(await asyncFind(input, finderFn)).to.be.null;
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1, 2, 3]);
// Array wrapped in promise.
visitedItems = [];
visitedIndices = [];
expect(await asyncFind(wrapInPromise(input), finderFn)).to.be.null;
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1, 2, 3]);
});
});
describe('asyncFilter', () => {
it('handles empty inputs', async () => {
const visitedIndices: number[] = [];
const filterFn = async (item: any, index: number) => {
await simulateAsyncOp();
visitedIndices.push(index);
return true;
};
// Raw array.
expect(await asyncFilter([], filterFn)).to.be.empty;
expect(visitedIndices).to.be.empty;
// Array wrapped in promise.
expect(await asyncFilter(wrapInPromise([]), filterFn)).to.be.empty;
expect(visitedIndices).to.be.empty;
});
it('filters the input', async () => {
let visitedItems: string[];
let visitedIndices: number[];
const filterFn = async (item: string, index: number) => {
await simulateAsyncOp();
visitedItems.push(item);
visitedIndices.push(index);
return item.startsWith('a') || item.startsWith('g');
};
const input = ['alpha', 'beta', 'gamma', 'delta'];
// Raw array.
visitedItems = [];
visitedIndices = [];
expect(await asyncFilter(input, filterFn)).to.deep.equal(['alpha', 'gamma']);
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1, 2, 3]);
// Array wrapped in promise.
visitedItems = [];
visitedIndices = [];
expect(await asyncFilter(wrapInPromise(input), filterFn)).to.deep.equal(['alpha', 'gamma']);
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1, 2, 3]);
});
});
describe('asyncMap', () => {
it('maps empty inputs', async () => {
const visitedIndices: number[] = [];
const mapperFn = async (item: any, index: number) => {
await simulateAsyncOp();
visitedIndices.push(index);
return 'hello';
};
// Raw array.
expect(await asyncMap([], mapperFn)).to.be.empty;
expect(visitedIndices).to.be.empty;
// Array wrapped in promise.
expect(await asyncMap(wrapInPromise([]), mapperFn)).to.be.empty;
expect(visitedIndices).to.be.empty;
});
it('maps non-empty inputs', async () => {
let visitedItems: string[];
let visitedIndices: number[];
const mapperFn = async (item: string, index: number) => {
await simulateAsyncOp();
visitedItems.push(item);
visitedIndices.push(index);
return item.toUpperCase();
};
const input = ['hello', 'world'];
// Raw array.
visitedItems = [];
visitedIndices = [];
expect(await asyncMap(input, mapperFn)).to.deep.equal(['HELLO', 'WORLD']);
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1]);
// Array wrapped in promise.
visitedItems = [];
visitedIndices = [];
expect(await asyncMap(wrapInPromise(input), mapperFn)).to.deep.equal(['HELLO', 'WORLD']);
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1]);
});
});
describe('asyncForEach', () => {
it('does not iterate on empty inputs', async () => {
const visitedIndices: number[] = [];
const callbackFn = async (item: string, index: number) => {
await simulateAsyncOp();
visitedIndices.push(index);
};
// Raw array.
await asyncForEach([], callbackFn);
expect(visitedIndices).to.be.empty;
// Array wrapped in promise.
await asyncForEach(wrapInPromise([]), callbackFn);
expect(visitedIndices).to.be.empty;
});
it('iterates on non-empty inputs', async () => {
let visitedItems: string[];
let visitedIndices: number[];
const callbackFn = async (item: string, index: number) => {
await simulateAsyncOp();
visitedItems.push(item);
visitedIndices.push(index);
};
const input = ['alpha', 'beta', 'gamma'];
// Raw array.
visitedItems = [];
visitedIndices = [];
await asyncForEach(input, callbackFn);
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1, 2]);
// Array wrapped in promise.
visitedItems = [];
visitedIndices = [];
await asyncForEach(wrapInPromise(input), callbackFn);
expect(visitedItems).to.deep.equal(input);
expect(visitedIndices).to.deep.equal([0, 1, 2]);
});
});
});
async function simulateAsyncOp(): Promise<void> {
// Do nothing.
}
function wrapInPromise<T>(value: T): Promise<T> {
return new Promise((resolve) => resolve(value));
}
describe('examples with and without async.ts', () => {
// These examples illustrate situations involving multiple async operations where async.ts can
// make the code simpler.
const stateCapitalsMap = new Map<string, string>([
['Colorado', 'Denver'],
['Georgia', 'Atlanta'],
['Massachussets', 'Boston'],
['North Carolina', 'Raleigh'],
['Texas', 'Austin'],
]);
const cityPopulationsMap = new Map<string, number>([
['Atlanta', 498_715],
['Austin', 950_807],
['Boston', 684_000],
['Denver', 705_576],
['Raleigh', 464_485],
]);
// Simulates an RPC operation.
const getStatesRPC = (): Promise<string[]> => wrapInPromise(Array.from(stateCapitalsMap.keys()));
// Simulates an RPC operation.
const getStateCapitalRPC = (state: string): Promise<string> => (
wrapInPromise(stateCapitalsMap.get(state)!)
);
// Simulates an RPC operation.
const getCityPopulationRPC = (city: string): Promise<number> => (
wrapInPromise(cityPopulationsMap.get(city)!)
);
describe('without async.ts', () => {
it('fetches state capitals', async () => {
const states = await getStatesRPC();
const stateCapitalPromises = states.map(getStateCapitalRPC);
const stateCapitals = await Promise.all(stateCapitalPromises);
expect(stateCapitals).to.have.length(5);
expect(stateCapitals).to.have.members(['Atlanta', 'Austin', 'Boston', 'Denver', 'Raleigh']);
});
it('fetches state capitals with population > 500k', async () => {
const states = await getStatesRPC();
const stateCapitals = await Promise.all(states.map(getStateCapitalRPC));
// We can't use Array.prototype.filter because async predicates return promises, which are
// truthy, and thus nothing gets filtered out.
const failedAttempt = stateCapitals.filter(
async (city) => (await getCityPopulationRPC(city)) > 500_000,
);
expect(failedAttempt).to.have.length(5);
const populousStateCapitals: string[] = [];
await Promise.all(stateCapitals.map(async (city) => {
const population = await getCityPopulationRPC(city);
if (population > 500_000) populousStateCapitals.push(city);
}));
expect(populousStateCapitals).to.have.length(3);
expect(populousStateCapitals).to.have.members(['Austin', 'Boston', 'Denver']);
});
});
describe('with async.ts', () => {
it('fetches state capitals', async () => {
const stateCapitals = await asyncMap(getStatesRPC(), getStateCapitalRPC);
expect(stateCapitals).to.have.length(5);
expect(stateCapitals).to.have.members(['Atlanta', 'Austin', 'Boston', 'Denver', 'Raleigh']);
});
it('fetches state capitals with population > 500k', async () => {
const stateCapitals = asyncMap(getStatesRPC(), getStateCapitalRPC);
const populousStateCapitals = await asyncFilter(
stateCapitals,
async (city) => (await getCityPopulationRPC(city)) > 500_000,
);
expect(populousStateCapitals).to.have.length(3);
expect(populousStateCapitals).to.have.members(['Austin', 'Boston', 'Denver']);
});
});
});