How Server-Sent Events (SSE) Enable Real-Time UI Updates in AgentsView
AgentsView uses a long-running HTTP connection that streams Server-Sent Events to push database changes from the Go backend to the browser instantly, eliminating the need for polling.
The kenn-io/agentsview repository implements a push-only real-time architecture using Server-Sent Events (SSE) to synchronize the SQLite database state with the single-page application (SPA) frontend. By maintaining an open HTTP stream, the server can broadcast incremental updates—such as new messages, sync progress, or insight generation—without client-side polling. This approach reduces latency and ensures the UI remains perfectly synchronized with the underlying data.
SSE Architecture Overview
AgentsView’s SSE implementation centers on three core components: the stream initializer that establishes the HTTP connection, the event emitter that formats and transmits data, and the server-side wiring that monitors database changes and broadcasts them to active clients.
Stream Initialization and Headers
In internal/server/sse.go, the NewSSEStream function validates that the http.ResponseWriter implements http.Flusher, confirming the server supports streaming responses. It then writes the mandatory SSE headers required by the browser’s EventSource API:
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive
After flushing the headers, the function returns an SSEStream struct that wraps the response writer. This setup establishes a persistent connection exempt from the server’s generic WriteTimeout (as noted in internal/server/timeout_test.go), allowing the stream to remain open for minutes while awaiting new data.
Event Emission Methods
The SSEStream type provides two primary methods for transmitting data:
Send(event, data string): Writes a plain-text SSE line following the formatevent: …\ndata: …\n\nand immediately flushes the writer. AsseWriteTimeoutof 3 seconds prevents stuck clients from blocking the server indefinitely.SendJSON(event, v any): Marshals a Go value to JSON and forwards it toSend. Both methods returnfalseon error, allowing the handler to terminate the stream gracefully.
Server-Side Event Wiring
The backend aggregates database changes and distributes them to all subscribed SSE streams using a monitor and broadcaster pattern.
Session Monitoring
The sessionMonitor function in internal/server/events.go creates a change-detection channel for a specific session ID. It instantiates a Watcher from internal/sessionwatch/watcher.go that polls the SQLite store and emits notifications whenever the session state changes:
func (s *Server) sessionMonitor(ctx context.Context, sessionID string) <-chan struct{} {
return sessionwatch.New(s.db, s.engine).Events(ctx, sessionID)
}
This channel blocks the SSE handler until a change occurs, ensuring events are only sent when the database actually updates.
Broadcasting to Clients
In internal/server/server.go, the WithBroadcaster middleware aggregates all active SSE streams. When the database changes—triggered by new messages, completed syncs, or insight generation—the broadcaster invokes Send or SendJSON on each registered SSEStream.
The SSE endpoints (e.g., /api/v1/watch, /api/v1/sync) are registered in the server’s router via Server.routes. Each handler follows this pattern:
- Initialize the stream with
NewSSEStream(w) - Subscribe to the monitor channel for the requested session
- Enter a
selectloop that blocks on context cancellation or the monitor channel - Emit events such as
"session_updated","messages","progress", or"done"until the client disconnects
Client-Side Integration
The frontend consumes these streams using the standard JavaScript EventSource API. In frontend/src/lib/api/client.ts, the SPA creates an EventSource pointing at /api/v1/watch and registers listeners for specific event names:
const src = new EventSource("/api/v1/watch?session_id=123");
src.addEventListener("session_updated", ev => {
const data = JSON.parse(ev.data);
updateSessionUI(data);
});
src.addEventListener("error", () => src.close());
When a payload arrives, the listener immediately updates the UI state, providing a seamless live-refresh experience without polling overhead.
Implementation Examples
Server-Side Handler
This example from internal/server/sse.go demonstrates creating a stream and publishing JSON events:
func (s *Server) handleWatch(w http.ResponseWriter, r *http.Request) {
// Initialise the stream (sets headers, flushes initial line).
stream, err := NewSSEStream(w)
if err != nil {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}
// Subscribe to changes for a particular session.
events := s.sessionMonitor(r.Context(), r.URL.Query().Get("session_id"))
for {
select {
case <-r.Context().Done():
// Client disconnected.
return
case <-events:
// DB changed – push a JSON payload.
payload := map[string]any{"type": "session_updated"}
if !stream.SendJSON("session_updated", payload) {
return // write error – stop streaming.
}
}
}
}
Client-Side Listener
The corresponding browser implementation parses incoming events and triggers UI updates:
const src = new EventSource("/api/v1/watch?session_id=123");
src.addEventListener("session_updated", ev => {
const data = JSON.parse(ev.data);
// Update UI state with the new session information.
updateSessionUI(data);
});
src.addEventListener("error", () => src.close()); // cleanup on failure.
Summary
- Long-running HTTP: AgentsView uses
NewSSEStreamininternal/server/sse.goto establish persistent connections with proper SSE headers, bypassing standard write timeouts. - Structured events: The
SendandSendJSONmethods format data according to the SSE specification and apply a 3-second write timeout to prevent resource exhaustion. - Database-driven:
sessionMonitorininternal/server/events.gowatches SQLite changes viainternal/sessionwatch/watcher.goand signals the broadcaster. - Push architecture: The
WithBroadcastermechanism ininternal/server/server.godistributes events to all active SSE streams, eliminating polling. - Browser consumption: The frontend uses
EventSourceinfrontend/src/lib/api/client.tsto receive real-time updates and refresh the UI immediately.
Frequently Asked Questions
What is the difference between SSE and WebSockets in AgentsView?
AgentsView uses Server-Sent Events because its architecture requires server-to-client push only, not bidirectional communication. SSE leverages standard HTTP, automatically handles reconnection via the browser’s EventSource API, and passes through most corporate firewalls and proxies without protocol upgrades. WebSockets would add unnecessary complexity for a one-way data flow from the SQLite database to the UI.
How does AgentsView prevent SSE connections from blocking the server?
The Send method in internal/server/sse.go applies a sseWriteTimeout of 3 seconds to every write operation. If a client becomes unresponsive, the write fails quickly, returning false and allowing the handler to close the stream. Additionally, the broadcaster removes disconnected clients from the active stream registry, preventing memory leaks.
Why does the SSE response bypass the server’s WriteTimeout?
According to comments in internal/server/timeout_test.go, the SSE response is explicitly exempt from the generic WriteTimeout because the connection must remain open for minutes to stream real-time updates. Without this exemption, the server would terminate long-running watch requests prematurely, breaking the live-update functionality.
What types of events does AgentsView stream to the client?
The server emits several event types through the /api/v1/watch endpoint, including "session_updated" for state changes, "messages" for new chat entries, "progress" for ongoing synchronization status, and "done" to signal completion. The frontend registers distinct listeners for each event type to update specific UI components accordingly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →