Files
ui/_docs/02_document/modules/src__api__sse.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
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>
2026-05-11 00:38:49 +03:00

4.7 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 (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 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.