"use client";

// src/nats/nats.ts
function assertClientSide() {
  if (typeof window === "undefined") {
    throw new Error("NATS client can only connect from the browser/runtime with WebSocket support (window is undefined).");
  }
}
async function importNats() {
  return await import("nats.ws");
}
function toNatsHeaders(nats, init) {
  if (!init) return void 0;
  if (typeof init.get === "function") return init;
  const h = nats.headers();
  for (const [k, v] of Object.entries(init)) {
    if (v !== void 0 && v !== null) h.set(k, String(v));
  }
  return h;
}
function cryptoRandom() {
  const buf = new Uint32Array(1);
  crypto.getRandomValues(buf);
  return buf[0] / (4294967295 + 1);
}
function createExponentialBackoffHandler(opts) {
  const initialDelay = opts.initialDelayMs ?? 1e3;
  const maxDelay = opts.maxDelayMs ?? 3e4;
  const multiplier = opts.multiplier ?? 2;
  const jitter = opts.jitter ?? true;
  let attempt = 0;
  return {
    handler: () => {
      const delay = Math.min(initialDelay * multiplier ** attempt, maxDelay);
      attempt++;
      return jitter ? delay * (0.5 + cryptoRandom() * 0.5) : delay;
    },
    reset: () => {
      attempt = 0;
    }
  };
}
function mapOptionsToConnectionOptions(opts, backoff) {
  return {
    servers: opts.servers,
    name: opts.name,
    token: opts.token,
    user: opts.user,
    pass: opts.pass,
    timeout: opts.connectTimeoutMs ?? 15e3,
    reconnect: opts.reconnect ?? true,
    maxReconnectAttempts: opts.maxReconnectAttempts,
    reconnectTimeWait: opts.reconnectTimeWaitMs,
    reconnectDelayHandler: backoff?.handler,
    pingInterval: opts.pingIntervalMs,
    maxPingOut: opts.maxPingOut,
    inboxPrefix: opts.inboxPrefix
  };
}
function mapNatsTypeToStatus(type) {
  const t = String(type).toLowerCase();
  if (t.includes("disconnect")) return "disconnected";
  if (t === "reconnecting") return "reconnecting";
  if (t === "reconnect" || t.includes("connect")) return "connected";
  if (t.includes("error")) return "error";
  if (t.includes("close")) return "closed";
  return null;
}
function createNatsClient(options) {
  let nc = null;
  let statusLoopAbort = null;
  let connectInFlight = null;
  const backoff = options.exponentialBackoff ? createExponentialBackoffHandler(options.exponentialBackoff) : void 0;
  const statusListeners = /* @__PURE__ */ new Set();
  function emitStatus(event) {
    for (const listener of statusListeners) {
      try {
        listener(event);
      } catch {
      }
    }
  }
  async function connect() {
    if (nc && !nc.isClosed()) return;
    if (connectInFlight) return connectInFlight;
    assertClientSide();
    connectInFlight = (async () => {
      try {
        emitStatus({ status: "connecting" });
        const nats = await importNats();
        const conn = await nats.connect(mapOptionsToConnectionOptions(options, backoff));
        nc = conn;
        emitStatus({ status: "connected" });
        statusLoopAbort = new AbortController();
        const signal = statusLoopAbort.signal;
        (async () => {
          try {
            for await (const s of conn.status()) {
              if (signal.aborted) return;
              const mapped = mapNatsTypeToStatus(s?.type);
              if (mapped) {
                if (mapped === "connected" && backoff) {
                  backoff.reset();
                }
                emitStatus({ status: mapped, data: s?.data });
                if (mapped === "closed") {
                  nc = null;
                }
              }
            }
          } catch (e) {
            if (!signal.aborted) {
              emitStatus({ status: "error", data: e });
              if (nc === conn && conn.isClosed()) {
                nc = null;
                emitStatus({ status: "closed" });
              }
            }
          }
        })().catch(() => {
        });
      } finally {
        connectInFlight = null;
      }
    })();
    return connectInFlight;
  }
  async function close() {
    const conn = nc;
    nc = null;
    if (statusLoopAbort) {
      try {
        statusLoopAbort.abort();
      } catch {
      }
      statusLoopAbort = null;
    }
    if (!conn) return;
    try {
      await conn.drain();
    } finally {
      try {
        await conn.close();
      } finally {
        emitStatus({ status: "closed" });
      }
    }
  }
  function requireConnection() {
    if (!nc) throw new Error("NATS is not connected. Call client.connect() first.");
    return nc;
  }
  function isConnected() {
    return Boolean(nc) && !nc.isClosed();
  }
  function publishBytes(subject, payload, opts) {
    const conn = requireConnection();
    (async () => {
      const nats = await importNats();
      conn.publish(subject, payload, { headers: toNatsHeaders(nats, opts?.headers) });
    })().catch((e) => emitStatus({ status: "error", data: e }));
  }
  function publishString(subject, payload, opts) {
    ;
    (async () => {
      const nats = await importNats();
      const sc = nats.StringCodec();
      publishBytes(subject, sc.encode(payload), opts);
    })().catch((e) => emitStatus({ status: "error", data: e }));
  }
  function publishJson(subject, payload, opts) {
    ;
    (async () => {
      const nats = await importNats();
      const jc = nats.JSONCodec();
      publishBytes(subject, jc.encode(payload), opts);
    })().catch((e) => emitStatus({ status: "error", data: e }));
  }
  async function requestBytes(subject, payload, opts) {
    const conn = requireConnection();
    const nats = await importNats();
    const msg = await conn.request(subject, payload, {
      timeout: opts?.timeoutMs ?? 2e3,
      headers: toNatsHeaders(nats, opts?.headers)
    });
    return msg;
  }
  async function requestString(subject, payload, opts) {
    const nats = await importNats();
    const sc = nats.StringCodec();
    const msg = await requestBytes(subject, sc.encode(payload), opts);
    return sc.decode(msg.data);
  }
  async function requestJson(subject, payload, opts) {
    const nats = await importNats();
    const reqCodec = nats.JSONCodec();
    const resCodec = nats.JSONCodec();
    const msg = await requestBytes(subject, reqCodec.encode(payload), opts);
    return resCodec.decode(msg.data);
  }
  function subscribeBytes(subject, onMessage, opts) {
    const conn = requireConnection();
    const sub = conn.subscribe(subject, { queue: opts?.queue });
    if (typeof opts?.max === "number") sub.unsubscribe(opts.max);
    const abortController = new AbortController();
    const signal = opts?.signal ?? abortController.signal;
    (async () => {
      try {
        for await (const msg of sub) {
          if (signal.aborted) break;
          await onMessage(msg);
        }
      } catch (e) {
        emitStatus({ status: "error", data: e });
      } finally {
        try {
          sub.unsubscribe();
        } catch {
        }
      }
    })().catch((e) => emitStatus({ status: "error", data: e }));
    return {
      subscription: sub,
      unsubscribe() {
        try {
          abortController.abort();
        } catch {
        }
        try {
          sub.unsubscribe();
        } catch {
        }
      }
    };
  }
  function subscribeString(subject, onMessage, opts) {
    return subscribeBytes(
      subject,
      async (msg) => {
        const nats = await importNats();
        const sc = nats.StringCodec();
        await onMessage(sc.decode(msg.data), msg);
      },
      opts
    );
  }
  function subscribeJson(subject, onMessage, opts) {
    return subscribeBytes(
      subject,
      async (msg) => {
        const nats = await importNats();
        const jc = nats.JSONCodec();
        await onMessage(jc.decode(msg.data), msg);
      },
      opts
    );
  }
  async function subscribeJetStreamOrdered(onMessage, opts) {
    const conn = requireConnection();
    if (opts.signal?.aborted) {
      return { unsubscribe() {
      } };
    }
    const nats = await importNats();
    if (opts.signal?.aborted) {
      return { unsubscribe() {
      } };
    }
    const inactiveThresholdNs = (opts.inactiveThresholdMs ?? 5 * 6e4) * 1e6;
    const js = conn.jetstream();
    const consumer = await js.consumers.get(opts.streamName, {
      filterSubjects: opts.filterSubject,
      deliver_policy: nats.DeliverPolicy.StartSequence,
      opt_start_seq: opts.optStartSeq ?? 0,
      inactive_threshold: inactiveThresholdNs
    });
    const iterRef = { current: null };
    let closed = false;
    const onAbort = () => {
      void teardown();
    };
    opts.signal?.addEventListener("abort", onAbort, { once: true });
    async function teardown() {
      if (closed) return;
      closed = true;
      opts.signal?.removeEventListener("abort", onAbort);
      const iter = iterRef.current;
      iterRef.current = null;
      if (iter) {
        try {
          await iter.close();
        } catch {
        }
      }
    }
    if (opts.signal?.aborted) {
      void teardown();
      return { unsubscribe() {
      } };
    }
    ;
    (async () => {
      try {
        const iter = await consumer.consume();
        if (closed) {
          try {
            await iter.close();
          } catch {
          }
          return;
        }
        iterRef.current = iter;
        for await (const msg of iter) {
          if (closed) break;
          try {
            await onMessage(msg);
          } catch (e) {
            emitStatus({ status: "error", data: e });
          }
        }
      } catch (e) {
        if (!closed) emitStatus({ status: "error", data: e });
      }
    })().catch((e) => emitStatus({ status: "error", data: e }));
    return {
      unsubscribe() {
        void teardown();
      }
    };
  }
  function onStatus(listener) {
    statusListeners.add(listener);
    return () => statusListeners.delete(listener);
  }
  return {
    connect,
    close,
    isConnected,
    publishBytes,
    publishString,
    publishJson,
    requestBytes,
    requestString,
    requestJson,
    subscribeBytes,
    subscribeString,
    subscribeJson,
    subscribeJetStreamOrdered,
    onStatus
  };
}

