06. JavaScript EventSource
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.
Replace the hardcoded appendToken with a function that appends the token to a <span> element and auto-scrolls the chat div to the bottom.