plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
Showing only changes of commit b431010241 - Show all commits
@@ -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