I recently refactored the vanilla ES6 solution I've been using for the couple of years into TypeScript, and it's been working great.
- Promise objects are instantiated as needed
- Results are yielded on state change
- Lean on native
PromiseSettledResult
typings
- Looks a bit like a Array.prototype.reduce() in usage ♂️
throttle.ts
export enum PromiseState {
Pending = 'pending',
Fulfilled = 'fulfilled',
Rejected = 'rejected',
}
function getPromiseState( promise: Promise<any> ): Promise<PromiseState> {
const control = Symbol();
return Promise
.race([ promise, control ])
.then( value => ( value === control ) ? PromiseState.Pending : PromiseState.Fulfilled )
.catch( () => PromiseState.Rejected );
}
export function isFulfilled<T>( promise: PromiseSettledResult<T> ): promise is PromiseFulfilledResult<T> {
return promise.status === "fulfilled";
}
export function isRejected<T>( promise: PromiseSettledResult<T> ): promise is PromiseRejectedResult {
return promise.status === "rejected";
}
export async function* throttle<InputType, OutputType>( reservoir: InputType[], promiseFn: ( args: InputType ) => Promise<OutputType>, concurrencyLimit: number ): AsyncGenerator<PromiseSettledResult<OutputType>[], void, PromiseSettledResult<OutputType>[] | undefined> {
let iterable = reservoir.splice( 0, concurrencyLimit ).map( args => promiseFn( args ) );
while ( iterable.length > 0 ) {
await Promise.race( iterable );
const pending: Promise<OutputType>[] = [];
const resolved: Promise<OutputType>[] = [];
for ( const currentValue of iterable ) {
if ( await getPromiseState( currentValue ) === PromiseState.Pending ) {
pending.push( currentValue );
} else {
resolved.push( currentValue );
}
}
iterable = [
...pending,
...reservoir.splice( 0, concurrencyLimit - pending.length ).map( args => promiseFn( args ) )
];
yield Promise.allSettled( resolved );
}
}
Usage example:
app.ts
import { throttle, isFulfilled, isRejected } from './throttle';
async function timeout( delay: number ): Promise<string> {
return new Promise( resolve => {
setTimeout( () => resolve( `timeout promise with ${ delay } delay resolved` ), delay );
} );
}
const inputArray: number[] = [ 1200, 1500, 1400, 1300, 1000, 1100, 1200, 1500, 1400, 1300, 1000, 1100 ];
( async () => {
const timeoutPromises = await throttle<number, string>( inputArray, async item => {
const result = await timeout( item );
return `${ result } and ready for for..await..of`;
}, 5 );
const messages: string[] = [];
for await ( const chunk of timeoutPromises ) {
console.log( chunk.filter( isFulfilled ).map( ({ value }) => value ) );
console.error( chunk.filter( isRejected ).map( ({ reason }) => reason ) );
}
})();