// src/nats/shared-connection.ts
var NATS_DEFAULTS = {
  SHARED_CLOSE_DELAY_MS: 3e3,
  CONNECT_TIMEOUT_MS: 1e4,
  PING_INTERVAL_MS: 3e4,
  MAX_PING_OUT: 3,
  RETRY_INITIAL_DELAY_MS: 1e3,
  RETRY_MAX_DELAY_MS: 3e4,
  RETRY_MULTIPLIER: 2
};
var shared = null;
function getSharedConnection() {
  return shared;
}
function acquireClient(url, opts) {
  if (shared?.wsUrl !== url) {
    if (shared) {
      if (shared.closeTimer) clearTimeout(shared.closeTimer);
      const old = shared;
      shared = null;
      void old.client.close().catch(() => {
      });
    }
    const {
      name = "openframe-frontend",
      user = "machine",
      pass = "",
      connectTimeoutMs = NATS_DEFAULTS.CONNECT_TIMEOUT_MS,
      pingIntervalMs = NATS_DEFAULTS.PING_INTERVAL_MS,
      maxPingOut = NATS_DEFAULTS.MAX_PING_OUT
    } = opts ?? {};
    const client = createNatsClient({
      servers: url,
      name,
      user,
      pass,
      connectTimeoutMs,
      reconnect: false,
      pingIntervalMs,
      maxPingOut
    });
    shared = {
      wsUrl: url,
      client,
      connectPromise: null,
      refCount: 0,
      closeTimer: null,
      retryTimer: null,
      retryOwner: null
    };
  }
  shared.refCount += 1;
  if (shared.closeTimer) {
    clearTimeout(shared.closeTimer);
    shared.closeTimer = null;
  }
  return shared;
}
function releaseClient(url, opts) {
  if (!shared || shared.wsUrl !== url) return;
  shared.refCount = Math.max(0, shared.refCount - 1);
  if (shared.refCount > 0) return;
  const delay = opts?.delayMs ?? NATS_DEFAULTS.SHARED_CLOSE_DELAY_MS;
  shared.closeTimer = setTimeout(() => {
    const s = shared;
    shared = null;
    if (s) {
      if (s.retryTimer) {
        clearTimeout(s.retryTimer);
        s.retryTimer = null;
      }
      void s.client.close().catch(() => {
      });
    }
  }, delay);
}
function getSharedConnectionFor(url) {
  if (!url) return null;
  return shared && shared.wsUrl === url ? shared : null;
}
var defaultShouldRetryOn = (status) => status === "closed" || status === "disconnected";
function startConnectionLifecycle(options) {
  const { conn, wsUrl } = options;
  const ownerToken = {};
  if (!conn.retryOwner) conn.retryOwner = ownerToken;
  let closed = false;
  let retryAttempt = 0;
  function emitSynthetic(status) {
    if (closed) return;
    options.onStatusChange?.(status, { status });
    if (status === "connected") {
      retryAttempt = 0;
    }
  }
  function scheduleRetry() {
    if (closed) return;
    if (getSharedConnectionFor(wsUrl) !== conn) return;
    if (!conn.retryOwner) conn.retryOwner = ownerToken;
    if (conn.retryOwner !== ownerToken) return;
    if (conn.retryTimer) {
      clearTimeout(conn.retryTimer);
      conn.retryTimer = null;
    }
    const cfg = options.backoff ?? {};
    const fastRetries = cfg.fastRetries ?? 0;
    const fastDelay = cfg.fastRetryDelayMs ?? NATS_DEFAULTS.RETRY_INITIAL_DELAY_MS;
    const baseDelay = cfg.initialDelayMs ?? NATS_DEFAULTS.RETRY_INITIAL_DELAY_MS;
    const maxDelay = cfg.maxDelayMs ?? NATS_DEFAULTS.RETRY_MAX_DELAY_MS;
    const multiplier = cfg.multiplier ?? NATS_DEFAULTS.RETRY_MULTIPLIER;
    const delay = retryAttempt < fastRetries ? fastDelay : Math.min(baseDelay * multiplier ** (retryAttempt - fastRetries), maxDelay);
    const jitteredDelay = delay * (0.5 + Math.random() * 0.5);
    retryAttempt++;
    conn.retryTimer = setTimeout(async () => {
      conn.retryTimer = null;
      if (closed) return;
      if (getSharedConnectionFor(wsUrl) !== conn) return;
      try {
        await options.onBeforeReconnect?.();
      } catch {
      }
      if (closed) return;
      if (getSharedConnectionFor(wsUrl) !== conn) return;
      const freshUrl = options.getFreshUrl();
      if (freshUrl !== wsUrl) return;
      try {
        conn.connectPromise = null;
        conn.connectPromise = conn.client.connect();
        await conn.connectPromise;
        if (!closed && getSharedConnectionFor(wsUrl) === conn) {
          retryAttempt = 0;
        }
      } catch {
        conn.connectPromise = null;
        if (!closed && getSharedConnectionFor(wsUrl) === conn) {
          scheduleRetry();
        }
      }
    }, jitteredDelay);
  }
  const shouldRetryOn = options.shouldRetryOn ?? defaultShouldRetryOn;
  const unsubStatus = conn.client.onStatus((evt) => {
    if (closed) return;
    options.onStatusChange?.(evt.status, evt);
    if (evt.status === "connected") {
      retryAttempt = 0;
    }
    if (shouldRetryOn(evt.status)) {
      scheduleRetry();
    }
  });
  if (conn.client.isConnected()) {
    emitSynthetic("connected");
  }
  void (async () => {
    try {
      conn.connectPromise || (conn.connectPromise = conn.client.connect());
      await conn.connectPromise;
    } catch {
      conn.connectPromise = null;
      if (closed) return;
      emitSynthetic("disconnected");
      scheduleRetry();
    }
  })();
  return {
    stop() {
      closed = true;
      unsubStatus();
      if (conn.retryTimer) {
        clearTimeout(conn.retryTimer);
        conn.retryTimer = null;
      }
      if (conn.retryOwner === ownerToken) {
        conn.retryOwner = null;
      }
    }
  };
}

