import { resolver } from "@rocicorp/resolver";
import { AbortError } from "../../shared/src/abort-error.js";
import { assert } from "../../shared/src/asserts.js";
import { sleep } from "../../shared/src/sleep.js";
import { requestIdle } from "./request-idle.js";
class ProcessScheduler {
  #process;
  #idleTimeoutMs;
  #throttleMs;
  #abortSignal;
  #requestIdle;
  #scheduledResolver = void 0;
  #runResolver = void 0;
  #runPromise = Promise.resolve();
  // oxlint-disable-next-line no-unused-private-class-members -- False positive, this is used in #run
  #throttlePromise = Promise.resolve();
  /**
   * Supports scheduling a `process` to be run with certain constraints.
   *  - Process runs are never concurrent.
   *  - Multiple calls to schedule will be fulfilled by a single process
   *    run started after the call to schedule.  A call is never fulfilled by an
   *    already running process run.  This can be thought of as debouncing.
   *  - Process runs are throttled so that the process runs at most once every
   *    `throttleMs`.
   *  - Process runs try to run during an idle period, but will delay at most
   *    `idleTimeoutMs`.
   *  - Scheduled runs which have not completed when `abortSignal` is aborted
   *    will reject with an `AbortError`.
   */
  constructor(process, idleTimeoutMs, throttleMs, abortSignal, requestIdle$1 = requestIdle) {
    this.#process = process;
    this.#idleTimeoutMs = idleTimeoutMs;
    this.#throttleMs = throttleMs;
    this.#abortSignal = abortSignal;
    this.#requestIdle = requestIdle$1;
    this.#abortSignal.addEventListener(
      "abort",
      () => {
        const abortError = new AbortError("Aborted");
        this.#runResolver?.reject(abortError);
        this.#scheduledResolver?.reject(abortError);
        this.#runResolver = void 0;
        this.#scheduledResolver = void 0;
      },
      { once: true }
    );
  }
  /**
   * Schedules the process to run.
   *
   * The returned promise resolves when the process has completed running.
   * If the process throws an error, the returned promise rejects with that error.
   *
   * If `schedule()` is called multiple times while a process is running/scheduled,
   * they will be debounced into a single run.
   */
  schedule() {
    if (this.#abortSignal.aborted) {
      return Promise.reject(new AbortError("Aborted"));
    }
    if (this.#scheduledResolver) {
      return this.#scheduledResolver.promise;
    }
    this.#scheduledResolver = resolver();
    void this.#scheduleInternal();
    return this.#scheduledResolver.promise;
  }
  /**
   * Runs the process immediately, skipping throttle and idle checks.
   *
   * The returned promise resolves when the process has completed running.
   * If the process throws an error, the returned promise rejects with that error.
   *
   * If there is a scheduled run pending (waiting for idle or throttle), this run
   * will effectively "take over" that scheduled run, and the promise returned
   * by `schedule()` will resolve when this run completes.
   *
   * If there is a process currently running, this run will wait for it to finish
   * before starting, satisfying the non-concurrency constraint.
   */
  async run() {
    if (this.#abortSignal.aborted) {
      return Promise.reject(new AbortError("Aborted"));
    }
    const resolverToResolve = this.#scheduledResolver;
    this.#scheduledResolver = void 0;
    const prevRunPromise = this.#runPromise;
    const runTask = async () => {
      try {
        await prevRunPromise;
      } catch {
      }
      if (this.#abortSignal.aborted) {
        throw new AbortError("Aborted");
      }
      return this.#process();
    };
    const executionPromise = runTask();
    this.#runPromise = executionPromise;
    this.#throttlePromise = throttle(this.#throttleMs, this.#abortSignal);
    try {
      await executionPromise;
      resolverToResolve?.resolve();
    } catch (e) {
      resolverToResolve?.reject(e);
      throw e;
    }
  }
  async #scheduleInternal() {
    try {
      await this.#runPromise;
    } catch {
    }
    await this.#throttlePromise;
    if (!this.#scheduledResolver) {
      return;
    }
    await this.#requestIdle(this.#idleTimeoutMs);
    if (!this.#scheduledResolver) {
      return;
    }
    this.#throttlePromise = throttle(this.#throttleMs, this.#abortSignal);
    this.#runResolver = this.#scheduledResolver;
    this.#scheduledResolver = void 0;
    try {
      this.#runPromise = this.#process();
      await this.#runPromise;
      this.#runResolver?.resolve();
    } catch (e) {
      this.#runResolver?.reject(e);
    }
    this.#runResolver = void 0;
  }
}
async function throttle(timeMs, abortSignal) {
  try {
    await sleep(timeMs, abortSignal);
  } catch (e) {
    assert(e instanceof AbortError);
  }
}
export {
  ProcessScheduler
};
//# sourceMappingURL=process-scheduler.js.map
