{"version":3,"file":"stream.cjs","names":["isNetworkError","reconnectDelayMs"],"sources":["../../src/utils/stream.ts"],"sourcesContent":["import { isNetworkError } from \"./error.js\";\nimport {\n  DEFAULT_MAX_RECONNECT_ATTEMPTS,\n  reconnectDelayMs,\n} from \"./reconnect.js\";\n\n// in this case don't quite match.\ntype IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;\n\n/**\n * Options for streaming with automatic retry logic.\n */\nexport interface StreamWithRetryOptions {\n  /**\n   * Maximum number of reconnection attempts. Default is\n   * {@link DEFAULT_MAX_RECONNECT_ATTEMPTS}.\n   */\n  maxRetries?: number;\n\n  /**\n   * AbortSignal to cancel the stream.\n   */\n  signal?: AbortSignal;\n\n  /**\n   * Callback invoked when a reconnection attempt is made.\n   */\n  onReconnect?: (options: {\n    attempt: number;\n    lastEventId?: string;\n    cause: unknown;\n  }) => void;\n}\n\n/**\n * Parameters for making a stream request\n */\nexport interface StreamRequestParams {\n  /**\n   * Last event ID to resume from, if available\n   */\n  lastEventId?: string;\n\n  /**\n   * Optional reconnection path from the Location header\n   */\n  reconnectPath?: string;\n}\n\n/**\n * Error thrown when maximum reconnection attempts are exceeded.\n */\nexport class MaxReconnectAttemptsError extends Error {\n  constructor(maxAttempts: number, cause: unknown) {\n    super(`Exceeded maximum SSE reconnection attempts (${maxAttempts})`);\n    this.name = \"MaxReconnectAttemptsError\";\n    this.cause = cause;\n  }\n}\n\n/**\n * Error injected into the stream by {@link idleReconnectStream} when no lines\n * arrive within the active idle window. Surfacing this during the read is what\n * lets the reconnect loops in `streamWithRetry` and the protocol SSE transport\n * recover from a half-open socket — one that was silently dropped (e.g. a hard\n * pod kill on a platform revision rollover) without a TCP FIN/RST, so neither\n * a `done` nor a thrown network error ever arrives.\n */\nexport class StreamIdleTimeoutError extends Error {\n  readonly idleTimeoutMs: number;\n\n  constructor(idleTimeoutMs: number) {\n    super(\n      `No SSE bytes received for ${idleTimeoutMs}ms; assuming the connection is half-open and reconnecting.`\n    );\n    this.name = \"StreamIdleTimeoutError\";\n    this.idleTimeoutMs = idleTimeoutMs;\n  }\n}\n\n/** `\":\"` — first byte of an SSE comment / keep-alive line. */\nconst SSE_COMMENT_BYTE = 0x3a;\n\n/**\n * How {@link idleReconnectStream} decides when the connection is idle.\n *\n * - A `number` is a fixed idle window in milliseconds: the watchdog arms\n *   immediately (even before the first byte) and trips after that long with no\n *   activity. Use when you know your server's behaviour and want guaranteed\n *   coverage from t=0; it does not depend on heartbeats.\n * - `\"auto\"` is heartbeat-adaptive: the watchdog stays dormant until it has\n *   observed the server's SSE keep-alive comments (e.g. LangGraph Platform's\n *   `: heartbeat` every ~5s), then arms with a window derived from the\n *   observed cadence. On a server that never sends heartbeats it never arms,\n *   so it can't false-fire during a legitimately quiet period.\n */\nexport type IdleReconnectMode = number | \"auto\";\n\n/**\n * Default idle-reconnect policy for SSE streams. Heartbeat-adaptive\n * `\"auto\"` stays dormant unless the server emits keep-alive heartbeats.\n * Pass `0` to disable.\n */\nexport const DEFAULT_IDLE_RECONNECT: IdleReconnectMode = \"auto\";\n\nexport interface IdleReconnectStreamOptions {\n  /** Fixed timeout (ms) or `\"auto\"` heartbeat-adaptive. */\n  mode: IdleReconnectMode;\n  /** `\"auto\"`: multiplier applied to the observed heartbeat interval. Default 3. */\n  timeoutFactor?: number;\n  /** `\"auto\"`: lower clamp for the derived timeout (ms). Default 6000. */\n  minTimeoutMs?: number;\n  /** `\"auto\"`: upper clamp for the derived timeout (ms). Default 30000. */\n  maxTimeoutMs?: number;\n  /** Fired immediately before the stream is errored, for logging/metrics. */\n  onIdle?: (info: { timeoutMs: number; source: \"fixed\" | \"heartbeat\" }) => void;\n}\n\n/**\n * A pass-through {@link TransformStream} that errors the stream when it goes\n * idle, so the surrounding reconnect logic can recover a half-open socket.\n *\n * MUST sit on the *line* stream — i.e. after\n * {@link import(\"./sse.js\").BytesLineDecoder} but before\n * {@link import(\"./sse.js\").SSEDecoder} (which discards `:` comment lines).\n * Operating at the line level lets the watchdog both (a) reset on any line\n * (data *or* heartbeat = liveness) and (b) recognise heartbeat comment lines\n * to drive `\"auto\"` mode.\n *\n * In `\"auto\"` mode the watchdog is intentionally dormant until it has seen at\n * least two heartbeats (so it can measure the cadence). This means a socket\n * that dies inside the first heartbeat interval won't be caught until a\n * heartbeat would have been due — an acceptable trade for never false-firing\n * on heartbeat-less servers. Pass a fixed `number` if you need coverage from\n * the very first byte.\n */\nexport function idleReconnectStream(\n  options: IdleReconnectStreamOptions\n): TransformStream<Uint8Array, Uint8Array> {\n  const factor = options.timeoutFactor ?? 3;\n  const minTimeoutMs = options.minTimeoutMs ?? 6_000;\n  const maxTimeoutMs = options.maxTimeoutMs ?? 30_000;\n  const fixedTimeoutMs = typeof options.mode === \"number\" ? options.mode : null;\n\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  let controllerRef: TransformStreamDefaultController<Uint8Array> | undefined;\n\n  // `\"auto\"` cadence inference.\n  let lastHeartbeatAt: number | undefined;\n  // The active idle window: the fixed value, or (auto) the heartbeat-derived\n  // value once known. `null` means \"not armed yet\".\n  let derivedTimeoutMs: number | null = fixedTimeoutMs;\n\n  const clear = () => {\n    if (timer != null) {\n      clearTimeout(timer);\n      timer = undefined;\n    }\n  };\n\n  const arm = () => {\n    clear();\n    const timeoutMs = derivedTimeoutMs;\n    if (timeoutMs == null || timeoutMs <= 0) return;\n    timer = setTimeout(() => {\n      options.onIdle?.({\n        timeoutMs,\n        source: fixedTimeoutMs != null ? \"fixed\" : \"heartbeat\",\n      });\n      try {\n        controllerRef?.error(new StreamIdleTimeoutError(timeoutMs));\n      } catch {\n        // Stream already closed/errored/cancelled — nothing to abort.\n      }\n    }, timeoutMs);\n    // Don't let the watchdog by itself keep a Node process alive.\n    (timer as unknown as { unref?: () => void }).unref?.();\n  };\n\n  const noteHeartbeat = () => {\n    if (fixedTimeoutMs != null) return; // cadence irrelevant in fixed mode\n    const now = Date.now();\n    if (lastHeartbeatAt != null) {\n      const interval = now - lastHeartbeatAt;\n      if (interval > 0) {\n        const candidate = Math.min(\n          Math.max(interval * factor, minTimeoutMs),\n          maxTimeoutMs\n        );\n        // Keep the most conservative (largest) window observed so far.\n        derivedTimeoutMs =\n          derivedTimeoutMs == null\n            ? candidate\n            : Math.max(derivedTimeoutMs, candidate);\n      }\n    }\n    lastHeartbeatAt = now;\n  };\n\n  return new TransformStream<Uint8Array, Uint8Array>({\n    start(controller) {\n      controllerRef = controller;\n      // Fixed mode arms eagerly (also catches pre-first-byte silence). Auto\n      // mode waits until a cadence is established in `transform`.\n      arm();\n    },\n    transform(line, controller) {\n      // A line beginning with \":\" is an SSE comment / keep-alive heartbeat.\n      if (line.length > 0 && line[0] === SSE_COMMENT_BYTE) {\n        noteHeartbeat();\n      }\n      // Any line is liveness — (re)arm the idle timer.\n      arm();\n      controller.enqueue(line);\n    },\n    flush() {\n      clear();\n    },\n  });\n}\n\n/**\n * Stream with automatic retry logic for SSE connections.\n * Implements reconnection behavior similar to the Python SDK.\n *\n * @param makeRequest Function to make requests. When `params` is undefined/empty, it's the initial request.\n *                    When `params.reconnectPath` is provided, it's a reconnection request.\n * @param options Configuration options\n * @returns AsyncGenerator yielding stream events\n */\nexport async function* streamWithRetry<T extends { id?: string }>(\n  makeRequest: (params?: StreamRequestParams) => Promise<{\n    response: Response;\n    stream: ReadableStream<T>;\n  }>,\n  options: StreamWithRetryOptions = {}\n): AsyncGenerator<T> {\n  const maxRetries = options.maxRetries ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;\n  let attempt = 0;\n  let lastEventId: string | undefined;\n  let reconnectPath: string | undefined;\n\n  while (true) {\n    let shouldRetry = false;\n    let lastError: unknown;\n    let reader: ReadableStreamDefaultReader<T> | undefined;\n\n    try {\n      // Check if aborted before making request\n      if (options.signal?.aborted) return;\n\n      // Make request - initial if no reconnect path, reconnect otherwise\n      const { response, stream } = await makeRequest(\n        reconnectPath ? { lastEventId, reconnectPath } : undefined\n      );\n\n      // Check for Location header (server-provided reconnection path)\n      const locationHeader = response.headers.get(\"location\");\n      if (locationHeader) {\n        reconnectPath = locationHeader;\n      }\n\n      // Verify content type\n      const contentType = response.headers.get(\"content-type\")?.split(\";\")[0];\n      if (contentType && !contentType.includes(\"text/event-stream\")) {\n        throw new Error(\n          `Expected response header Content-Type to contain 'text/event-stream', got '${contentType}'`\n        );\n      }\n\n      reader = stream.getReader();\n\n      try {\n        while (true) {\n          // Check abort signal before each read\n          if (options.signal?.aborted) {\n            await reader.cancel();\n            return;\n          }\n\n          const { done, value } = await reader.read();\n\n          if (done) {\n            // Stream completed successfully\n            break;\n          }\n\n          // Track last event ID for reconnection\n          if (value.id) {\n            lastEventId = value.id;\n          }\n\n          yield value;\n        }\n\n        // Stream completed successfully, exit retry loop\n        break;\n      } catch (error) {\n        // Error during streaming - attempt reconnect if we have a location header\n        if (reconnectPath && !options.signal?.aborted) {\n          shouldRetry = true;\n        } else {\n          throw error;\n        }\n      } finally {\n        if (reader) {\n          try {\n            reader.releaseLock();\n          } catch {\n            // Ignore errors when releasing lock\n          }\n        }\n      }\n    } catch (error) {\n      lastError = error;\n\n      // Only retry if we have reconnection capability and it's a network error\n      if (isNetworkError(error) && reconnectPath && !options.signal?.aborted) {\n        shouldRetry = true;\n      } else {\n        throw error;\n      }\n    }\n\n    if (shouldRetry) {\n      attempt += 1;\n      if (attempt > maxRetries) {\n        throw new MaxReconnectAttemptsError(maxRetries, lastError);\n      }\n\n      // Notify about reconnection attempt\n      options.onReconnect?.({ attempt, lastEventId, cause: lastError });\n\n      // Exponential backoff with jitter (shared with protocol transports).\n      const delay = reconnectDelayMs(attempt);\n\n      await new Promise((resolve) => {\n        setTimeout(resolve, delay);\n      });\n\n      continue;\n    }\n\n    // Successfully completed\n    break;\n  }\n}\n\n/*\n * Support async iterator syntax for ReadableStreams in all environments.\n * Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490\n */\nexport class IterableReadableStream<T>\n  extends ReadableStream<T>\n  implements IterableReadableStreamInterface<T>\n{\n  public reader: ReadableStreamDefaultReader<T>;\n\n  ensureReader() {\n    if (!this.reader) {\n      this.reader = this.getReader();\n    }\n  }\n\n  async next(): Promise<IteratorResult<T>> {\n    this.ensureReader();\n    try {\n      const result = await this.reader.read();\n      if (result.done) {\n        this.reader.releaseLock(); // release lock when stream becomes closed\n        return {\n          done: true,\n          value: undefined,\n        };\n      } else {\n        return {\n          done: false,\n          value: result.value,\n        };\n      }\n    } catch (e) {\n      this.reader.releaseLock(); // release lock when stream becomes errored\n      throw e;\n    }\n  }\n\n  async return(): Promise<IteratorResult<T>> {\n    this.ensureReader();\n    // If wrapped in a Node stream, cancel is already called.\n    if (this.locked) {\n      const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n      this.reader.releaseLock(); // release lock first\n      await cancelPromise; // now await it\n    }\n    return { done: true, value: undefined };\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  async throw(e: any): Promise<IteratorResult<T>> {\n    this.ensureReader();\n    if (this.locked) {\n      const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n      this.reader.releaseLock(); // release lock first\n      await cancelPromise; // now await it\n    }\n    throw e;\n  }\n\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore Not present in Node 18 types, required in latest Node 22\n  async [Symbol.asyncDispose]() {\n    await this.return();\n  }\n\n  [Symbol.asyncIterator]() {\n    return this;\n  }\n\n  static fromReadableStream<T>(stream: ReadableStream<T>) {\n    // From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream\n    const reader = stream.getReader();\n    return new IterableReadableStream<T>({\n      start(controller) {\n        return pump();\n        function pump(): Promise<T | undefined> {\n          return reader.read().then(({ done, value }) => {\n            // When no more data needs to be consumed, close the stream\n            if (done) {\n              controller.close();\n              return;\n            }\n            // Enqueue the next data chunk into our target stream\n            controller.enqueue(value);\n            return pump();\n          });\n        }\n      },\n      cancel() {\n        reader.releaseLock();\n      },\n    });\n  }\n\n  static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {\n    return new IterableReadableStream<T>({\n      async pull(controller) {\n        const { value, done } = await generator.next();\n        // When no more data needs to be consumed, close the stream\n        if (done) {\n          controller.close();\n        }\n        // Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled\n        controller.enqueue(value);\n      },\n      async cancel(reason) {\n        await generator.return(reason);\n      },\n    });\n  }\n}\n"],"mappings":";;;;;;AAoDA,IAAa,4BAAb,cAA+C,MAAM;CACnD,YAAY,aAAqB,OAAgB;AAC/C,QAAM,+CAA+C,YAAY,GAAG;AACpE,OAAK,OAAO;AACZ,OAAK,QAAQ;;;;;;;;;;;AAYjB,IAAa,yBAAb,cAA4C,MAAM;CAChD;CAEA,YAAY,eAAuB;AACjC,QACE,6BAA6B,cAAc,4DAC5C;AACD,OAAK,OAAO;AACZ,OAAK,gBAAgB;;;;AAKzB,MAAM,mBAAmB;;;;;;AAsBzB,MAAa,yBAA4C;;;;;;;;;;;;;;;;;;;AAiCzD,SAAgB,oBACd,SACyC;CACzC,MAAM,SAAS,QAAQ,iBAAiB;CACxC,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,iBAAiB,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;CAEzE,IAAI;CACJ,IAAI;CAGJ,IAAI;CAGJ,IAAI,mBAAkC;CAEtC,MAAM,cAAc;AAClB,MAAI,SAAS,MAAM;AACjB,gBAAa,MAAM;AACnB,WAAQ,KAAA;;;CAIZ,MAAM,YAAY;AAChB,SAAO;EACP,MAAM,YAAY;AAClB,MAAI,aAAa,QAAQ,aAAa,EAAG;AACzC,UAAQ,iBAAiB;AACvB,WAAQ,SAAS;IACf;IACA,QAAQ,kBAAkB,OAAO,UAAU;IAC5C,CAAC;AACF,OAAI;AACF,mBAAe,MAAM,IAAI,uBAAuB,UAAU,CAAC;WACrD;KAGP,UAAU;AAEZ,QAA4C,SAAS;;CAGxD,MAAM,sBAAsB;AAC1B,MAAI,kBAAkB,KAAM;EAC5B,MAAM,MAAM,KAAK,KAAK;AACtB,MAAI,mBAAmB,MAAM;GAC3B,MAAM,WAAW,MAAM;AACvB,OAAI,WAAW,GAAG;IAChB,MAAM,YAAY,KAAK,IACrB,KAAK,IAAI,WAAW,QAAQ,aAAa,EACzC,aACD;AAED,uBACE,oBAAoB,OAChB,YACA,KAAK,IAAI,kBAAkB,UAAU;;;AAG/C,oBAAkB;;AAGpB,QAAO,IAAI,gBAAwC;EACjD,MAAM,YAAY;AAChB,mBAAgB;AAGhB,QAAK;;EAEP,UAAU,MAAM,YAAY;AAE1B,OAAI,KAAK,SAAS,KAAK,KAAK,OAAO,iBACjC,gBAAe;AAGjB,QAAK;AACL,cAAW,QAAQ,KAAK;;EAE1B,QAAQ;AACN,UAAO;;EAEV,CAAC;;;;;;;;;;;AAYJ,gBAAuB,gBACrB,aAIA,UAAkC,EAAE,EACjB;CACnB,MAAM,aAAa,QAAQ,cAAA;CAC3B,IAAI,UAAU;CACd,IAAI;CACJ,IAAI;AAEJ,QAAO,MAAM;EACX,IAAI,cAAc;EAClB,IAAI;EACJ,IAAI;AAEJ,MAAI;AAEF,OAAI,QAAQ,QAAQ,QAAS;GAG7B,MAAM,EAAE,UAAU,WAAW,MAAM,YACjC,gBAAgB;IAAE;IAAa;IAAe,GAAG,KAAA,EAClD;GAGD,MAAM,iBAAiB,SAAS,QAAQ,IAAI,WAAW;AACvD,OAAI,eACF,iBAAgB;GAIlB,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe,EAAE,MAAM,IAAI,CAAC;AACrE,OAAI,eAAe,CAAC,YAAY,SAAS,oBAAoB,CAC3D,OAAM,IAAI,MACR,8EAA8E,YAAY,GAC3F;AAGH,YAAS,OAAO,WAAW;AAE3B,OAAI;AACF,WAAO,MAAM;AAEX,SAAI,QAAQ,QAAQ,SAAS;AAC3B,YAAM,OAAO,QAAQ;AACrB;;KAGF,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAE3C,SAAI,KAEF;AAIF,SAAI,MAAM,GACR,eAAc,MAAM;AAGtB,WAAM;;AAIR;YACO,OAAO;AAEd,QAAI,iBAAiB,CAAC,QAAQ,QAAQ,QACpC,eAAc;QAEd,OAAM;aAEA;AACR,QAAI,OACF,KAAI;AACF,YAAO,aAAa;YACd;;WAKL,OAAO;AACd,eAAY;AAGZ,OAAIA,cAAAA,eAAe,MAAM,IAAI,iBAAiB,CAAC,QAAQ,QAAQ,QAC7D,eAAc;OAEd,OAAM;;AAIV,MAAI,aAAa;AACf,cAAW;AACX,OAAI,UAAU,WACZ,OAAM,IAAI,0BAA0B,YAAY,UAAU;AAI5D,WAAQ,cAAc;IAAE;IAAS;IAAa,OAAO;IAAW,CAAC;GAGjE,MAAM,QAAQC,kBAAAA,iBAAiB,QAAQ;AAEvC,SAAM,IAAI,SAAS,YAAY;AAC7B,eAAW,SAAS,MAAM;KAC1B;AAEF;;AAIF;;;AAQJ,IAAa,yBAAb,MAAa,+BACH,eAEV;CACE;CAEA,eAAe;AACb,MAAI,CAAC,KAAK,OACR,MAAK,SAAS,KAAK,WAAW;;CAIlC,MAAM,OAAmC;AACvC,OAAK,cAAc;AACnB,MAAI;GACF,MAAM,SAAS,MAAM,KAAK,OAAO,MAAM;AACvC,OAAI,OAAO,MAAM;AACf,SAAK,OAAO,aAAa;AACzB,WAAO;KACL,MAAM;KACN,OAAO,KAAA;KACR;SAED,QAAO;IACL,MAAM;IACN,OAAO,OAAO;IACf;WAEI,GAAG;AACV,QAAK,OAAO,aAAa;AACzB,SAAM;;;CAIV,MAAM,SAAqC;AACzC,OAAK,cAAc;AAEnB,MAAI,KAAK,QAAQ;GACf,MAAM,gBAAgB,KAAK,OAAO,QAAQ;AAC1C,QAAK,OAAO,aAAa;AACzB,SAAM;;AAER,SAAO;GAAE,MAAM;GAAM,OAAO,KAAA;GAAW;;CAIzC,MAAM,MAAM,GAAoC;AAC9C,OAAK,cAAc;AACnB,MAAI,KAAK,QAAQ;GACf,MAAM,gBAAgB,KAAK,OAAO,QAAQ;AAC1C,QAAK,OAAO,aAAa;AACzB,SAAM;;AAER,QAAM;;CAKR,OAAO,OAAO,gBAAgB;AAC5B,QAAM,KAAK,QAAQ;;CAGrB,CAAC,OAAO,iBAAiB;AACvB,SAAO;;CAGT,OAAO,mBAAsB,QAA2B;EAEtD,MAAM,SAAS,OAAO,WAAW;AACjC,SAAO,IAAI,uBAA0B;GACnC,MAAM,YAAY;AAChB,WAAO,MAAM;IACb,SAAS,OAA+B;AACtC,YAAO,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM,YAAY;AAE7C,UAAI,MAAM;AACR,kBAAW,OAAO;AAClB;;AAGF,iBAAW,QAAQ,MAAM;AACzB,aAAO,MAAM;OACb;;;GAGN,SAAS;AACP,WAAO,aAAa;;GAEvB,CAAC;;CAGJ,OAAO,mBAAsB,WAA8B;AACzD,SAAO,IAAI,uBAA0B;GACnC,MAAM,KAAK,YAAY;IACrB,MAAM,EAAE,OAAO,SAAS,MAAM,UAAU,MAAM;AAE9C,QAAI,KACF,YAAW,OAAO;AAGpB,eAAW,QAAQ,MAAM;;GAE3B,MAAM,OAAO,QAAQ;AACnB,UAAM,UAAU,OAAO,OAAO;;GAEjC,CAAC"}