// src/nats/nats-provider.tsx
import * as React from "react";
import { jsx } from "react/jsx-runtime";
var NatsContext = React.createContext(null);
function NatsProvider({
  children,
  getWsUrl,
  onBeforeReconnect,
  clientConfig,
  reconnectionBackoff,
  urlRevision
}) {
  const [client, setClient] = React.useState(null);
  const [status, setStatus] = React.useState("closed");
  const [reconnectionCount, setReconnectionCount] = React.useState(0);
  const getWsUrlRef = React.useRef(getWsUrl);
  React.useEffect(() => {
    getWsUrlRef.current = getWsUrl;
  }, [getWsUrl]);
  const onBeforeReconnectRef = React.useRef(onBeforeReconnect);
  React.useEffect(() => {
    onBeforeReconnectRef.current = onBeforeReconnect;
  }, [onBeforeReconnect]);
  const reconnectionBackoffRef = React.useRef(reconnectionBackoff);
  React.useEffect(() => {
    reconnectionBackoffRef.current = reconnectionBackoff;
  }, [reconnectionBackoff]);
  const clientConfigRef = React.useRef(clientConfig);
  React.useEffect(() => {
    clientConfigRef.current = clientConfig;
  }, [clientConfig]);
  const heldUrlRef = React.useRef(null);
  const hadConnectionBeforeRef = React.useRef(false);
  React.useEffect(() => {
    const wsUrl = getWsUrlRef.current();
    if (!wsUrl) {
      if (heldUrlRef.current) {
        releaseClient(heldUrlRef.current);
        heldUrlRef.current = null;
        setClient(null);
        setStatus("closed");
      }
      return;
    }
    if (heldUrlRef.current && heldUrlRef.current !== wsUrl) {
      releaseClient(heldUrlRef.current);
      heldUrlRef.current = null;
    }
    const conn = acquireClient(wsUrl, clientConfigRef.current);
    heldUrlRef.current = wsUrl;
    setClient(conn.client);
    setStatus(conn.client.isConnected() ? "connected" : "connecting");
    const lifecycle = startConnectionLifecycle({
      conn,
      wsUrl,
      onBeforeReconnect: () => onBeforeReconnectRef.current?.(),
      backoff: reconnectionBackoffRef.current,
      getFreshUrl: () => getWsUrlRef.current(),
      onStatusChange: (newStatus) => {
        if (newStatus === "error") return;
        setStatus(newStatus);
        if (newStatus === "connected") {
          if (hadConnectionBeforeRef.current) {
            setReconnectionCount((c) => c + 1);
          }
          hadConnectionBeforeRef.current = true;
        }
      }
    });
    return () => {
      lifecycle.stop();
      if (heldUrlRef.current) {
        releaseClient(heldUrlRef.current);
        heldUrlRef.current = null;
      }
      setClient(null);
      setStatus("closed");
    };
  }, [urlRevision]);
  const value = React.useMemo(
    () => ({
      client,
      status,
      isReady: status === "connected" && client !== null,
      reconnectionCount
    }),
    [client, status, reconnectionCount]
  );
  return /* @__PURE__ */ jsx(NatsContext.Provider, { value, children });
}
function useNats() {
  const ctx = React.useContext(NatsContext);
  if (!ctx) throw new Error("useNats must be used inside <NatsProvider>");
  return ctx;
}
function useOptionalNats() {
  return React.useContext(NatsContext);
}

