export class ExecutionQueueOverflowError extends Error {
  constructor(message = "Execution queue is full") {
    super(message);
    this.name = "ExecutionQueueOverflowError";
  }
}

type Task<T> = () => Promise<T>;

interface PendingTask {
  task: () => Promise<unknown>;
  resolve: (value: unknown) => void;
  reject: (reason?: unknown) => void;
}

export class ExecutionQueue {
  private running = 0;
  private readonly queue: PendingTask[] = [];

  constructor(
    private readonly concurrency: number,
    private readonly queueLimit: number
  ) {}

  run<T>(task: Task<T>): Promise<T> {
    if (this.running < this.concurrency) {
      return this.execute(task);
    }

    const limit = this.queueLimit;
    if (Number.isFinite(limit) && limit >= 0 && this.queue.length >= limit) {
      throw new ExecutionQueueOverflowError();
    }

    return new Promise<T>((resolve, reject) => {
      this.queue.push({
        task: () => task(),
        resolve: (value) => resolve(value as T),
        reject,
      });
    });
  }

  private async execute<T>(task: Task<T>): Promise<T> {
    this.running += 1;
    try {
      return await task();
    } finally {
      this.running -= 1;
      this.processQueue();
    }
  }

  private processQueue(): void {
    while (this.running < this.concurrency && this.queue.length > 0) {
      const next = this.queue.shift();
      if (!next) {
        return;
      }

      this.execute(next.task).then(next.resolve).catch(next.reject);
    }
  }
}
