class Scheduler {
    constructor(config) {
        this.reservoirTimer = null;
        this.running = 0;
        this.lastStartTime = 0;
        this.queue = [];
        this.stopped = false;
        this.maxConcurrent = config.maxConcurrent;
        this.minTime = config.minTime;
        this.reservoirEnabled = config.reservoir != null;
        this.reservoir = config.reservoir ?? Infinity;
        this.reservoirRefreshAmount = config.reservoirRefreshAmount ?? 0;
        this.reservoirRefreshInterval = config.reservoirRefreshInterval ?? 0;
        if (this.reservoirEnabled && this.reservoirRefreshInterval > 0) {
            this.reservoirTimer = setInterval(() => {
                this.reservoir = this.reservoirRefreshAmount;
                this.drain();
            }, this.reservoirRefreshInterval);
        }
    }
    schedule(fn) {
        if (this.stopped)
            return Promise.reject(new Error('Scheduler stopped'));
        return new Promise((resolve, reject) => {
            this.queue.push(async () => {
                try {
                    resolve(await fn());
                }
                catch (err) {
                    reject(err);
                }
            });
            this.drain();
        });
    }
    async stop(opts = {}) {
        this.stopped = true;
        if (opts.dropWaitingJobs !== false)
            this.queue.length = 0;
        if (this.reservoirTimer !== null)
            clearInterval(this.reservoirTimer);
        this.reservoirTimer = null;
    }
    drain() {
        if (this.stopped || this.queue.length === 0)
            return;
        if (this.running >= this.maxConcurrent)
            return;
        if (this.reservoirEnabled && this.reservoir <= 0)
            return;
        const elapsed = Date.now() - this.lastStartTime;
        if (elapsed < this.minTime) {
            setTimeout(() => this.drain(), this.minTime - elapsed);
            return;
        }
        const task = this.queue.shift();
        if (!task)
            return;
        if (this.reservoirEnabled)
            this.reservoir -= 1;
        this.running += 1;
        this.lastStartTime = Date.now();
        Promise.resolve()
            .then(task)
            .finally(() => {
            this.running -= 1;
            this.drain();
        });
        if (this.running < this.maxConcurrent)
            this.drain();
    }
}
export class RateLimiter {
    constructor(config = {}) {
        this.config = {
            maxConcurrentRequests: config.maxConcurrentRequests ?? 1,
            maxBatchSize: config.maxBatchSize ?? 15,
            minTimeMsBetweenRequests: config.minTimeMsBetweenRequests ?? config.requestDelayMs ?? 1000,
            reservoir: config.reservoir,
            reservoirRefreshInterval: config.reservoirRefreshInterval,
            reservoirRefreshAmount: config.reservoirRefreshAmount,
        };
        this.limiter = new Scheduler({
            maxConcurrent: this.config.maxConcurrentRequests,
            minTime: this.config.minTimeMsBetweenRequests,
            reservoir: this.config.reservoir,
            reservoirRefreshInterval: this.config.reservoirRefreshInterval,
            reservoirRefreshAmount: this.config.reservoirRefreshAmount,
        });
    }
    async execute(requests, batchRpcCall) {
        if (requests.length === 0)
            return [];
        if (!this.config.maxBatchSize || this.config.maxBatchSize <= 0) {
            throw new Error('Batch size must be greater than 0');
        }
        const indexedRequests = requests.map((req, idx) => ({ originalIndex: idx, ref: req }));
        const methodGroups = new Map();
        for (const ir of indexedRequests) {
            const group = methodGroups.get(ir.ref.method);
            if (group)
                group.push(ir);
            else
                methodGroups.set(ir.ref.method, [ir]);
        }
        const results = new Array(requests.length).fill(null);
        for (const group of methodGroups.values()) {
            for (let i = 0; i < group.length; i += this.config.maxBatchSize) {
                const batch = group.slice(i, i + this.config.maxBatchSize);
                const plainRequests = batch.map((ir) => ir.ref);
                const batchResults = await this.limiter.schedule(() => batchRpcCall(plainRequests));
                if (!Array.isArray(batchResults))
                    continue;
                for (let j = 0; j < batch.length; j += 1) {
                    const ir = batch[j];
                    results[ir.originalIndex] = j < batchResults.length ? batchResults[j] : null;
                }
            }
        }
        return results;
    }
    async stop() {
        await this.limiter.stop({ dropWaitingJobs: true });
    }
    getConfig() {
        return this.config;
    }
}
