Add Phase 1E SignalR wrapper implementation plan
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user