(
+ selector: (store: SseConnectionStore) => T
+): T => {
+ const context = useContext(SseConnectionStoreContext);
+
+ if (!context) {
+ throw new Error('useSseConnectionStore must be used within SseProvider');
+ }
+
+ return useStore(context, useShallow(selector));
+};
+```
+
+---
+
+### Phase 8: Integrate into Root Providers
+
+**File:** `src/components/providers.tsx`
+
+```typescript
+"use client";
+
+import { ReactNode } from 'react';
+import { AuthStoreProvider } from '@/lib/providers/auth-store-provider';
+import { OrganizationStateStoreProvider } from '@/lib/providers/organization-state-store-provider';
+import { CoachingRelationshipStateStoreProvider } from '@/lib/providers/coaching-relationship-state-store-provider';
+import { SessionCleanupProvider } from '@/lib/providers/session-cleanup-provider';
+import { SseProvider } from '@/lib/providers/sse-provider';
+import { SWRConfig } from 'swr';
+
+interface ProvidersProps {
+ children: ReactNode;
+}
+
+export function Providers({ children }: ProvidersProps) {
+ return (
+
+
+
+
+ new Map(),
+ }}
+ >
+
+ {children}
+
+
+
+
+
+
+ );
+}
+```
+
+---
+
+### Phase 9 (Optional): Connection Status UI
+
+**File:** `src/components/sse-connection-indicator.tsx`
+
+```typescript
+"use client";
+
+import { useSseConnectionStore } from '@/lib/providers/sse-provider';
+import { SseConnectionState } from '@/lib/stores/sse-connection-store';
+import { AlertCircle, Loader2, WifiOff } from 'lucide-react';
+
+export function SseConnectionIndicator() {
+ const { state, lastError } = useSseConnectionStore((store) => ({
+ state: store.state,
+ lastError: store.lastError,
+ }));
+
+ if (state === SseConnectionState.Connected) {
+ return null;
+ }
+
+ const getIcon = () => {
+ switch (state) {
+ case SseConnectionState.Connecting:
+ return ;
+ case SseConnectionState.Error:
+ return ;
+ case SseConnectionState.Disconnected:
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const getMessage = () => {
+ switch (state) {
+ case SseConnectionState.Connecting:
+ return 'Connecting to live updates...';
+ case SseConnectionState.Error:
+ return lastError?.message || 'Connection error';
+ case SseConnectionState.Disconnected:
+ return 'Disconnected from live updates';
+ default:
+ return '';
+ }
+ };
+
+ const getColorClass = () => {
+ switch (state) {
+ case SseConnectionState.Connecting:
+ return 'text-yellow-600 dark:text-yellow-400';
+ case SseConnectionState.Error:
+ return 'text-red-600 dark:text-red-400';
+ case SseConnectionState.Disconnected:
+ return 'text-gray-600 dark:text-gray-400';
+ default:
+ return '';
+ }
+ };
+
+ return (
+
+ {getIcon()}
+ {getMessage()}
+
+ );
+}
+```
+
+---
+
+## Testing Strategy
+
+### Unit Tests
+
+```typescript
+// src/lib/hooks/__tests__/use-sse-event-handler.test.ts
+import { renderHook } from '@testing-library/react';
+import { useSseEventHandler } from '../use-sse-event-handler';
+
+describe('useSseEventHandler', () => {
+ it('should handle action_created events with correct typing', () => {
+ const mockEventSource = new EventSource('mock-url');
+ const mockHandler = jest.fn();
+
+ renderHook(() =>
+ useSseEventHandler(mockEventSource, 'action_created', mockHandler)
+ );
+
+ const event = new MessageEvent('action_created', {
+ data: JSON.stringify({
+ type: 'action_created',
+ data: {
+ coaching_session_id: 'session-123',
+ action: { id: 'action-1' }
+ }
+ })
+ });
+
+ mockEventSource.dispatchEvent(event);
+
+ expect(mockHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'action_created',
+ data: expect.objectContaining({
+ coaching_session_id: 'session-123'
+ })
+ })
+ );
+ });
+});
+```
+
+### Integration Tests
+
+```typescript
+// Integration test: SSE event → cache invalidation → component update
+import { render, screen, waitFor } from '@testing-library/react';
+import { SseProvider } from '@/lib/providers/sse-provider';
+import { CoachingSessionPage } from '@/app/coaching-sessions/[id]/page';
+
+describe('SSE Integration', () => {
+ it('should update action list when action_created event received', async () => {
+ render(
+
+
+
+ );
+
+ expect(screen.queryByText('Test Action')).not.toBeInTheDocument();
+
+ const event = new MessageEvent('action_created', {
+ data: JSON.stringify({
+ type: 'action_created',
+ data: {
+ coaching_session_id: 'session-123',
+ action: { id: 'action-1', body: 'Test Action' }
+ }
+ })
+ });
+
+ window.EventSource.prototype.dispatchEvent(event);
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Action')).toBeInTheDocument();
+ });
+ });
+});
+```
+
+---
+
+## Security Considerations
+
+- SSE endpoint requires valid session cookie (handled by `withCredentials: true`)
+- Backend validates session via `AuthenticatedUser` extractor
+- Backend determines event recipients (not client-controlled)
+- Type guards validate event structure at runtime
+- HTTPS enforced in production (via nginx)
+- Session cookies are httpOnly and secure
+
+---
+
+## Performance Considerations
+
+- Single connection per user (not per component)
+- Minimal bandwidth (only events for this user)
+- Keep-alive messages every 15s prevent timeouts
+- Native browser reconnection prevents connection leaks
+- SWR deduplicates simultaneous cache invalidations
+- Broad cache invalidation leverages SWR's batching
+
+---
+
+## Debugging Guide
+
+### Check connection state:
+```typescript
+import { useSseConnectionStore } from '@/lib/providers/sse-provider';
+
+function DebugComponent() {
+ const connectionState = useSseConnectionStore((store) => ({
+ state: store.state,
+ lastError: store.lastError,
+ lastConnectedAt: store.lastConnectedAt,
+ lastEventAt: store.lastEventAt,
+ }));
+
+ return {JSON.stringify(connectionState, null, 2)};
+}
+```
+
+### Check Redux DevTools:
+1. Open Redux DevTools → `sse-connection-store`
+2. View connection state, errors, timestamps
+3. Monitor state transitions
+
+---
+
+## Summary
+
+This implementation provides a robust, type-safe, and maintainable SSE solution that:
+
+✅ Uses native browser APIs (no external dependencies)
+✅ Follows existing patterns (SWR, Zustand, hooks, providers, logout cleanup)
+✅ Requires zero component changes (automatic cache invalidation)
+✅ Strongly typed throughout (discriminated unions)
+✅ Handles failures gracefully (automatic reconnection)
+✅ Integrates seamlessly (with existing auth, routing, caching)
+✅ Production-ready (error tracking, debugging, monitoring)
+
+The key insight: **leverage SWR cache invalidation instead of manual state updates**. This is simpler, more reliable, and consistent with existing codebase patterns.