Captures the full output of autodev existing-code Phase A through Step 4 (Code Testability Revision) for the Azaion UI workspace: - Step 1 Document: _docs/02_document/ (FINAL_report, architecture, glossary, components/, modules/, diagrams/, system-flows, module-layout) plus _docs/00_problem/ + _docs/01_solution/ + _docs/legacy/ + _docs/how_to_test + README. - Step 2 Architecture Baseline: architecture_compliance_baseline.md. - Step 3 Test Spec: _docs/02_document/tests/ (environment, test-data, blackbox/performance/resilience/security/ resource-limit tests, traceability-matrix), enum_spec_snapshot, expected_results/results_report.md (98 rows), plus the run-tests.sh + run-performance-tests.sh runners. - Step 4 Code Testability Revision: 01-testability-refactoring/ run dir (list-of-changes C01-C07, deferred_to_refactor, analysis/research_findings + refactoring_roadmap) and the 7 child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/ plus _dependencies_table.md. - _docs/_autodev_state.md pins the cursor at Step 4 / refactor Phase 4 entry so /autodev resumes cleanly. Epic AZ-447 (UI testability gates) tracks the 7 child tasks that will land in subsequent commits. Co-authored-by: Cursor <cursoragent@cursor.com>
4.7 KiB
Module: src/api/sse.ts
Source:
src/api/sse.ts(25 lines) Topo batch: B3 (depends on B2 leaf:api/clientforgetToken()only)
Purpose
A 25-line wrapper around the browser's native EventSource that (a) appends the current bearer token as an access_token query parameter (since EventSource does not let callers set headers), (b) parses each MessageEvent payload as JSON, and (c) returns a cleanup function. Used by features that listen for server-pushed updates (annotations queue, flight ingestion progress, etc.).
Public interface
export function createSSE<T>(
url: string,
onMessage: (data: T) => void,
onError?: (err: Event) => void,
): () => void
The returned function closes the underlying EventSource. Callers MUST call it on unmount to avoid leaking long-lived connections.
Internal logic
const token = getToken()— read the current bearer fromapi/client.ts.- Build
fullUrl:- If
tokenis non-null: appendaccess_token=<token>using&if the URL already has a query string,?otherwise. - If
tokenis null: useurlas-is.
- If
new EventSource(fullUrl).source.onmessage = (e) => { try { onMessage(JSON.parse(e.data) as T) } catch { /* ignore */ } }.- JSON parse errors are silently swallowed. The contract is that the server sends valid JSON; a malformed frame degrades to "skipped".
source.onerror = (e) => onError?.(e)— forwarded straight through. The browser auto-reconnects by default;onErrorlets callers observe the disconnect.- Return
() => source.close().
Dependencies
- Internal:
./client—getToken(). - External:
EventSource(browser global).
Consumers (intra-repo)
From the §7a dependency graph:
src/features/annotations/AnnotationsSidebar.tsx— subscribes to the annotations stream.src/features/flights/FlightsPage.tsx— subscribes to flight ingestion / state updates.
Data models
None defined here. The generic T is supplied by the caller.
Configuration
URLs are passed in by callers (string-literal at call sites). The same testability remark as api/client.ts applies: a VITE_API_BASE_URL is the natural Step 4 fix.
External integrations
Whichever backend exposes the SSE endpoint at the URL the caller provides. Per nginx.conf, the suite's /api/* reverse proxy forwards SSE traffic by default (no special EventSource-blocking config) — verify in Step 4.
Security
- Bearer in query string:
access_token=<jwt>ends up in browser-history URLs, server access logs, and proxy logs. This is a known weakness of the EventSource API — the API has no headers parameter, so cookie or query are the only options. The trade-off was made knowingly (SSE is a long-lived GET; the bearer is short-lived; nginx access logs are an internal-only artefact). Document insecurity_approach.md(Step 6) and consider rotating to a dedicated SSE-only short-lived token in Step 8. getToken()on connect, no refresh: if the bearer rotates mid-session (viaclient.ts's 401 retry path), theEventSourcekeeps using the old token and will eventually error. Callers must observeonErrorand reconnect. The current consumers (AnnotationsSidebar,FlightsPage) do NOT do this — they create the source once on mount. Flag for Step 4 / Step 8.- Silent JSON parse error: a single malformed frame is skipped without a
console.warn. Acceptable for production noise reduction but obscures real backend bugs in dev. Defer.
Tests
None.
Notes / open questions
- No reconnect / backoff logic — relies on the browser's built-in EventSource auto-reconnect. The default is "keep retrying"; on a flaky connection this may produce a tight loop with backend logs. Confirm acceptable in Step 6 against the
annotations/andflights/service rate-limit posture. - Cleanup on token-less call:
getToken()returningnullproduces an unauthenticatedEventSource. The backend should reject with401, theEventSourcethen errors, andonError?fires. The caller is expected to interpret this as "user logged out, stop subscribing". None of the current consumers do that explicitly; instead, theProtectedRoutegate preventsuseEffectfrom ever running while logged out, so the path is unreachable in practice. Document but no action needed. - The function is fully synchronous — does not return a
Promise. The connection is initiated on call but the first message may arrive later. Consumers must handle the "no messages yet" UI state. - The query-string token assembly does NOT URL-encode the token. JWTs are URL-safe Base64 by default and contain only
A–Z, a–z, 0–9, -, _, ., so this is safe for the current token shape. If the token format ever changes, addencodeURIComponent. Note for Step 8.