import { resolver } from "@rocicorp/resolver";
import { assert } from "../../../shared/src/asserts.js";
class Subscription {
  /**
   * Convenience factory method for creating a {@link Subscription} with internal message type
   * `M` as a subtype of `T`, defaulting to the same type. The default `publish` method publishes
   * the message of type `M` directly to the AsyncIterable.
   */
  static create(options = {}, publish = (m) => m) {
    return new Subscription(options, publish);
  }
  // Consumers waiting to consume messages (i.e. an async iteration awaiting the next message).
  #consumers = [];
  // Messages waiting to be consumed.
  #messages = [];
  #pipelineEnabled;
  // Sentinel value signaling that the subscription is "done" and no more
  // messages can be added.
  #sentinel = void 0;
  #coalesce;
  #consumed;
  #cleanup;
  #publish;
  /**
   * @param publish function for converting the internally pushed / coalesced message
   *        of type `M` to the external type `T` exposed via async iteration.
   */
  constructor(options = {}, publish) {
    const {
      coalesce,
      consumed = () => {
      },
      cleanup = () => {
      },
      pipeline = coalesce === void 0
    } = options;
    this.#coalesce = !coalesce ? void 0 : (curr, prev) => {
      try {
        return coalesce(curr.value, prev.value);
      } finally {
        prev.resolve("coalesced");
      }
    };
    this.#consumed = (entry) => {
      consumed(entry.value);
      entry.resolve("consumed");
    };
    this.#cleanup = (entries, err) => {
      cleanup(
        entries.map((e) => e.value),
        err
      );
      entries.forEach((e) => e.resolve("unconsumed"));
    };
    this.#publish = publish;
    this.#pipelineEnabled = pipeline;
  }
  /**
   * Pushes the next message to be consumed, and returns a `result` that resolves to the
   * eventual {@link Result} of the `value`.
   *
   * If there is an existing unconsumed message and the Subscription has a
   * {@link Options#coalesce coalesce} function, the specified `value` will be coalesced
   * with the pending message. In this case, the result of the pending message
   * is resolved to `coalesced`, regardless of the `coalesce` function implementation.
   *
   * If the subscription is in a terminal state, the message is dropped and the
   * result resolves to `unconsumed`.
   */
  push(value) {
    const { promise: result, resolve } = resolver();
    const entry = { value, resolve };
    if (this.#sentinel) {
      entry.resolve("unconsumed");
      return { result };
    }
    const consumer = this.#consumers.shift();
    if (consumer) {
      consumer.resolve(entry);
    } else if (this.#coalesce && this.#messages.length && this.#messages[this.#messages.length - 1] !== "terminus") {
      const prev = this.#messages[this.#messages.length - 1];
      assert(prev !== "terminus", "prev should not be terminus after check");
      this.#messages[this.#messages.length - 1] = {
        value: this.#coalesce(entry, prev),
        resolve
      };
    } else {
      this.#messages.push(entry);
    }
    return { result };
  }
  /** False if the subscription has been canceled or has failed. */
  get active() {
    return this.#sentinel === void 0;
  }
  /** The number messages waiting to be consumed. */
  get queued() {
    return this.#messages.length;
  }
  /**
   * Cancels the subscription after any queued messages are consumed. This is
   * meant for the producer-side code.
   *
   * Any messages pushed after calling `end()` will be unconsumed as if
   * `cancel()` were called (once the first set of pending messages is
   * consumed). In particular, if a coalesce function is defined, the new
   * messages will not be coalesced with the messages enqueued before `end()`
   * was called. However, to effect the intent of memory efficiency, multiple
   * messages pushed after calling `end()` will be coalesced together.
   *
   */
  end() {
    if (this.#sentinel) ;
    else if (this.#messages.length === 0) {
      this.cancel();
    } else {
      this.#messages.push("terminus");
    }
  }
  /**
   * Cancels the subscription immediately, cleans up, and terminates any iteration.
   * This is intended for the consumer to call when it is no longer interested
   * in the subscription.
   *
   * @param err If an `err` is specified, an iteration over the Subscription /
   *        Sink will throw the `err` (equivalent to the producer calling
   *        {@link fail()}). If undefined, the iteration will exit gracefully.
   */
  cancel(err) {
    this.#terminate(err ?? "canceled");
  }
  /** Fails the subscription, cleans up, and throws from any iteration. */
  fail(err) {
    this.#terminate(err);
  }
  #terminate(sentinel) {
    if (!this.#sentinel) {
      this.#sentinel = sentinel;
      this.#cleanup(
        this.#messages.filter((m) => m !== "terminus"),
        sentinel instanceof Error ? sentinel : void 0
      );
      this.#messages.splice(0);
      for (let consumer = this.#consumers.shift(); consumer; consumer = this.#consumers.shift()) {
        sentinel === "canceled" ? consumer.resolve(null) : consumer.reject(sentinel);
      }
    }
  }
  get pipeline() {
    return this.#pipelineEnabled ? { [Symbol.asyncIterator]: () => this.#pipeline() } : void 0;
  }
  #pipeline() {
    return {
      next: async () => {
        const entry = this.#messages.shift();
        if (entry === "terminus") {
          this.cancel();
          return { value: void 0, done: true };
        }
        if (entry !== void 0) {
          return {
            value: {
              value: this.#publish(entry.value),
              consumed: () => this.#consumed(entry)
            }
          };
        }
        if (this.#sentinel === "canceled") {
          return { value: void 0, done: true };
        }
        if (this.#sentinel) {
          return Promise.reject(this.#sentinel);
        }
        const consumer = resolver();
        this.#consumers.push(consumer);
        const result = await consumer.promise;
        return result ? {
          value: {
            value: this.#publish(result.value),
            consumed: () => this.#consumed(result)
          }
        } : { value: void 0, done: true };
      },
      return: (value) => {
        this.cancel();
        return Promise.resolve({ value, done: true });
      }
    };
  }
  [Symbol.asyncIterator]() {
    const delegate = this.#pipeline();
    let prevConsumed = () => {
    };
    return {
      next: async () => {
        prevConsumed();
        const entry = await delegate.next();
        if (entry.done) {
          return entry;
        }
        const { value, consumed } = entry.value;
        prevConsumed = consumed;
        return { value };
      },
      return: (value) => {
        prevConsumed();
        this.cancel();
        return Promise.resolve({ value, done: true });
      }
    };
  }
}
export {
  Subscription
};
//# sourceMappingURL=subscription.js.map
