"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.tryInSequence = exports.withThrottling = void 0;
exports.pLimitFn = pLimitFn;
exports.pOne = pOne;
exports.pOnce = pOnce;
const p_limit_1 = __importDefault(require("p-limit"));
const p_throttle_1 = __importDefault(require("p-throttle"));
/**
 * Returns a function that executes with limited concurrency.
 */
function pLimitFn(fn, limit = 1) {
    const queue = (0, p_limit_1.default)(limit);
    return Object.assign((...args) => queue(() => fn(...args)), {
        clear() {
            queue.clearQueue();
        }
    });
}
/**
 * Only allows one outstanding call.
 * Returns same promise while task is executing.
 */
function pOne(fn) {
    const once = pOnce(fn);
    return async (...args) => {
        try {
            return await once(...args);
        }
        finally {
            once.reset();
        }
    };
}
/**
 * Only allows calling `fn` once.
 * Returns same promise while task is executing.
 */
function pOnce(fn) {
    let currentCall = { status: 'init' };
    return Object.assign(async function pOnceWrap(...args) {
        // capture currentCall so can assign to it, even after reset
        const thisCall = currentCall;
        if (thisCall.status === 'pending') {
            return thisCall.promise;
        }
        if (thisCall.status === 'fulfilled') {
            return thisCall.value;
        }
        if (thisCall.status === 'rejected') {
            throw thisCall.reason;
        }
        // status === 'init'
        currentCall = thisCall;
        const promise = (async () => {
            // capture value/error
            try {
                const value = await fn(...args);
                Object.assign(thisCall, {
                    promise: undefined, // release promise
                    status: 'fulfilled',
                    value,
                });
                return value;
            }
            catch (reason) {
                Object.assign(thisCall, {
                    promise: undefined, // release promise
                    status: 'rejected',
                    reason,
                });
                throw reason;
            }
        })();
        promise.catch(() => { }); // prevent unhandled
        Object.assign(thisCall, {
            status: 'pending',
            promise,
        });
        return promise;
    }, {
        isStarted() {
            return currentCall.status !== 'init';
        },
        reset() {
            currentCall = { status: 'init' };
        }
    });
}
// TODO better type annotations
const withThrottling = (fn, maxInvocationsPerSecond) => {
    const throttler = (0, p_throttle_1.default)({
        limit: maxInvocationsPerSecond,
        interval: 1000
    });
    return throttler(fn);
};
exports.withThrottling = withThrottling;
const tryInSequence = async (fns) => {
    if (fns.length === 0) {
        throw new Error('no tasks');
    }
    let firstError;
    for (const fn of fns) {
        try {
            const promise = fn();
            return await promise;
        }
        catch (e) {
            if (firstError === undefined) {
                firstError = e;
            }
        }
    }
    throw firstError;
};
exports.tryInSequence = tryInSequence;
//# sourceMappingURL=promises.js.map