Event Types and IDs
Named events
SSE supports named event types with the event: field. The client listens for specific types:
event: task_created
data: {"task":{"id":"task-5","title":"New task"}}
event: task_moved
data: {"task":{"id":"task-3","listId":"list-done"}}
event: task_deleted
data: {"taskId":"task-2"}
On the client, use addEventListener instead of onmessage to listen for specific types:
const source = new EventSource("/boards/board-1/events");
source.addEventListener("task_created", (event) => {
const data = JSON.parse(event.data);
addTaskToBoard(data.task);
});
source.addEventListener("task_moved", (event) => {
const data = JSON.parse(event.data);
moveTaskOnBoard(data.task);
});
source.addEventListener("task_deleted", (event) => {
const data = JSON.parse(event.data);
removeTaskFromBoard(data.taskId);
}); onmessage only fires for events without a named type. addEventListener fires for the specific type. This lets the client handle each event type differently without parsing the data first.
Update the formatting
function formatSSE(data: any, eventType?: string, id?: string): string {
let event = "";
if (id) event += `id: ${id}\n`;
if (eventType) event += `event: ${eventType}\n`;
event += `data: ${JSON.stringify(data)}\n\n`;
return event;
}
// Usage
broadcast(boardId, { task }, "task_created", eventId); Event IDs and resumption
Each event can have an id: field. The browser tracks the last received ID. When the connection drops and the browser reconnects, it sends a Last-Event-ID header with the ID of the last event it received.
The server can use this to replay missed events:
id: 42
event: task_created
data: {"task":{"id":"task-5"}}
id: 43
event: task_moved
data: {"task":{"id":"task-3","listId":"list-done"}}
If the connection drops after event 42, the browser reconnects with Last-Event-ID: 42. The server replays events starting from ID 43.
Implementing resume
Store recent events in a buffer so they can be replayed:
// src/sse.ts — add event buffer
const EVENT_BUFFER_SIZE = 100;
const eventBuffer: Map<string, { id: number; type: string; data: any }[]> = new Map();
let globalEventId = 0;
export function broadcast(boardId: string, data: any, eventType?: string): void {
const id = ++globalEventId;
const event = formatSSE(data, eventType, String(id));
// Store in buffer
if (!eventBuffer.has(boardId)) eventBuffer.set(boardId, []);
const buffer = eventBuffer.get(boardId)!;
buffer.push({ id, type: eventType ?? "message", data });
if (buffer.length > EVENT_BUFFER_SIZE) buffer.shift();
// Send to connected clients
const matching = clients.filter((c) => c.boardId === boardId);
for (const client of matching) {
try {
client.controller.enqueue(event);
} catch {
const index = clients.indexOf(client);
if (index !== -1) clients.splice(index, 1);
}
}
}
export function replayEvents(boardId: string, lastEventId: string): string {
const buffer = eventBuffer.get(boardId) ?? [];
const lastId = parseInt(lastEventId, 10);
const missed = buffer.filter((e) => e.id > lastId);
return missed.map((e) => formatSSE(e.data, e.type, String(e.id))).join("");
} Update the SSE route to handle Last-Event-ID:
route.get("/boards/:boardId/events", {
resolve: (c) => {
const lastEventId = c.request.headers.get("last-event-id");
const stream = new ReadableStream({
start(controller) {
// Replay missed events
if (lastEventId) {
const replay = replayEvents(c.params.boardId, lastEventId);
if (replay) controller.enqueue(replay);
}
// Register for future events
const client = { boardId: c.params.boardId, controller };
clients.push(client);
controller.enqueue(formatSSE({ type: "connected" }));
},
cancel() {
// cleanup...
},
});
return new Response(stream, {
headers: {
"content-type": "text/event-stream",
"cache-control": "no-cache",
connection: "keep-alive",
},
});
},
}); When the browser reconnects, the server replays any events the client missed during the disconnection. The client’s event handlers process them as if they arrived in real time.
Exercises
Exercise 1: Add named event types. Use event: task_created instead of putting the type in the data. Verify addEventListener works on the client.
Exercise 2: Add event IDs. Connect, receive some events, disconnect (kill the connection), reconnect. Verify missed events are replayed.
Exercise 3: Fill the event buffer beyond its limit. Verify old events are dropped and the buffer stays at the configured size.
What happens when a browser reconnects to an SSE endpoint with Last-Event-ID?