Files
ui/_docs/02_document/modules/src__api__sse.md
T
Oleksandr Bezdieniezhnykh 17d5bb45e7
ci/woodpecker/push/build-arm Pipeline was successful
[AZ-485] [AZ-486] Cycle 1 docs refresh (Step 13)
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>
2026-05-12 00:01:04 +03:00

5.0 KiB
Raw Blame History

Module: src/api/sse.ts

Source: src/api/sse.ts (25 lines) Topo batch: B3 (depends on B2 leaf: api/client for getToken() 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

  1. const token = getToken() — read the current bearer from api/client.ts.
  2. Build fullUrl:
    • If token is non-null: append access_token=<token> using & if the URL already has a query string, ? otherwise.
    • If token is null: use url as-is.
  3. new EventSource(fullUrl).
  4. 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".
  5. source.onerror = (e) => onError?.(e) — forwarded straight through. The browser auto-reconnects by default; onError lets callers observe the disconnect.
  6. Return () => source.close().

Dependencies

  • Internal: ./clientgetToken().
  • 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 in security_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 (via client.ts's 401 retry path), the EventSource keeps using the old token and will eventually error. Callers must observe onError and 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/ and flights/ service rate-limit posture.
  • Cleanup on token-less call: getToken() returning null produces an unauthenticated EventSource. The backend should reject with 401, the EventSource then errors, and onError? fires. The caller is expected to interpret this as "user logged out, stop subscribing". None of the current consumers do that explicitly; instead, the ProtectedRoute gate prevents useEffect from 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 AZ, az, 09, -, _, ., so this is safe for the current token shape. If the token format ever changes, add encodeURIComponent. Note for Step 8.