{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-event-source",
  "title": "useEventSource",
  "description": "Subscribe to a Server-Sent Events stream with a four-state connection machine, capped exponential-backoff reconnects, named-event subscriptions and callbacks that never re-open the stream.",
  "files": [
    {
      "path": "src/registry/hooks/use-event-source.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n/**\n * The connection state machine. **Four states only**, and they describe what this hook\n * is doing right now — they are not a mirror of the native `readyState`:\n *\n * - `connecting` — opening a connection, **or** waiting out a backoff before the next\n *   retry. To the UI those are the same thing (\"nothing is flowing yet\"); read\n *   `retryCount` to tell them apart.\n * - `open` — the stream is up, the `open` event arrived.\n * - `closed` — the consumer called `close()`, or `enabled: false`. **Terminal: it never reconnects on its own.**\n * - `error` — where it stops once the retry budget is spent. **Terminal**; only the caller's `reconnect()` gets out.\n */\nexport type EventSourceStatus = \"connecting\" | \"open\" | \"closed\" | \"error\"\n\n/** One SSE event delivered to the consumer. `data` is never parsed — it is handed over verbatim. */\nexport interface SseEvent {\n  /** Event name: `\"message\"` on the default stream, the server's `event:` field for named events. */\n  event: string\n  /** The raw string the `data:` lines produced (the browser joins multi-line data for you). */\n  data: string\n  /** The `id:` this event carries or inherits; an empty string when the server never sent one. */\n  lastEventId: string\n}\n\n/** What `onError` is handed — every fact needed to decide whether to give up. */\nexport interface SseErrorMeta {\n  /**\n   * `true` = the browser has given up on this EventSource (`readyState === CLOSED`): the\n   * response was not a 200 + `text/event-stream`, i.e. a 4xx/5xx, the wrong Content-Type,\n   * a 401/407, a failed CORS check. A native EventSource **does not** retry those, and does\n   * not tell you the status code (the spec never exposes it). `false` = the connection\n   * dropped at the transport level (a network blip, the server closing the stream), which\n   * is the case a native EventSource would have retried by itself.\n   */\n  fatal: boolean\n  /** Which failure this is since the last successful `open` (1-based). */\n  attempt: number\n  /** Whether this hook will retry. `false` means it has landed in the terminal `error` state. */\n  willRetry: boolean\n  /** Milliseconds until the next retry; 0 when `willRetry` is false. */\n  retryIn: number\n}\n\n/**\n * The listener signature is `Event`, the way the DOM writes it; message events are narrowed\n * to `MessageEvent<string>` internally. Only an event map of known names lets DOM types say\n * \"this listener can only ever receive a MessageEvent\", and SSE's named events are runtime\n * strings that live in no map.\n */\nexport type EventSourceListener = (event: Event) => void\n\n/**\n * The slice of EventSource this hook actually uses. It is a structural type so that a\n * stand-in (a fake EventSource in tests, a polyfill that can set request headers) does not\n * have to implement the whole `EventSource` interface. The native `EventSource` and any\n * `EventTarget` subclass satisfy it for free.\n */\nexport interface EventSourceLike {\n  readonly readyState: number\n  close(): void\n  addEventListener(type: string, listener: EventSourceListener): void\n  removeEventListener(type: string, listener: EventSourceListener): void\n}\n\nexport interface UseEventSourceOptions {\n  /**\n   * Off disconnects (status back to `closed`), on connects again. Defaults to `true`.\n   *\n   * This is how you pause properly: an EventSource **does not** pause because the tab went\n   * to the background — the stream keeps arriving, keeps costing bandwidth and battery. To\n   * stop paying for it, wire visibility in: `useEventSource(url, { enabled: isVisible })`\n   * (this library's `use-page-visibility` drops straight in). Note that reconnecting loses\n   * whatever was sent while it was off, unless the server can resume from an id (see\n   * `lastEventIdParam`).\n   */\n  enabled?: boolean\n  /**\n   * Whether cross-origin requests carry cookies / HTTP auth. Defaults to `false`.\n   *\n   * It is a constructor argument, so changing it **does** reconnect. With credentials the\n   * server must answer with a concrete origin in `Access-Control-Allow-Origin` (never `*`)\n   * plus `Access-Control-Allow-Credentials: true`.\n   */\n  withCredentials?: boolean\n  /**\n   * Called for **default** events — the ones with no `event:` field.\n   *\n   * Kept in a latest-ref: synced on every render, **never in an effect's dependency array**.\n   * This is the easiest thing to get wrong in a hook like this — consumers almost always\n   * pass an inline arrow, so depending on it would tear down and reconnect on every render,\n   * and the stream would never actually come up.\n   */\n  onMessage?: (event: SseEvent) => void\n  /**\n   * Subscribe to **named events** (the server's `event: progress`). Keys are event names,\n   * values are the callbacks.\n   *\n   * Same semantics as the native API: a named event fires **only** the callback of that\n   * name, never `onMessage`, and a name nobody subscribed to is not delivered at all. A new\n   * callback identity neither reinstalls listeners nor reconnects; only a change to the\n   * **set of names** adds or removes one (and that does not reconnect either).\n   *\n   * The keys `\"open\"` / `\"error\"` are ignored — those are connection lifecycle, use `onOpen`\n   * / `onError`. A `\"message\"` key coexists with `onMessage`; both get called.\n   */\n  events?: Record<string, (event: SseEvent) => void>\n  /** Called when the connection opens (the native `open` event). Latest-ref as well. */\n  onOpen?: () => void\n  /**\n   * Called once per failure — **exactly once**. A native EventSource fires a burst of\n   * `error`s while it retries on its own; this hook owns the retry, so one failure is one\n   * callback.\n   *\n   * Calling `close()` in here cancels the retry that was just scheduled — that is how you\n   * say \"it's fatal, stop trying\".\n   */\n  onError?: (error: Error, meta: SseErrorMeta) => void\n  /**\n   * How many consecutive retries before giving up, default `5`. `0` = the first failure is\n   * terminal `error`; `Infinity` = never give up. One successful `open` resets it.\n   *\n   * The ceiling is one of the main reasons this hook exists: a native EventSource\n   * **retries forever, on an interval you cannot set**, so an offline user sits on\n   * \"connecting\" indefinitely and every client stampedes a restarting server at once. At the\n   * ceiling this stops at `error` and hands \"keep going?\" back to the caller.\n   */\n  maxRetries?: number\n  /** How long to wait before the first retry (ms), default `1000`. It grows by `backoffFactor` after that. */\n  retryDelay?: number\n  /** Ceiling for the backoff delay (ms), default `30000`. Clamped to `setTimeout`'s 32-bit limit. */\n  maxRetryDelay?: number\n  /** Backoff multiplier, default `2` (1s → 2s → 4s → 8s …). Below 1 is clamped to 1, or the waits would shrink. */\n  backoffFactor?: number\n  /**\n   * Which query parameter carries the last event id when this hook reconnects. Defaults to\n   * `\"lastEventId\"`; pass `null` to switch it off.\n   *\n   * **Why it is needed**: the browser only sends the `Last-Event-ID` header when **it**\n   * reconnects. To own the interval and the count, this hook `close()`s the native object on\n   * error and builds a new one — and a fresh EventSource has an empty last event ID, so that\n   * header never goes out. `EventSource` cannot set request headers either, so the only way\n   * left is to put the id in the URL. A server that reads the parameter resumes from it; one\n   * that does not know it ignores it and starts from the beginning, exactly as if it had\n   * never been sent — which is why leaving it on by default is safe. Set it to `null` when\n   * the URL is signed (the query is part of an HMAC), and accept that a reconnect starts over.\n   */\n  lastEventIdParam?: string | null\n  /**\n   * How many recent events to keep in `history`, default `0` — **nothing accumulates**.\n   *\n   * Off by default on purpose: a streaming response can be tens of thousands of events, and\n   * piling them into state makes every message re-render an ever-growing array. If you want\n   * all of them, collect them in `onMessage` yourself (a reducer, or a ref flushed on a\n   * timer). A positive value keeps the last N, and whatever is pushed out counts into\n   * `droppedCount` — the truncation is stated, never silent.\n   */\n  historyLimit?: number\n  /**\n   * Build the connection object yourself. Defaults to `(url, init) => new EventSource(url, init)`.\n   *\n   * Two real uses: ① inject a stand-in in tests and drive the whole state machine with no\n   * server; ② swap in an implementation that can set request headers (a native EventSource\n   * cannot send `Authorization`, so the usual answer is a fetch-based SSE client). Latest-ref,\n   * so changing the implementation does not reconnect.\n   */\n  createEventSource?: (url: string, init: { withCredentials: boolean }) => EventSourceLike\n}\n\nexport interface UseEventSourceResult {\n  status: EventSourceStatus\n  /** The most recent **delivered** event (default stream + subscribed named events), or `null`. */\n  lastEvent: SseEvent | null\n  /** The last `id:` the server sent; cleared when the URL changes. This is what resumes a stream. */\n  lastEventId: string\n  /** The most recent connection failure; cleared on a successful `open`. Its `message` says fatal or dropped. */\n  error: Error | null\n  /** Retries since the last successful `open`, capped at `maxRetries`. */\n  retryCount: number\n  /** The last N events when `historyLimit > 0`; an empty array at the default 0. */\n  history: SseEvent[]\n  /** How many events `historyLimit` pushed out of `history` — the truncation is stated. */\n  droppedCount: number\n  /** Reconnect now: resets the retry count and leaves the terminal `closed` / `error` states. Stable identity. */\n  reconnect: () => void\n  /** Disconnect and stop at `closed`, with no further automatic retries. Stable identity. */\n  close: () => void\n}\n\n/**\n * `EventSource.CLOSED`. It cannot be written as `EventSource.CLOSED` — this module has to\n * evaluate in environments with no global EventSource (SSR, an injected stand-in).\n */\nconst READY_STATE_CLOSED = 2\n\n/** `setTimeout`'s 32-bit ceiling; past it a delay overflows into \"fire immediately\". */\nconst MAX_TIMEOUT = 0x7fffffff\n\n/** Separator for serialising the set of event names. Escaped, so no literal control character sits in the source. */\nconst NAME_SEP = \"\\u0000\"\n\nconst EMPTY_HISTORY: SseEvent[] = []\n\nconst RESERVED_EVENT_NAMES = new Set([\"open\", \"error\"])\n\nfunction clampRetries(value: number | undefined): number {\n  if (typeof value !== \"number\" || Number.isNaN(value)) return 5\n  if (value === Infinity) return Infinity\n  return Math.max(0, Math.floor(value))\n}\n\n/** `Infinity` / `NaN` / negatives all fall back to the default: `setTimeout(Infinity)` fires at once, not never. */\nfunction clampDelay(value: number | undefined, fallback: number): number {\n  if (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) return fallback\n  return Math.min(value, MAX_TIMEOUT)\n}\n\nfunction clampFactor(value: number | undefined): number {\n  if (typeof value !== \"number\" || !Number.isFinite(value)) return 2\n  return Math.max(1, value)\n}\n\nfunction clampHistory(value: number | undefined): number {\n  if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) return 0\n  return Math.floor(value)\n}\n\nfunction backoffDelay(attempt: number, base: number, factor: number, ceiling: number): number {\n  const raw = base * Math.pow(factor, attempt - 1)\n  return Math.min(Number.isFinite(raw) ? raw : ceiling, ceiling)\n}\n\n/** Hang the last event id on the URL as a query parameter; relative paths work too. */\nfunction withLastEventId(url: string, param: string | null, id: string): string {\n  if (!param || !id) return url\n  try {\n    const parsed = new URL(url, typeof window === \"undefined\" ? undefined : window.location.href)\n    parsed.searchParams.set(param, id)\n    return parsed.toString()\n  } catch {\n    // The URL itself does not parse (a deliberately bad value, say) — hand it to\n    // EventSource unchanged and let it fail by its own rules, rather than deciding here.\n    return url\n  }\n}\n\nfunction toError(cause: unknown): Error {\n  if (cause instanceof Error) return cause\n  return new Error(typeof cause === \"string\" ? cause : \"EventSource could not be created.\")\n}\n\nconst defaultCreateEventSource = (\n  url: string,\n  init: { withCredentials: boolean },\n): EventSourceLike => new EventSource(url, init)\n\ninterface InternalState {\n  status: EventSourceStatus\n  error: Error | null\n  retryCount: number\n  lastEvent: SseEvent | null\n  lastEventId: string\n  history: SseEvent[]\n  droppedCount: number\n}\n\n/**\n * Subscribe to one Server-Sent Events stream: streaming AI output, live notifications and\n * background-job progress all ride on this.\n *\n * What it adds on top of a native `EventSource` is exactly the part you cannot read out of\n * the docs and only meet in production:\n *\n * - **A new URL reconnects; a new callback does not.** The connection effect depends on\n *   `url` / `enabled` / `withCredentials` only; `onMessage` / `events` / `onError` /\n *   `createEventSource` all go through latest-refs. Depending on the callbacks is the\n *   classic bug in this kind of hook: the consumer's inline arrow is a new reference every\n *   render, so every render tears the connection down and the stream never comes up.\n * - **This hook owns the retry: the interval is yours, the count is bounded.** A native\n *   EventSource retries forever on an interval you cannot change after a transport drop,\n *   and conversely does **not retry at all** when the response is a 4xx/5xx — without\n *   telling you the status code. Here the first error `close()`s the native object and takes\n *   over: exponential backoff, capped at `maxRetries`, then a stop in the terminal `error`\n *   state that hands the decision back to the caller. `meta.fatal` on `onError` separates\n *   the two kinds of failure.\n * - **Owning the retry means owning `Last-Event-ID`.** The browser only sends that header\n *   when it reconnects itself; the object this hook builds cannot, and EventSource cannot\n *   set headers, so the id travels in a query parameter instead (`lastEventIdParam`,\n *   `lastEventId` by default, switchable off).\n * - **Only the newest event is kept by default.** A streaming response can be tens of\n *   thousands of events, and putting them all in state means every message re-renders an\n *   ever-growing array. To accumulate, turn on `historyLimit` (which states what it\n *   truncated) or collect them yourself in `onMessage`.\n * - **Unmount always kills.** Each effect instance only closes the connection it opened, so\n *   after StrictMode's mount → cleanup → mount the live one is the new one, and an unmount\n *   leaves nothing behind.\n *\n * One thing you have to wire yourself: **an EventSource does not pause when the tab goes to\n * the background** — the stream keeps arriving and keeps costing bandwidth. Feed visibility\n * into `enabled` to stop paying for it. Also, HTTP/1.1 allows six connections per origin, so\n * do not open a pile of streams on one page.\n */\nexport function useEventSource(\n  url: string,\n  options: UseEventSourceOptions = {},\n): UseEventSourceResult {\n  const { enabled = true, withCredentials = false } = options\n\n  const [state, setState] = React.useState<InternalState>(() => ({\n    status: enabled ? \"connecting\" : \"closed\",\n    error: null,\n    retryCount: 0,\n    lastEvent: null,\n    lastEventId: \"\",\n    history: EMPTY_HISTORY,\n    droppedCount: 0,\n  }))\n\n  // Resetting state when the inputs change is done by adjusting state during render, not by\n  // a setState in an effect (that would show one frame of the old state, and would be caught\n  // by react-hooks/set-state-in-effect).\n  const [tracked, setTracked] = React.useState({ url, enabled, withCredentials })\n  if (\n    tracked.url !== url ||\n    tracked.enabled !== enabled ||\n    tracked.withCredentials !== withCredentials\n  ) {\n    const urlChanged = tracked.url !== url\n    setTracked({ url, enabled, withCredentials })\n    setState(prev => ({\n      status: enabled ? \"connecting\" : \"closed\",\n      error: null,\n      retryCount: 0,\n      // A new URL is a new stream: the previous events and resume id no longer hold. Merely\n      // toggling enabled or swapping withCredentials keeps them, so reconnecting can resume.\n      lastEvent: urlChanged ? null : prev.lastEvent,\n      lastEventId: urlChanged ? \"\" : prev.lastEventId,\n      history: urlChanged ? EMPTY_HISTORY : prev.history,\n      droppedCount: urlChanged ? 0 : prev.droppedCount,\n    }))\n  }\n\n  const onMessageRef = React.useRef(options.onMessage)\n  const onOpenRef = React.useRef(options.onOpen)\n  const onErrorRef = React.useRef(options.onError)\n  const eventsRef = React.useRef(options.events)\n  const createRef = React.useRef(options.createEventSource)\n  const configRef = React.useRef({\n    maxRetries: clampRetries(options.maxRetries),\n    baseDelay: clampDelay(options.retryDelay, 1000),\n    ceilDelay: Math.max(\n      clampDelay(options.retryDelay, 1000),\n      clampDelay(options.maxRetryDelay, 30000),\n    ),\n    factor: clampFactor(options.backoffFactor),\n    historyLimit: clampHistory(options.historyLimit),\n    lastEventIdParam:\n      options.lastEventIdParam === undefined ? \"lastEventId\" : options.lastEventIdParam,\n  })\n\n  // Must sit before the connection effect: effects run in declaration order, so the\n  // connection reads this render's latest callbacks and config as it is set up.\n  React.useEffect(() => {\n    onMessageRef.current = options.onMessage\n    onOpenRef.current = options.onOpen\n    onErrorRef.current = options.onError\n    eventsRef.current = options.events\n    createRef.current = options.createEventSource\n    const baseDelay = clampDelay(options.retryDelay, 1000)\n    configRef.current = {\n      maxRetries: clampRetries(options.maxRetries),\n      baseDelay,\n      // The ceiling must not fall below the base delay, or the backoff backs off into nothing.\n      ceilDelay: Math.max(baseDelay, clampDelay(options.maxRetryDelay, 30000)),\n      factor: clampFactor(options.backoffFactor),\n      historyLimit: clampHistory(options.historyLimit),\n      lastEventIdParam:\n        options.lastEventIdParam === undefined ? \"lastEventId\" : options.lastEventIdParam,\n    }\n  })\n\n  const esRef = React.useRef<EventSourceLike | null>(null)\n  const activeUrlRef = React.useRef(url)\n  /** The resume id is stored with the URL it belongs to, so a new URL never inherits the old stream's id. */\n  const lastEventIdRef = React.useRef<{ url: string; id: string }>({ url, id: \"\" })\n  const namedListenersRef = React.useRef(new Map<string, EventSourceListener>())\n  const controlRef = React.useRef<{ close: () => void; reconnect: () => void } | null>(null)\n\n  /** Deliver one event: update state first, then call back. Both paths (default stream / named event) are identical. */\n  const deliver = React.useCallback((name: string, event: MessageEvent<string>) => {\n    const record: SseEvent = {\n      event: name,\n      data: typeof event.data === \"string\" ? event.data : String(event.data ?? \"\"),\n      lastEventId: event.lastEventId ?? \"\",\n    }\n    if (record.lastEventId) {\n      lastEventIdRef.current = { url: activeUrlRef.current, id: record.lastEventId }\n    }\n    // Read the config outside the updater: an updater has to be pure (StrictMode calls it twice).\n    const limit = configRef.current.historyLimit\n    setState(prev => {\n      let history = prev.history\n      let droppedCount = prev.droppedCount\n      if (limit > 0) {\n        const next = [...prev.history, record]\n        const overflow = Math.max(0, next.length - limit)\n        history = overflow > 0 ? next.slice(overflow) : next\n        droppedCount = prev.droppedCount + overflow\n      } else if (prev.history.length > 0) {\n        // historyLimit turned back to 0 at runtime: the window closes and what was already\n        // collected goes with it, rather than leaving a stale list that never updates again.\n        history = EMPTY_HISTORY\n      }\n      return {\n        ...prev,\n        lastEvent: record,\n        lastEventId: record.lastEventId || prev.lastEventId,\n        history,\n        droppedCount,\n      }\n    })\n    if (name === \"message\") onMessageRef.current?.(record)\n    eventsRef.current?.[name]?.(record)\n  }, [])\n\n  /**\n   * Align the named listeners installed on the live connection with the current set of event\n   * names. **Adds and removes incrementally, never rebuilds the connection** — subscribing to\n   * one more `progress` mid-flight must not cut the stream and start it over.\n   */\n  const syncNamedListeners = React.useCallback(\n    (names: string[]) => {\n      const es = esRef.current\n      const installed = namedListenersRef.current\n      if (!es) {\n        installed.clear()\n        return\n      }\n      for (const [name, listener] of installed) {\n        if (!names.includes(name)) {\n          es.removeEventListener(name, listener)\n          installed.delete(name)\n        }\n      }\n      for (const name of names) {\n        // \"message\" already has a listener (a second one would deliver twice); \"open\" /\n        // \"error\" are connection lifecycle, not data.\n        if (name === \"message\" || RESERVED_EVENT_NAMES.has(name) || installed.has(name)) continue\n        const listener: EventSourceListener = event => deliver(name, event as MessageEvent<string>)\n        es.addEventListener(name, listener)\n        installed.set(name, listener)\n      }\n    },\n    [deliver],\n  )\n\n  // Fingerprint of the set of event names. Callback identities change every render, the set\n  // of names does not — depend on this and \"new callbacks\" does nothing, while \"one more\n  // subscription\" installs exactly one more listener.\n  const namedKey = Object.keys(options.events ?? {})\n    .sort()\n    .join(NAME_SEP)\n\n  React.useEffect(() => {\n    syncNamedListeners(namedKey ? namedKey.split(NAME_SEP) : [])\n  }, [namedKey, syncNamedListeners])\n\n  React.useEffect(() => {\n    if (!enabled) return\n\n    activeUrlRef.current = url\n\n    let disposed = false\n    /** Set once the consumer calls `close()`: blocks the scheduled retry, and stops a late error scheduling another. */\n    let stopped = false\n    let attempt = 0\n    let timer: ReturnType<typeof setTimeout> | null = null\n\n    const clearTimer = () => {\n      if (timer !== null) {\n        clearTimeout(timer)\n        timer = null\n      }\n    }\n\n    const teardown = () => {\n      const es = esRef.current\n      if (!es) return\n      es.removeEventListener(\"open\", handleOpen)\n      es.removeEventListener(\"message\", handleMessage)\n      es.removeEventListener(\"error\", handleError)\n      for (const [name, listener] of namedListenersRef.current) {\n        es.removeEventListener(name, listener)\n      }\n      namedListenersRef.current.clear()\n      es.close()\n      esRef.current = null\n    }\n\n    function handleOpen() {\n      if (disposed || stopped) return\n      attempt = 0\n      setState(prev =>\n        prev.status === \"open\" && prev.error === null && prev.retryCount === 0\n          ? prev\n          : { ...prev, status: \"open\", error: null, retryCount: 0 },\n      )\n      onOpenRef.current?.()\n    }\n\n    function handleMessage(event: Event) {\n      if (disposed || stopped) return\n      deliver(\"message\", event as MessageEvent<string>)\n    }\n\n    function handleError() {\n      if (disposed || stopped) return\n      // fatal has to be read before close(): close() sets readyState to CLOSED itself.\n      const fatal = (esRef.current?.readyState ?? READY_STATE_CLOSED) === READY_STATE_CLOSED\n      // The step that takes the retry over: after a transport error the native object retries\n      // forever on its own schedule, so close it first and the schedule becomes ours.\n      teardown()\n\n      attempt += 1\n      const config = configRef.current\n      const willRetry = attempt <= config.maxRetries\n      const retryIn = willRetry\n        ? backoffDelay(attempt, config.baseDelay, config.factor, config.ceilDelay)\n        : 0\n      const error = new Error(\n        `useEventSource(\"${url}\"): ${\n          fatal\n            ? \"the browser gave up on this stream (readyState CLOSED) — the response was not a 200 \" +\n              \"text/event-stream. A 4xx/5xx status, a wrong Content-Type, a 401/407 or a failed CORS \" +\n              \"check all land here; EventSource does not expose the status code.\"\n            : \"the connection dropped (network error, or the server closed the stream).\"\n        } ${\n          willRetry\n            ? `Reconnecting (attempt ${attempt} of ${config.maxRetries}) in ${retryIn}ms.`\n            : `Giving up after ${config.maxRetries} ${\n                config.maxRetries === 1 ? \"retry\" : \"retries\"\n              }; call reconnect() to try again.`\n        }`,\n      )\n\n      setState(prev => ({\n        ...prev,\n        status: willRetry ? \"connecting\" : \"error\",\n        error,\n        retryCount: Math.min(attempt, config.maxRetries),\n      }))\n\n      if (willRetry) {\n        timer = setTimeout(() => {\n          timer = null\n          if (!disposed && !stopped) connect()\n        }, retryIn)\n      }\n\n      // The callback goes last so the consumer can close() here to cancel the retry just\n      // scheduled — \"saw a fatal, stop trying\".\n      onErrorRef.current?.(error, { fatal, attempt, willRetry, retryIn })\n    }\n\n    function connect() {\n      if (disposed || stopped) return\n      // Exactly one live connection, at all times.\n      teardown()\n\n      const carried = lastEventIdRef.current\n      const target = withLastEventId(\n        url,\n        configRef.current.lastEventIdParam,\n        carried.url === url ? carried.id : \"\",\n      )\n\n      let es: EventSourceLike\n      try {\n        es = (createRef.current ?? defaultCreateEventSource)(target, { withCredentials })\n      } catch (cause) {\n        // Throwing from the constructor means an invalid URL, or an environment with no\n        // EventSource at all. Retrying that is pointless: go terminal and hand the cause on\n        // unchanged.\n        const error = new Error(\n          `useEventSource(\"${url}\"): could not create the connection. Either the URL is invalid or ` +\n            \"this environment has no EventSource (pass createEventSource to supply one).\",\n          { cause: toError(cause) },\n        )\n        setState(prev => ({ ...prev, status: \"error\", error }))\n        onErrorRef.current?.(error, {\n          fatal: true,\n          attempt: attempt + 1,\n          willRetry: false,\n          retryIn: 0,\n        })\n        return\n      }\n\n      esRef.current = es\n      es.addEventListener(\"open\", handleOpen)\n      es.addEventListener(\"message\", handleMessage)\n      es.addEventListener(\"error\", handleError)\n      syncNamedListeners(Object.keys(eventsRef.current ?? {}))\n    }\n\n    const handle = {\n      close: () => {\n        stopped = true\n        clearTimer()\n        teardown()\n        setState(prev => (prev.status === \"closed\" ? prev : { ...prev, status: \"closed\" }))\n      },\n      reconnect: () => {\n        stopped = false\n        attempt = 0\n        clearTimer()\n        teardown()\n        setState(prev => ({ ...prev, status: \"connecting\", error: null, retryCount: 0 }))\n        connect()\n      },\n    }\n    controlRef.current = handle\n\n    // The first connect goes in a microtask: ① the effect body then holds no synchronous\n    // setState (only the construction-failure path is allowed one); ② under StrictMode's\n    // mount → cleanup → mount the first effect instance is already disposed, so the double\n    // invocation really opens **one** connection.\n    queueMicrotask(() => {\n      if (!disposed && !stopped) connect()\n    })\n\n    return () => {\n      disposed = true\n      clearTimer()\n      teardown()\n      if (controlRef.current === handle) controlRef.current = null\n    }\n  }, [url, enabled, withCredentials, deliver, syncNamedListeners])\n\n  // With enabled false (or the component unmounted) there is no live connection to act on, so\n  // both are silent no-ops — they mean \"do something to the current connection\", and having\n  // none is not an error worth throwing over.\n  const close = React.useCallback(() => {\n    controlRef.current?.close()\n  }, [])\n\n  const reconnect = React.useCallback(() => {\n    controlRef.current?.reconnect()\n  }, [])\n\n  return {\n    status: state.status,\n    lastEvent: state.lastEvent,\n    lastEventId: state.lastEventId,\n    error: state.error,\n    retryCount: state.retryCount,\n    history: state.history,\n    droppedCount: state.droppedCount,\n    reconnect,\n    close,\n  }\n}\n\nexport default useEventSource\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}