// Force strict mode and setup for ESM
"use strict";
import {
  init_esbuild_shims
} from "./chunk-A4BMJM77.js";
import {
  __name
} from "./chunk-J2S4EL5Y.js";

// packages/acp-bridge/src/eventBus.ts
init_esbuild_shims();
var EVENT_SCHEMA_VERSION = 1;
var DEFAULT_MAX_QUEUED = 256;
var DEFAULT_RING_SIZE = 8e3;
var WARN_THRESHOLD_RATIO = 0.75;
var WARN_RESET_RATIO = 0.375;
var DEFAULT_MAX_SUBSCRIBERS = 64;
function getServerTimestamp(meta) {
  const existing = meta?.["serverTimestamp"];
  return typeof existing === "number" && Number.isFinite(existing) ? existing : Date.now();
}
__name(getServerTimestamp, "getServerTimestamp");
var SubscriberLimitExceededError = class extends Error {
  static {
    __name(this, "SubscriberLimitExceededError");
  }
  limit;
  constructor(limit) {
    super(`EventBus subscriber limit reached (${limit})`);
    this.name = "SubscriberLimitExceededError";
    this.limit = limit;
  }
};
var EventBus = class {
  constructor(ringSize = DEFAULT_RING_SIZE, maxSubscribers = DEFAULT_MAX_SUBSCRIBERS, compactionEngine) {
    this.ringSize = ringSize;
    this.maxSubscribers = maxSubscribers;
    this.compactionEngine = compactionEngine;
  }
  static {
    __name(this, "EventBus");
  }
  nextId = 1;
  ring = [];
  subs = /* @__PURE__ */ new Set();
  closed = false;
  snapshotReplay() {
    return this.compactionEngine?.snapshot();
  }
  /** Most recent id ever assigned by `publish`. 0 if no events published. */
  get lastEventId() {
    return this.nextId - 1;
  }
  /** Snapshot of the live subscriber count. */
  get subscriberCount() {
    return this.subs.size;
  }
  /**
   * Publish an event to the bus. Returns the constructed `BridgeEvent`
   * (with `id` + `v` assigned) on success, or `undefined` when the
   * bus is closed.
   *
   * **Never throws** (never-throws contract). Closing the bus mid-publish
   * is the only abnormal path and is handled as a return-undefined
   * no-op; subscriber-enqueue failures are caught internally and
   * translated to per-subscriber eviction. Call sites can rely on
   * this — the historical `try { publish(...) } catch {}` blocks in
   * `httpAcpBridge.ts` are defense-in-depth, not load-bearing, and
   * may be removed in a future cleanup pass without changing
   * behavior. Don't add new try/catch wrappers around `publish()`.
   */
  publish(input) {
    if (this.closed) return void 0;
    const existingMeta = input._meta;
    const event = {
      id: this.nextId++,
      v: EVENT_SCHEMA_VERSION,
      ...input,
      _meta: {
        ...existingMeta ?? {},
        serverTimestamp: getServerTimestamp(existingMeta)
      }
    };
    this.ring.push(event);
    try {
      this.compactionEngine?.ingest(event);
    } catch {
    }
    if (this.ring.length > this.ringSize) this.ring.shift();
    for (const sub of Array.from(this.subs)) {
      if (sub.evicted) continue;
      if (!sub.queue.push(event)) {
        sub.evicted = true;
        const evictionFrame = {
          v: EVENT_SCHEMA_VERSION,
          type: "client_evicted",
          data: { reason: "queue_overflow", droppedAfter: event.id }
        };
        sub.queue.forcePush(evictionFrame);
        sub.queue.close();
        sub.dispose();
        continue;
      }
      const liveSize = sub.queue.size;
      if (!sub.warned && liveSize >= sub.warnThreshold) {
        sub.warned = true;
        const warningFrame = {
          v: EVENT_SCHEMA_VERSION,
          type: "slow_client_warning",
          data: {
            queueSize: liveSize,
            maxQueued: sub.maxQueued,
            // `event.id` is always defined here — the just-published
            // `event` is constructed at the top of `publish()` with
            // `id: this.nextId++`. No `??` fallback needed.
            lastEventId: event.id
          }
        };
        sub.queue.forcePush(warningFrame);
      } else if (sub.warned && liveSize <= sub.warnResetThreshold) {
        sub.warned = false;
      }
    }
    return event;
  }
  /**
   * Note: registration is synchronous — by the time `subscribe()` returns,
   * the subscriber is already attached and will receive any subsequent
   * `publish()` even if the consumer hasn't started iterating yet. (A
   * generator-style implementation would defer registration to the first
   * `next()` call, which races with publishes that happen before the
   * consumer's first await.)
   *
   * The returned iterator is NOT safe to drive from concurrent callers —
   * two simultaneous `.next()` calls would race for the same event from
   * the underlying queue. Daemon usage is sequential (`for await ... of`
   * inside the SSE route), so this is safe in production. Callers that
   * fan an iterator out to multiple consumers must serialize themselves.
   */
  subscribe(opts = {}) {
    if (this.closed) {
      return emptyAsyncIterable();
    }
    if (this.subs.size >= this.maxSubscribers) {
      throw new SubscriberLimitExceededError(this.maxSubscribers);
    }
    const maxQueued = opts.maxQueued ?? DEFAULT_MAX_QUEUED;
    const queue = new BoundedAsyncQueue(maxQueued);
    const sub = {
      queue,
      evicted: false,
      maxQueued,
      warnThreshold: WARN_THRESHOLD_RATIO * maxQueued,
      warnResetThreshold: WARN_RESET_RATIO * maxQueued,
      warned: false,
      dispose: /* @__PURE__ */ __name(() => {
      }, "dispose")
    };
    this.subs.add(sub);
    if (opts.lastEventId !== void 0) {
      const epochReset = opts.lastEventId >= this.nextId;
      if (epochReset) {
        queue.forcePush({
          v: EVENT_SCHEMA_VERSION,
          type: "state_resync_required",
          data: {
            reason: "epoch_reset",
            lastDeliveredId: opts.lastEventId,
            // Ring is typically empty right after a restart; fall back to
            // `nextId` (the first id this epoch will assign) so the field
            // stays meaningful ("fresh sequence starts here").
            earliestAvailableId: this.ring[0]?.id ?? this.nextId
          }
        });
      } else {
        const earliestInRing = this.ring[0]?.id;
        if (earliestInRing !== void 0 && earliestInRing > opts.lastEventId + 1) {
          queue.forcePush({
            v: EVENT_SCHEMA_VERSION,
            type: "state_resync_required",
            data: {
              reason: "ring_evicted",
              lastDeliveredId: opts.lastEventId,
              earliestAvailableId: earliestInRing
            }
          });
        }
      }
      const replayFrom = epochReset ? 0 : opts.lastEventId;
      let replayedCount = 0;
      let lastReplayedId;
      for (const e of this.ring) {
        if (e.id !== void 0 && e.id > replayFrom) {
          queue.forcePush(e);
          replayedCount += 1;
          lastReplayedId = e.id;
        }
      }
      queue.forcePush({
        v: EVENT_SCHEMA_VERSION,
        type: "replay_complete",
        data: {
          // Note: `lastReplayedEventId`
          // is the canonical wire name — the old `lastEventId` collided
          // semantically with the SSE protocol's `Last-Event-ID` (envelope
          // `id`) in raw daemon traces. Emit both: `lastReplayedEventId`
          // for current SDKs and `lastEventId` as a deprecated alias so
          // pre-rename consumers keep working (additive, non-breaking).
          ...lastReplayedId !== void 0 ? {
            lastReplayedEventId: lastReplayedId,
            lastEventId: lastReplayedId
          } : {},
          replayedCount
        }
      });
    }
    let disposed = false;
    const dispose = /* @__PURE__ */ __name(() => {
      if (disposed) return;
      disposed = true;
      this.subs.delete(sub);
      opts.signal?.removeEventListener("abort", onAbort);
    }, "dispose");
    sub.dispose = dispose;
    const onAbort = /* @__PURE__ */ __name(() => {
      queue.close({ drain: false });
      dispose();
    }, "onAbort");
    if (opts.signal) {
      if (opts.signal.aborted) {
        onAbort();
      } else {
        opts.signal.addEventListener("abort", onAbort, { once: true });
      }
    }
    return {
      [Symbol.asyncIterator]: () => ({
        async next() {
          const r = await queue.next();
          if (r.done) dispose();
          return r;
        },
        async return() {
          queue.close();
          dispose();
          return { value: void 0, done: true };
        }
      })
    };
  }
  /** Close all live subscribers and prevent further `publish`/`subscribe`. */
  close() {
    if (this.closed) return;
    this.closed = true;
    for (const sub of this.subs) sub.queue.close();
    this.subs.clear();
    this.compactionEngine?.close();
  }
};
function emptyAsyncIterable() {
  return {
    [Symbol.asyncIterator]: () => ({
      async next() {
        return { value: void 0, done: true };
      }
    })
  };
}
__name(emptyAsyncIterable, "emptyAsyncIterable");
var BoundedAsyncQueue = class {
  constructor(maxSize) {
    this.maxSize = maxSize;
  }
  static {
    __name(this, "BoundedAsyncQueue");
  }
  buf = [];
  resolvers = [];
  closed = false;
  /**
   * O(1) snapshot of how many LIVE (non-forced) entries are in `buf`.
   * Maintained directly by `push()`/`next()`: any time a forced entry
   * is added or removed `liveCount` is untouched; any time a live entry
   * is added or removed `liveCount` moves with it. Replaces the
   * position-dependent `forcedInBuf` heuristic — `liveCount` is correct
   * no matter where in the queue the forced entries are.
   */
  liveCount = 0;
  /**
   * Number of LIVE (non-force-pushed) items currently waiting in the
   * buffer. Backpressure decisions in `EventBus.publish()` (the
   * `slow_client_warning` threshold) read this value.
   */
  get size() {
    return this.liveCount;
  }
  /** Returns true if accepted, false if dropped due to overflow. */
  push(value) {
    if (this.closed) return false;
    const r = this.resolvers.shift();
    if (r) {
      r({ value, done: false });
      return true;
    }
    if (this.liveCount >= this.maxSize) return false;
    this.buf.push({ value, forced: false });
    this.liveCount += 1;
    return true;
  }
  /** Bypasses the size cap. Used for replay frames, eviction terminal,
   * and slow-client warnings. */
  forcePush(value) {
    if (this.closed) return;
    const r = this.resolvers.shift();
    if (r) {
      r({ value, done: false });
      return;
    }
    this.buf.push({ value, forced: true });
  }
  /**
   * Mark the queue closed. By default `next()` continues to drain
   * any items already in `buf` before returning `done: true` —
   * that's what the eviction path relies on (the synthetic
   * `client_evicted` frame is force-pushed THEN close is called,
   * and we want the consumer to see the terminal frame before the
   * iterator unwinds).
   *
   * Pass `{ drain: false }` to drop buffered items immediately
   * (the AbortSignal-driven unsubscribe path uses this — the
   * subscribe docstring says abort should close the iterator
   * promptly, but draining hundreds of queued events first
   * contradicts that and adds post-abort work to the SSE route).
   */
  close(opts = {}) {
    if (this.closed) return;
    this.closed = true;
    if (opts.drain === false) {
      this.buf.length = 0;
      this.liveCount = 0;
    }
    while (this.resolvers.length > 0) {
      this.resolvers.shift()({
        value: void 0,
        done: true
      });
    }
  }
  next() {
    if (this.buf.length > 0) {
      const entry = this.buf.shift();
      if (!entry.forced) this.liveCount -= 1;
      return Promise.resolve({ value: entry.value, done: false });
    }
    if (this.closed) {
      return Promise.resolve({
        value: void 0,
        done: true
      });
    }
    return new Promise((resolve) => this.resolvers.push(resolve));
  }
};

export {
  EVENT_SCHEMA_VERSION,
  DEFAULT_RING_SIZE,
  SubscriberLimitExceededError,
  EventBus
};
/**
 * @license
 * Copyright 2025 Qwen Team
 * SPDX-License-Identifier: Apache-2.0
 */