// src/nats/use-nats-subscription.ts
import * as React2 from "react";
function useNatsSubscription(subject, onMessage, options) {
  const nats = useOptionalNats();
  const handlerRef = React2.useRef(onMessage);
  React2.useEffect(() => {
    handlerRef.current = onMessage;
  }, [onMessage]);
  const [isSubscribed, setIsSubscribed] = React2.useState(false);
  const enabled = options?.enabled !== false;
  const queue = options?.queue;
  const max = options?.max;
  const reconnectionCount = nats?.reconnectionCount ?? 0;
  const isReady = !!nats?.isReady;
  const client = nats?.client ?? null;
  React2.useEffect(() => {
    if (!client || !isReady || !subject || !enabled) {
      setIsSubscribed(false);
      return;
    }
    const sub = client.subscribeBytes(
      subject,
      (msg) => handlerRef.current(msg),
      { queue, max }
    );
    setIsSubscribed(true);
    return () => {
      setIsSubscribed(false);
      try {
        sub.unsubscribe();
      } catch {
      }
    };
  }, [client, isReady, subject, enabled, queue, max, reconnectionCount]);
  return { isSubscribed, isReady };
}
function useNatsJsonSubscription(subject, onPayload, options) {
  const handlerRef = React2.useRef(onPayload);
  React2.useEffect(() => {
    handlerRef.current = onPayload;
  }, [onPayload]);
  const decoderRef = React2.useRef(null);
  if (!decoderRef.current && typeof TextDecoder !== "undefined") {
    decoderRef.current = new TextDecoder();
  }
  const wrapped = React2.useCallback(async (msg) => {
    const decoder = decoderRef.current;
    if (!decoder) return;
    try {
      const parsed = JSON.parse(decoder.decode(msg.data));
      await handlerRef.current(parsed, msg);
    } catch {
    }
  }, []);
  return useNatsSubscription(subject, wrapped, options);
}

export {
  createNatsClient,
  NATS_DEFAULTS,
  getSharedConnection,
  acquireClient,
  releaseClient,
  getSharedConnectionFor,
  startConnectionLifecycle,
  NatsProvider,
  useNats,
  useOptionalNats,
  useNatsSubscription,
  useNatsJsonSubscription
};
//# sourceMappingURL=chunk-PHWQLKVE.js.map