06. JavaScript EventSource

Chapter 6 of 15 · 15 min

The browser receives streamed responses via EventSource, which only works with GET requests. Since the chatbot needs to send a POST body (the message), we use a two-step pattern: POST the message to create the stream, then read the stream via a separate mechanism. For SSE, the POST route itself returns the stream, so EventSource works—but you must pass the message as URL query parameters or use a hidden fetch + manual parsing.

The cleanest approach uses a fetch + ReadableStream reader in JavaScript, bypassing EventSource for POST-initiated streams:

async function sendMessage(message) {
  const response = await fetch(`/chat?model=${encodeURIComponent(selectedModel)}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages: conversationHistory }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let partial = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    partial += decoder.decode(value, { stream: true });
    // SSE format: data: {...}\n\n
    const lines = partial.split("\n");
    partial = lines.pop() ?? "";
    for (const line of lines) {
      if (line.startsWith("data: ")) {
        try {
          const data = JSON.parse(line.slice(6));
          if (data.message?.content) {
            appendToken(data.message.content);
          }
        } catch (e) { /* ignore parse errors on partial JSON */ }
      }
    }
  }
}

A failure mode: if the Ollama response is not valid SSE format (e.g., a JSON parse error from Ollama), the JSON.parse throws. Wrap it in try/catch and log to console.

Local verification checkpoint

Run the smallest example from this chapter in a local workspace and record the package version, runtime, data path, and observed output. If the result depends on model size, vector count, CPU/GPU backend, or available memory, note that constraint beside the exercise so the lesson remains reproducible.

EXERCISE

Replace the hardcoded appendToken with a function that appends the token to a <span> element and auto-scrolls the chat div to the bottom.