# Phase 1E -- SignalR wrapper contracts

## Goal

Deliver a reference-counted SignalR connection wrapper and a generic SSR-safe React hook (`useLiveFlights`) that subscribes to live flight data channels. All SignalR imports are dynamic so they never enter the SSR bundle.

## Deliverables

| File | Purpose |
|---|---|
| `src/shared/signalr/connection.ts` | `SignalRConnection` class, `getSharedConnection`, `ConnectionStatus` type, `HubOptions` interface |
| `src/shared/signalr/connection.test.ts` | Vitest tests for connection lifecycle, ref-counting, grace period, status changes |
| `src/shared/hooks/useLiveFlights.ts` | Generic live-data hook wrapping SignalR subscription |
| `src/shared/hooks/useLiveFlights.test.ts` | Vitest tests for hook behavior including SSR path |

## Tasks

### Task 1 -- Install `@microsoft/signalr`

Add `@microsoft/signalr` to the root `package.json` dependencies.

**Commit:** "Add @microsoft/signalr dependency"

### Task 2 -- `SignalRConnection` class + tests

Create `src/shared/signalr/connection.ts` with:

- `HubOptions` interface: `hubUrl`, `reconnectDelaysMs` (default `[0, 2000, 10000, 30000]`), `gracePeriodMs` (default `5000`)
- `ConnectionStatus` type: `"idle" | "connecting" | "live" | "reconnecting" | "offline"`
- `SignalRConnection` class:
  - Constructor takes `HubOptions`, uses dynamic `import("@microsoft/signalr")` to build connection
  - `subscribe(channel, handler)` returns unsubscribe fn; increments ref count; starts connection on first subscriber
  - `onStatusChange(handler)` returns unsubscribe fn
  - `get status()` returns current `ConnectionStatus`
  - Reference counting: connection closes after last unsubscribe + grace period elapses
- `getSharedConnection(options)` -- singleton map keyed by `hubUrl`

Create `src/shared/signalr/connection.test.ts` with tests:

1. Two rapid subscribes produce exactly one `HubConnection.start()` call
2. Unmount + remount within grace period reuses connection (no second `start()`)
3. Unmount + remount after grace period creates fresh connection
4. Status transitions fire `onStatusChange` handlers
5. `getSharedConnection` returns same instance for same `hubUrl`

**Commit:** "Add SignalRConnection ref-counted wrapper with tests"

### Task 3 -- `useLiveFlights` hook + tests

Create `src/shared/hooks/useLiveFlights.ts`:

- Generic `useLiveFlights<TParams, TData>(params, initialData, config)` hook
- SSR-safe: checks `typeof window !== "undefined"` before subscribing
- Uses `useEffect` for subscribe/unsubscribe lifecycle
- Returns `{ data: TData[]; connectionStatus: ConnectionStatus }`

Create `src/shared/hooks/useLiveFlights.test.ts`:

1. SSR path returns `initialData` without importing `@microsoft/signalr`
2. Client path subscribes to the correct channel
3. Data updates when messages arrive
4. Cleanup unsubscribes on unmount

**Commit:** "Add useLiveFlights SSR-safe hook with tests"

### Task 4 -- Verification

- `pnpm typecheck && pnpm lint && pnpm test`
- Confirm ESLint SSR-bundle guard still blocks `@microsoft/signalr` in `src/routes/`

**Commit:** none (verification only)

## Exit gate

- Two rapid `useEffect` mounts (Strict Mode simulation) result in exactly one `HubConnection.start()` call
- Unmount + remount within grace period reuses connection
- Unmount + remount after grace period creates fresh connection
- SSR render path does not import `@microsoft/signalr`
- `pnpm typecheck && pnpm lint && pnpm test` all green
