Live events
Subscribe to server-sent events for import progress instead of polling.
GET /api/events/stream is a Server-Sent
Events stream of
changes to the caller's data. Its main use is import progress — it removes
polling entirely.
event: import.updated
data: {"id":"4d1f8c2e","status":"processing"}
event: import.completed
data: {"id":"4d1f8c2e","recipe_id":"9b8a7c6d"}Connecting
The stream is authenticated with the usual bearer header.
`EventSource` will not work in a browser
The browser's built-in EventSource cannot set request headers, so it cannot
send Authorization. Use a fetch-based SSE client instead. This trips up
nearly everyone the first time.
async function subscribe(onEvent: (name: string, data: unknown) => void) {
const res = await fetch('https://api.flambe.dev/api/events/stream', {
headers: { Authorization: `Bearer ${await getToken()}` },
});
if (!res.ok) throw new Error(`stream failed: ${res.status}`);
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
// SSE frames are separated by a blank line.
const frames = buffer.split('\n\n');
buffer = frames.pop() ?? '';
for (const frame of frames) {
let name = 'message';
const payload: string[] = [];
for (const line of frame.split('\n')) {
if (line.startsWith(':')) continue; // keep-alive comment
if (line.startsWith('event:')) name = line.slice(6).trim();
if (line.startsWith('data:')) payload.push(line.slice(5).trim());
}
if (payload.length) onEvent(name, JSON.parse(payload.join('\n')));
}
}
}Keep-alives
The server sends a comment line — one beginning with : — every few seconds so
intermediaries do not drop an idle connection. Skip those lines; they are not
events.
Reconnecting
The stream is best-effort and carries no replay. Events that occurred while you were disconnected are gone.
So on every reconnect, delta sync to catch up, then rely on the stream for what happens next. Reconnect with backoff rather than immediately — a tight reconnect loop against a server that is having trouble makes things worse.
A 401 closes the stream with an empty body. Refresh the token and reconnect
once; do not loop.
Still poll as a backstop
For a job you must not lose track of, combine both: react to
import.completed, and also poll GET /api/imports/{id} on a slow interval —
say every 15 seconds — so a dropped connection cannot leave a job hanging in
your UI forever.