Phase B cycle 1 was a structural refactor only: F4 (barrel imports + STC-ARCH-01) and F7 (endpoint builders + STC-ARCH-02). This commit brings docs in line with source after the cycle, no code changes. Module docs (12 consumers): swap every /api/<service>/... literal in code snippets and integration tables for the matching endpoints.* builder; note the barrel import migration in Dependencies. New module doc: src__api__endpoints.md (public surface, F4 barrel re-export note, STC-ARCH-02 enforcement, contract-test reference). Architecture compliance baseline: mark F4 + F7 CLOSED with commit hashes (23746ec,8a461a2). 01_api-transport component description: add endpoints.ts + barrel to Internal Interfaces, close the F7 caveat, extend Module Inventory. ripple_log_cycle1.md: Task Step 0.5 reverse-dep analysis records the import-graph closure (no extra docs needed beyond the direct set). Carry-over reports landed alongside the docs: - test_run_report_phase_b_cycle1.md (Step 11 outcome) - implementation_report_refactor_phase_b_cycle1.md (cycle summary) State file: trimmed to the autodev <30-line target; Steps 14 + 15 recorded as SKIPPED with rationale (no security or perf surface changed in this cycle); pointer moved to Step 16 (Deploy). Co-authored-by: Cursor <cursoragent@cursor.com>
5.0 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. Since AZ-486 / F7 (commit 8a461a2), callers obtain those URLs from endpoints.* builders in src/api/endpoints.ts rather than from inline string literals. The STC-ARCH-02 static gate enforces this at every callsite under src/. createSSE itself is path-agnostic and takes any url — the endpoints discipline is upheld at the call site, not here.
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.