{"version":3,"file":"sse-writer.cjs","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n  if (ms <= 0 || signal?.aborted) return Promise.resolve();\n  return new Promise((resolve) => {\n    const timer = setTimeout(resolve, ms);\n    signal?.addEventListener(\n      \"abort\",\n      () => {\n        clearTimeout(timer);\n        resolve();\n      },\n      { once: true },\n    );\n  });\n}\n\nexport interface StreamOptions {\n  latency?: number;\n  streamingProfile?: StreamingProfile;\n  recordedTimings?: RecordedTimings;\n  replaySpeed?: number;\n  signal?: AbortSignal;\n  onChunkSent?: () => void;\n  /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n  usageChunk?: SSEChunk;\n}\n\nexport function calculateDelay(\n  chunkIndex: number,\n  profile?: StreamingProfile,\n  fallbackLatency?: number,\n  recordedTimings?: RecordedTimings,\n  replaySpeed?: number,\n): number {\n  const speed = replaySpeed ?? 1.0;\n  let delayMs: number;\n\n  if (profile) {\n    // StreamingProfile has highest precedence\n    let fromProfile = true;\n    if (chunkIndex === 0 && profile.ttft !== undefined) {\n      delayMs = profile.ttft;\n    } else if (profile.tps !== undefined && profile.tps > 0) {\n      delayMs = 1000 / profile.tps;\n    } else {\n      delayMs = fallbackLatency ?? 0;\n      fromProfile = false;\n    }\n    // Jitter only applies when the delay came from ttft/tps, not fallback\n    if (fromProfile && profile.jitter && profile.jitter > 0) {\n      delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n      if (delayMs < 0) delayMs = 0;\n    }\n  } else if (recordedTimings) {\n    // Recorded timings (second precedence)\n    if (chunkIndex === 0) {\n      delayMs = recordedTimings.ttftMs;\n    } else {\n      const idx = chunkIndex - 1;\n      if (idx < recordedTimings.interChunkDelaysMs.length) {\n        delayMs = recordedTimings.interChunkDelaysMs[idx];\n      } else {\n        // Excess chunks: derive average from recorded inter-chunk delays\n        const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n        delayMs =\n          recordedTimings.interChunkDelaysMs.length > 0\n            ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n            : 0;\n      }\n    }\n  } else {\n    delayMs = fallbackLatency ?? 0;\n  }\n\n  delayMs = Math.max(0, delayMs);\n  return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n  res: http.ServerResponse,\n  chunks: SSEChunk[],\n  optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n  const opts: StreamOptions =\n    typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n  const latency = opts.latency ?? 0;\n  const profile = opts.streamingProfile;\n  const { recordedTimings, replaySpeed } = opts;\n  const signal = opts.signal;\n  const onChunkSent = opts.onChunkSent;\n\n  if (res.writableEnded) return true;\n  res.setHeader(\"Content-Type\", \"text/event-stream\");\n  res.setHeader(\"Cache-Control\", \"no-cache\");\n  res.setHeader(\"Connection\", \"keep-alive\");\n\n  let chunkIndex = 0;\n  for (const chunk of chunks) {\n    const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n    if (chunkDelay > 0) {\n      await delay(chunkDelay, signal);\n    }\n    if (signal?.aborted) return false;\n    if (res.writableEnded) return true;\n    res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n    onChunkSent?.();\n    if (signal?.aborted) return false;\n    chunkIndex++;\n  }\n\n  if (!res.writableEnded) {\n    if (opts.usageChunk) {\n      res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n    }\n    res.write(\"data: [DONE]\\n\\n\");\n    res.end();\n  }\n  return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n  \"x-ratelimit-limit-requests\": \"60\",\n  \"x-ratelimit-limit-tokens\": \"150000\",\n  \"x-ratelimit-remaining-requests\": \"0\",\n  \"x-ratelimit-remaining-tokens\": \"0\",\n  \"x-ratelimit-reset-requests\": \"1s\",\n  \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n  /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n  retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n  res: http.ServerResponse,\n  status: number,\n  body: string,\n  options?: ErrorResponseOptions,\n): void {\n  const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n  if (status === 429) {\n    headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n    Object.assign(headers, RATE_LIMIT_HEADERS);\n  }\n  res.writeHead(status, headers);\n  res.end(body);\n}\n"],"mappings":";;AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;AAcJ,SAAgB,eACd,YACA,SACA,iBACA,iBACA,aACQ;CACR,MAAM,QAAQ,eAAe;CAC7B,IAAI;AAEJ,KAAI,SAAS;EAEX,IAAI,cAAc;AAClB,MAAI,eAAe,KAAK,QAAQ,SAAS,OACvC,WAAU,QAAQ;WACT,QAAQ,QAAQ,UAAa,QAAQ,MAAM,EACpD,WAAU,MAAO,QAAQ;OACpB;AACL,aAAU,mBAAmB;AAC7B,iBAAc;;AAGhB,MAAI,eAAe,QAAQ,UAAU,QAAQ,SAAS,GAAG;AACvD,cAAW,KAAK,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ;AACjD,OAAI,UAAU,EAAG,WAAU;;YAEpB,gBAET,KAAI,eAAe,EACjB,WAAU,gBAAgB;MACrB;EACL,MAAM,MAAM,aAAa;AACzB,MAAI,MAAM,gBAAgB,mBAAmB,OAC3C,WAAU,gBAAgB,mBAAmB;OACxC;GAEL,MAAM,kBAAkB,gBAAgB,mBAAmB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE;AACrF,aACE,gBAAgB,mBAAmB,SAAS,IACxC,kBAAkB,gBAAgB,mBAAmB,SACrD;;;KAIV,WAAU,mBAAmB;AAG/B,WAAU,KAAK,IAAI,GAAG,QAAQ;AAC9B,QAAO,QAAQ,IAAI,UAAU,QAAQ;;AAGvC,eAAsB,eACpB,KACA,QACA,kBACkB;CAClB,MAAM,OACJ,OAAO,qBAAqB,WAAW,EAAE,SAAS,kBAAkB,GAAI,oBAAoB,EAAE;CAChG,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,UAAU,KAAK;CACrB,MAAM,EAAE,iBAAiB,gBAAgB;CACzC,MAAM,SAAS,KAAK;CACpB,MAAM,cAAc,KAAK;AAEzB,KAAI,IAAI,cAAe,QAAO;AAC9B,KAAI,UAAU,gBAAgB,oBAAoB;AAClD,KAAI,UAAU,iBAAiB,WAAW;AAC1C,KAAI,UAAU,cAAc,aAAa;CAEzC,IAAI,aAAa;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,aAAa,eAAe,YAAY,SAAS,SAAS,iBAAiB,YAAY;AAC7F,MAAI,aAAa,EACf,OAAM,MAAM,YAAY,OAAO;AAEjC,MAAI,QAAQ,QAAS,QAAO;AAC5B,MAAI,IAAI,cAAe,QAAO;AAC9B,MAAI,MAAM,SAAS,KAAK,UAAU,MAAM,CAAC,MAAM;AAC/C,iBAAe;AACf,MAAI,QAAQ,QAAS,QAAO;AAC5B;;AAGF,KAAI,CAAC,IAAI,eAAe;AACtB,MAAI,KAAK,WACP,KAAI,MAAM,SAAS,KAAK,UAAU,KAAK,WAAW,CAAC,MAAM;AAE3D,MAAI,MAAM,mBAAmB;AAC7B,MAAI,KAAK;;AAEX,QAAO;;;;;;AAOT,MAAM,qBAA6C;CACjD,8BAA8B;CAC9B,4BAA4B;CAC5B,kCAAkC;CAClC,gCAAgC;CAChC,8BAA8B;CAC9B,4BAA4B;CAC7B;AAOD,SAAgB,mBACd,KACA,QACA,MACA,SACM;CACN,MAAM,UAAkC,EAAE,gBAAgB,oBAAoB;AAC9E,KAAI,WAAW,KAAK;AAClB,UAAQ,iBAAiB,OAAO,SAAS,cAAc,EAAE;AACzD,SAAO,OAAO,SAAS,mBAAmB;;AAE5C,KAAI,UAAU,QAAQ,QAAQ;AAC9B,KAAI,IAAI,KAAK"}