Frontend consumption

The browser consumes SSE with either the native EventSource API or a fetch with a ReadableStream. Fetch is more flexible for POST bodies, which is what the /ask/stream endpoint requires. Parse each data: line, JSON.parse the payload, and switch on the keys you recognize.

sse-client.ts
typescript
async function streamAsk(question: string) {
  const res = await fetch("/advanced-rag/ask/stream", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ question, rewrite_n: 3, with_evaluation: true }),
  });
  if (!res.body) throw new Error("No stream body");

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const parts = buffer.split("\n\n");
    buffer = parts.pop() ?? "";

    for (const part of parts) {
      if (!part.startsWith("data: ")) continue;
      const payload = JSON.parse(part.slice(6));

      if (payload.rewrites) renderRewrites(payload.rewrites);
      else if (payload.sources) renderSources(payload.sources);
      else if (payload.content) renderAnswer(payload.content);
      else if (payload.eval_score) renderScores(payload.eval_score);
      else if (payload.thinking) renderThinking(payload.thinking);
      else if (payload.done) onDone();
      else if (payload.error) onError(payload.error);
    }
  }
}

The buffer pattern matters. SSE events are separated by double newlines, but they can arrive in arbitrary TCP chunks. Buffer until you see the separator, process the complete events, and keep any trailing partial event for the next read.

Flashcards: Flashcards

Loading practice…

Quiz: Quiz

Loading practice…