import { resolver } from "@rocicorp/resolver";
import { assert } from "../../../../../shared/src/asserts.js";
import { Subscription } from "../../../types/subscription.js";
class ChangeStreamMultiplexer {
  #lc;
  #sub;
  #producers = [];
  #listeners = [];
  /**
   * The `#lastWatermark` tracks the watermark of the last transaction
   * committed to stream. It is set to null when a producer has reserved
   * the stream, and set to the new watermark when the producer releases
   * the reservation.
   */
  #lastWatermark;
  /**
   * Tracks the queue of producers waiting for a reservation.
   */
  #waiters = [];
  constructor(lc, lastWatermark) {
    this.#lc = lc;
    this.#sub = Subscription.create({
      cleanup: () => this.#producers.forEach((p) => p.cancel())
    });
    this.#lastWatermark = lastWatermark;
  }
  addProducers(...p) {
    this.#producers.push(...p);
    return this;
  }
  addListeners(...l) {
    this.#listeners.push(...l);
    return this;
  }
  /**
   * Called by a producer to "reserve" the exclusive right to push data
   * changes to the stream. If the stream is currently reserved,
   * the resulting Promise must be awaited.
   *
   * The producer must then {@link release()} the reservation when it sees
   * fit.
   *
   * @param producer The name of the producer, purely for debugging output
   */
  reserve(producer) {
    if (this.#lastWatermark !== null) {
      const lastWatermark = this.#lastWatermark;
      this.#lastWatermark = null;
      return lastWatermark;
    }
    const startTime = performance.now();
    const { promise, resolve: grantReservation } = resolver();
    this.#waiters.push({ producer, startTime, grantReservation });
    return promise;
  }
  /**
   * If there are producers currently awaiting a reservation, returns
   * the duration (in milliseconds) of the oldest reservation request.
   * Returns a negative number if there are no waiters.
   */
  waiterDelay() {
    if (this.#waiters.length === 0) {
      return -1;
    }
    return performance.now() - this.#waiters[0].startTime;
  }
  /**
   * Called by a producer to release its reservation after committing its
   * last transaction.
   */
  release(newWatermark) {
    const waiter = this.#waiters.shift();
    if (!waiter) {
      this.#lastWatermark = newWatermark;
    } else {
      const { producer, startTime, grantReservation } = waiter;
      grantReservation(newWatermark);
      const elapsed = performance.now() - startTime;
      this.#lc.info?.(
        `${producer} waited ${elapsed.toFixed(3)} ms for stream reservation`
      );
    }
  }
  /**
   * `pushStatus()` can be called without a reservation, as it
   * does not constitute a data change and can appear anywhere
   * in the stream.
   */
  pushStatus(message) {
    this.#listeners.forEach((l) => l.onChange(message));
    if (message[1].ack) {
      this.#sub.push(message);
    }
  }
  /**
   * `push()` must only be called by a producer after it has
   * {@link reserve}d the stream.
   */
  push(message) {
    assert(
      this.#lastWatermark === null,
      `push() called without reserve()-ing the stream`
    );
    this.#listeners.forEach((l) => l.onChange(message));
    return this.#sub.push(message).result;
  }
  fail(err) {
    this.#sub.fail(err);
  }
  asSource() {
    return this.#sub;
  }
}
export {
  ChangeStreamMultiplexer
};
//# sourceMappingURL=change-stream-multiplexer.js.map
