/** * Use as a Decorator on async functions, it will limit how many times the function can be * in-flight at a time. * * Calls beyond the limit will be queued, and run when one of the active calls finishes */ export function throttle(limit: number, throttleKey: (args: any[]) => string) { return (target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise>) => { const originalMethod: () => Promise = descriptor.value; const allThrottles = new Map void)[]>>(); const getThrottles = (obj: any) => { let throttles = allThrottles.get(obj); if (throttles != null) { return throttles; } throttles = new Map void)[]>(); allThrottles.set(obj, throttles); return throttles; }; return { value: function(...args: any[]) { const throttles = getThrottles(this); const argsThrottleKey = throttleKey(args); let queue = throttles.get(argsThrottleKey); if (queue == null) { queue = []; throttles.set(argsThrottleKey, queue); } return new Promise((resolve, reject) => { const exec = () => { const onFinally = () => { queue.splice(queue.indexOf(exec), 1); if (queue.length >= limit) { queue[limit - 1](); } else if (queue.length === 0) { throttles.delete(argsThrottleKey); if (throttles.size === 0) { allThrottles.delete(this); } } }; originalMethod.apply(this, args).then((val: any) => { onFinally(); return val; }).catch((err: any) => { onFinally(); throw err; }).then(resolve, reject); }; queue.push(exec); if (queue.length <= limit) { exec(); } }); }, }; }; }