How the Svelte 5 SPA in Agentsview Communicates with the Go Backend Using SSE for Real-Time Session Updates
The Svelte 5 SPA opens a native EventSource connection to the Go backend's /api/v1/sessions/:id/watch endpoint, which streams Server-Sent Events (SSE) through a broadcaster pattern that pushes session updates instantly to the browser.
The agentsview repository pairs a Svelte 5 single-page application with a Go HTTP server to provide live visibility into AI agent sessions. To synchronize the frontend with backend state changes—such as new messages or timing updates—the Svelte SPA communicates with the Go backend using Server-Sent Events (SSE), allowing the server to push data to the browser over a persistent HTTP connection without polling.
Frontend: Establishing the SSE Connection
The SPA’s API layer in frontend/src/lib/api/client.ts exposes the watchSession function, which creates a native browser EventSource and wires event listeners for real-time updates.
Creating the EventSource
Because the EventSource API does not support custom headers, the authentication token is appended as a query parameter to satisfy the Go server’s authorization requirements. This behavior is documented in docs/remote-access.md.
export function watchSession(
sessionId: string,
onUpdate: () => void,
onTiming?: (t: SessionTiming) => void,
): EventSource {
const url = `${getBase()}/sessions/${sessionId}/watch`;
const token = getAuthToken();
const fullUrl = token ? `${url}?token=${encodeURIComponent(token)}` : url;
const es = new EventSource(fullUrl); // Native SSE connection
// Circuit-breaker logic
let consecutiveErrors = 0;
es.addEventListener("open", () => { consecutiveErrors = 0; });
es.addEventListener("session_updated", () => {
consecutiveErrors = 0;
onUpdate(); // Trigger UI refresh
});
if (onTiming) {
es.addEventListener("session.timing", (ev) => {
onTiming(JSON.parse(ev.data) as SessionTiming);
});
}
es.onerror = () => {
consecutiveErrors += 1;
if (consecutiveErrors >= WATCH_SESSION_MAX_CONSECUTIVE_ERRORS) {
es.close(); // Prevent infinite reconnect loops
}
};
return es;
}
The function returns the EventSource instance, which the SPA stores in frontend/src/lib/stores/events.svelte.ts. The store automatically closes the connection when the last subscriber disappears, ensuring the browser only maintains the stream while the session page is active.
Backend: Registering the SSE Stream
In internal/server/huma_routes_sessions.go, the Go server registers the streaming endpoint using the Huma router’s stream helper, which configures the response for SSE by setting Content-Type: text/event-stream and disabling buffering.
func (s *Server) registerSessionRoutes() {
group := newRouteGroup(s.api, "/api/v1", "Sessions")
// ...
stream(s, group, http.MethodGet,
"/sessions/{id}/watch", "Watch session events", s.humaWatchSession)
}
The humaWatchSession Handler
The humaWatchSession handler (located in the same file) validates the session ID, checks permissions, and subscribes the HTTP connection to the global broadcaster. It writes RFC-compliant SSE frames—consisting of an event: line, a data: line, and a double newline terminator—to the http.ResponseWriter.
func (s *Server) humaWatchSession(ctx context.Context, in *watchSessionInput) error {
// 1. Resolve session and auth
events, unsubscribe := broadcaster.Subscribe(in.ID)
defer unsubscribe()
// 2. Access underlying ResponseWriter and Flusher
w := huma.ResponseWriterFrom(ctx)
flusher := w.(http.Flusher)
// 3. Stream loop
for {
select {
case ev := <-events:
fmt.Fprintf(w, "event: %s\n", ev.Event)
fmt.Fprintf(w, "data: %s\n\n", ev.Data) // Double \n terminates frame
flusher.Flush() // Push immediately to client
case <-ctx.Done():
return nil // Client disconnected
}
}
}
The Broadcaster Pattern
The broadcaster struct in internal/server/broadcaster.go manages per-session subscriptions using a thread-safe map of channels. When any part of the backend—such as the sync engine in internal/sync/engine.go or message parsers—detects a state change, it broadcasts the event to all active listeners.
type broadcaster struct {
mu sync.RWMutex
subs map[string]map[chan SSEEvent]struct{} // sessionID → subscriber channels
}
func (b *broadcaster) Broadcast(sessionID string, ev SSEEvent) {
b.mu.RLock()
for ch := range b.subs[sessionID] {
select {
case ch <- ev:
default:
// Drop if subscriber is too far behind (respects WATCH_MAX_BEHIND)
}
}
b.mu.RUnlock()
}
When broadcaster.Broadcast is called, the humaWatchSession handler receives the event on its subscribed channel and writes it to the SSE stream, delivering the update to the browser within milliseconds.
End-to-End Data Flow
- User opens a session page: The Svelte component mounts and calls
watchSession(sessionId, callback). - Browser connects: The
EventSourceinitiates a GET request to/api/v1/sessions/<id>/watch?token=.... - Go server subscribes:
humaWatchSessionvalidates the token, callsbroadcaster.Subscribe(sessionID), and enters a streaming loop. - State change occurs: The sync engine or parser detects a new message and invokes
broadcaster.Broadcastwithevent: session_updated. - Client receives update: The browser fires the
session_updatedevent, triggering theonUpdatecallback and refreshing the UI. - Cleanup: If the connection drops, the browser auto-reconnects; after five consecutive errors, the frontend circuit-breaker closes the stream to prevent infinite loops.
Summary
- Native EventSource: The Svelte 5 SPA uses the browser's built-in
EventSourceAPI to maintain a persistent connection to/api/v1/sessions/:id/watch. - Token-in-URL: Because
EventSourcecannot set theAuthorizationheader, the JWT token is passed as a query parameter and validated by the Go backend. - Broadcaster Pattern: The Go backend decouples HTTP handling from business logic via
internal/server/broadcaster.go, which multicasts updates to all active session viewers. - Circuit-Breaker: Frontend logic in
frontend/src/lib/api/client.tsprevents infinite reconnection attempts by closing the stream afterWATCH_SESSION_MAX_CONSECUTIVE_ERRORSfailures.
Frequently Asked Questions
Why is the authentication token passed as a query parameter instead of a header?
The native EventSource API does not allow setting custom HTTP headers, including the Authorization header typically used for JWT tokens. As documented in docs/remote-access.md, the agentsview implementation appends the token to the URL as ?token=... and validates it on the server side before establishing the SSE stream.
How does the application handle temporary network interruptions?
The browser's EventSource implementation automatically attempts to reconnect when the connection drops. On the frontend, a circuit-breaker pattern tracks consecutive errors via the onerror handler; if WATCH_SESSION_MAX_CONSECUTIVE_ERRORS is reached, the code explicitly calls es.close() to stop retrying and prevent endless reconnect loops against a permanently failed endpoint.
What happens to the SSE connection when the user navigates away from the session page?
The SPA manages connection lifecycle through frontend/src/lib/stores/events.svelte.ts. When the last Svelte store subscriber unsubscribes—which occurs when the user leaves the session page—the store closes the EventSource, immediately freeing the HTTP connection and removing the subscriber from the Go broadcaster's internal map.
How does the backend support multiple users watching the same session simultaneously?
The broadcaster struct in internal/server/broadcaster.go maintains a map of sessionID to a set of subscriber channels. When humaWatchSession handles a new request, it registers a unique Go channel for that connection. The Broadcast method iterates over all channels for the specific session ID, pushing the same SSE frame to every active viewer independently.
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 →