# Task 1: Setup SignalR Client SDK **Task ID**: TASK-001 **Story**: [STORY-001 - SignalR Client Integration](sprint_1_story_1.md) **Sprint**: [Sprint 1](sprint_1.md) **Estimated Hours**: 3h **Actual Hours**: _TBD_ **Assignee**: Frontend Developer 1 **Priority**: P0 (Must Have) **Status**: Not Started --- ## Task Description Install and configure the SignalR client SDK in the React application, set up project structure for SignalR services, and establish basic connection to backend hub. --- ## Objectives 1. Install @microsoft/signalr npm package 2. Create SignalR service file structure 3. Implement basic HubConnection setup 4. Verify connection to backend hub 5. Setup logging for development --- ## Detailed Steps ### Step 1: Install SignalR Client SDK (15 min) ```bash # Navigate to frontend project cd colaflow-frontend # Install SignalR client package npm install @microsoft/signalr@8.0.0 # Install TypeScript types (if not included) npm install --save-dev @types/microsoft__signalr ``` **Verification**: - Check `package.json` contains `@microsoft/signalr: ^8.0.0` - Run `npm list @microsoft/signalr` to verify installation --- ### Step 2: Create Project Structure (30 min) Create the following directory structure: ``` src/ ├── services/ │ └── signalr/ │ ├── SignalRService.ts # Main service class │ ├── SignalRContext.tsx # React context provider │ ├── types.ts # TypeScript interfaces │ └── config.ts # Configuration constants ├── hooks/ │ └── useSignalR.ts # Custom React hook └── utils/ └── signalr-logger.ts # Logging utility ``` **Files to Create**: 1. **src/services/signalr/config.ts**: ```typescript export const SIGNALR_CONFIG = { hubUrl: process.env.REACT_APP_SIGNALR_HUB_URL || 'https://localhost:5001/hubs/project', reconnectDelays: [1000, 2000, 4000, 8000, 16000], // Exponential backoff transport: 'WebSockets', // Prefer WebSockets over other transports logLevel: process.env.NODE_ENV === 'development' ? 'Information' : 'Warning' }; ``` 2. **src/services/signalr/types.ts**: ```typescript // Event payload types export interface ProjectEvent { projectId: string; projectName: string; tenantId: string; timestamp: string; } export interface EpicEvent { epicId: string; epicTitle: string; projectId: string; tenantId: string; timestamp: string; } export interface StoryEvent { storyId: string; storyTitle: string; epicId?: string; projectId: string; tenantId: string; timestamp: string; } export interface TaskEvent { taskId: string; taskTitle: string; storyId?: string; projectId: string; status?: string; tenantId: string; timestamp: string; } // Connection status export enum ConnectionStatus { Disconnected = 'Disconnected', Connecting = 'Connecting', Connected = 'Connected', Reconnecting = 'Reconnecting', Failed = 'Failed' } ``` 3. **src/utils/signalr-logger.ts**: ```typescript export class SignalRLogger { private isDev = process.env.NODE_ENV === 'development'; log(message: string, data?: any): void { if (this.isDev) { console.log(`[SignalR] ${message}`, data || ''); } } error(message: string, error?: any): void { console.error(`[SignalR Error] ${message}`, error || ''); } warn(message: string, data?: any): void { if (this.isDev) { console.warn(`[SignalR Warning] ${message}`, data || ''); } } } export const signalRLogger = new SignalRLogger(); ``` --- ### Step 3: Implement Basic SignalRService (1.5h) **File**: `src/services/signalr/SignalRService.ts` ```typescript import * as signalR from '@microsoft/signalr'; import { SIGNALR_CONFIG } from './config'; import { signalRLogger } from '../../utils/signalr-logger'; import { ConnectionStatus } from './types'; export class SignalRService { private connection: signalR.HubConnection | null = null; private connectionStatus: ConnectionStatus = ConnectionStatus.Disconnected; private statusChangeCallbacks: Array<(status: ConnectionStatus) => void> = []; /** * Initialize SignalR connection * @param accessToken JWT token for authentication * @param tenantId Current user's tenant ID */ async connect(accessToken: string, tenantId: string): Promise { if (this.connection) { signalRLogger.warn('Connection already exists. Disconnecting first...'); await this.disconnect(); } this.updateStatus(ConnectionStatus.Connecting); try { // Build connection this.connection = new signalR.HubConnectionBuilder() .withUrl(SIGNALR_CONFIG.hubUrl, { accessTokenFactory: () => accessToken, transport: signalR.HttpTransportType.WebSockets, skipNegotiation: true // We're forcing WebSockets }) .withAutomaticReconnect(SIGNALR_CONFIG.reconnectDelays) .configureLogging( process.env.NODE_ENV === 'development' ? signalR.LogLevel.Information : signalR.LogLevel.Warning ) .build(); // Setup connection lifecycle handlers this.setupConnectionHandlers(tenantId); // Start connection await this.connection.start(); signalRLogger.log('SignalR connected successfully'); this.updateStatus(ConnectionStatus.Connected); // Join tenant group await this.joinTenant(tenantId); } catch (error) { signalRLogger.error('Failed to connect to SignalR hub', error); this.updateStatus(ConnectionStatus.Failed); throw error; } } /** * Disconnect from SignalR hub */ async disconnect(): Promise { if (this.connection) { try { await this.connection.stop(); signalRLogger.log('SignalR disconnected'); } catch (error) { signalRLogger.error('Error during disconnect', error); } finally { this.connection = null; this.updateStatus(ConnectionStatus.Disconnected); } } } /** * Join tenant-specific group on the hub */ private async joinTenant(tenantId: string): Promise { if (!this.connection) { throw new Error('Connection not established'); } try { await this.connection.invoke('JoinTenant', tenantId); signalRLogger.log(`Joined tenant group: ${tenantId}`); } catch (error) { signalRLogger.error('Failed to join tenant group', error); throw error; } } /** * Setup connection lifecycle event handlers */ private setupConnectionHandlers(tenantId: string): void { if (!this.connection) return; // Handle reconnecting this.connection.onreconnecting((error) => { signalRLogger.warn('Connection lost. Reconnecting...', error); this.updateStatus(ConnectionStatus.Reconnecting); }); // Handle reconnected this.connection.onreconnected(async (connectionId) => { signalRLogger.log('Reconnected to SignalR', { connectionId }); this.updateStatus(ConnectionStatus.Connected); // Rejoin tenant group after reconnection try { await this.joinTenant(tenantId); } catch (error) { signalRLogger.error('Failed to rejoin tenant after reconnect', error); } }); // Handle connection closed this.connection.onclose((error) => { signalRLogger.error('Connection closed', error); this.updateStatus(ConnectionStatus.Disconnected); }); } /** * Get current connection status */ getStatus(): ConnectionStatus { return this.connectionStatus; } /** * Subscribe to connection status changes */ onStatusChange(callback: (status: ConnectionStatus) => void): () => void { this.statusChangeCallbacks.push(callback); // Return unsubscribe function return () => { const index = this.statusChangeCallbacks.indexOf(callback); if (index > -1) { this.statusChangeCallbacks.splice(index, 1); } }; } /** * Update connection status and notify subscribers */ private updateStatus(status: ConnectionStatus): void { this.connectionStatus = status; this.statusChangeCallbacks.forEach(callback => callback(status)); } /** * Get underlying HubConnection (for registering event handlers) */ getConnection(): signalR.HubConnection | null { return this.connection; } } // Singleton instance export const signalRService = new SignalRService(); ``` --- ### Step 4: Test Basic Connection (45 min) **Create Test File**: `src/services/signalr/__tests__/SignalRService.test.ts` ```typescript import { SignalRService } from '../SignalRService'; import { ConnectionStatus } from '../types'; // Mock SignalR jest.mock('@microsoft/signalr'); describe('SignalRService', () => { let service: SignalRService; const mockToken = 'mock-jwt-token'; const mockTenantId = 'tenant-123'; beforeEach(() => { service = new SignalRService(); }); afterEach(async () => { await service.disconnect(); }); test('should initialize with Disconnected status', () => { expect(service.getStatus()).toBe(ConnectionStatus.Disconnected); }); test('should connect successfully with valid token', async () => { await service.connect(mockToken, mockTenantId); expect(service.getStatus()).toBe(ConnectionStatus.Connected); }); test('should handle connection failure', async () => { // Mock connection failure const invalidToken = ''; await expect(service.connect(invalidToken, mockTenantId)) .rejects .toThrow(); expect(service.getStatus()).toBe(ConnectionStatus.Failed); }); test('should disconnect cleanly', async () => { await service.connect(mockToken, mockTenantId); await service.disconnect(); expect(service.getStatus()).toBe(ConnectionStatus.Disconnected); }); test('should notify status change subscribers', async () => { const statusChanges: ConnectionStatus[] = []; service.onStatusChange((status) => { statusChanges.push(status); }); await service.connect(mockToken, mockTenantId); expect(statusChanges).toContain(ConnectionStatus.Connecting); expect(statusChanges).toContain(ConnectionStatus.Connected); }); }); ``` **Run Tests**: ```bash npm test -- SignalRService.test.ts ``` --- ### Step 5: Manual Testing (15 min) 1. **Update App Entry Point** (`src/index.tsx` or `src/App.tsx`): ```typescript import { signalRService } from './services/signalr/SignalRService'; // For testing only - replace with actual auth token const testToken = 'your-test-jwt-token'; const testTenantId = 'your-test-tenant-id'; // Test connection on app load signalRService.connect(testToken, testTenantId) .then(() => console.log('✅ SignalR connected')) .catch(err => console.error('❌ SignalR connection failed:', err)); ``` 2. **Open Browser Console**: - Look for: `[SignalR] SignalR connected successfully` - Verify: `[SignalR] Joined tenant group: ` 3. **Test Reconnection**: - Open DevTools Network tab - Throttle to "Offline" - Wait 5 seconds - Switch back to "Online" - Verify: `[SignalR] Reconnected to SignalR` --- ## Acceptance Criteria - [ ] `@microsoft/signalr` package installed (version 8.0+) - [ ] Project structure created (5 files minimum) - [ ] SignalRService class implemented with: - [ ] connect() method - [ ] disconnect() method - [ ] Status management - [ ] Reconnection handling - [ ] Unit tests passing (5+ tests) - [ ] Manual test: Connection successful in browser console - [ ] Manual test: Reconnection works after network drop --- ## Deliverables 1. ✅ SignalR SDK installed 2. ✅ Service files created (SignalRService.ts, config.ts, types.ts, logger.ts) 3. ✅ Basic connection working 4. ✅ Unit tests passing 5. ✅ Code committed to feature branch --- ## Notes - Use WebSockets transport for best performance - JWT token must be valid and not expired - Backend hub must be running on configured URL - Test with actual backend, not mock --- ## Blockers - None (all dependencies available) --- **Status**: Completed **Created**: 2025-11-04 **Updated**: 2025-11-04 **Completed**: 2025-11-04 **Actual Hours**: 1h (estimated: 3h) **Efficiency**: 33% (significantly faster than estimated) --- ## Completion Summary **Status**: Completed **Completed Date**: 2025-11-04 **Actual Hours**: 1h (estimated: 3h) **Efficiency**: 33% (actual/estimated) **Deliverables**: - SignalR Client SDK (@microsoft/signalr@8.0.0) installed - Project structure created (lib/signalr/, lib/hooks/) - TypeScript type definitions (19 event types in lib/signalr/types.ts) - Connection management service (lib/hooks/useProjectHub.ts) - Basic connection verified and working **Git Commits**: - Frontend: 01132ee (SignalR Client Integration - 1053 lines) **Notes**: - Task completed significantly faster than estimated due to clear requirements - Actually implemented 19 event types instead of 13 (6 bonus event types added) - Connection management integrated with React hooks for better developer experience