"use strict"; // License: MIT const RUNNING = Symbol(); const LIMIT = Symbol(); const ITEMS = Symbol(); function nothing() { /* ignored */ } type Wrapped = (...args: any[]) => Promise; interface Item { readonly ctx: any; readonly fn: Function; readonly args: any[]; readonly resolve: Function; readonly reject: Function; } function scheduleDirect(ctx: any, fn: Function, ...args: any[]): Promise { try { const p = Promise.resolve(fn.call(ctx, ...args)); this[RUNNING]++; p.finally(this._next).catch(nothing); return p; } catch (ex) { return Promise.reject(ex); } } function scheduleForLater( head: boolean, ctx: any, fn: Function, ...args: any[]): Promise { const rv = new Promise((resolve, reject) => { const item = { ctx, fn, args, resolve, reject }; this[ITEMS][head ? "unshift" : "push"](item); }); return rv; } function scheduleInternal( head: boolean, ctx: any, fn: Function, ...args: any[]): Promise { if (this[RUNNING] < this.limit) { return scheduleDirect.call(this, ctx, fn, ...args); } return scheduleForLater.call(this, head, ctx, fn, ...args); } export class PromiseSerializer { private [LIMIT]: number; private [RUNNING]: number; private [ITEMS]: Item[]; private readonly _next: () => void; constructor(limit: number) { this[LIMIT] = Math.max(limit || 5, 1); this[ITEMS] = []; this[RUNNING] = 0; this._next = this.next.bind(this); Object.seal(this); } get limit() { return this[LIMIT]; } get running() { return this[RUNNING]; } get scheduled() { return this[ITEMS].length; } get total() { return this.scheduled + this.running; } static wrapNew(limit: number, ctx: any, fn: Function): Wrapped { return new PromiseSerializer(limit).wrap(ctx, fn); } wrap(ctx: any, fn: Function): Wrapped { const rv = this.scheduleWithContext.bind(this, ctx, fn); Object.defineProperty(rv, "prepend", { value: this.prependWithContext.bind(this, ctx, fn) }); return rv; } schedule(fn: Function, ...args: any[]): Promise { return this.scheduleWithContext(null, fn, ...args); } scheduleWithContext(ctx: any, fn: Function, ...args: any[]): Promise { return scheduleInternal.call(this, false, ctx, fn, ...args); } prepend(fn: Function, ...args: any[]): Promise { return this.prependWithContext(null, fn, ...args); } prependWithContext(ctx: any, fn: Function, ...args: any[]): Promise { return scheduleInternal.call(this, true, ctx, fn, ...args); } next() { this[RUNNING]--; const item = this[ITEMS].shift(); if (!item) { return; } try { const p = Promise.resolve(item.fn.call(item.ctx, ...item.args)); this[RUNNING]++; item.resolve(p); p.finally(this._next).catch(nothing); } catch (ex) { try { item.reject(ex); } finally { this.next(); } } } }