13 KiB
Task 1: Setup SignalR Client SDK
Task ID: TASK-001 Story: STORY-001 - SignalR Client Integration Sprint: Sprint 1 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
- Install @microsoft/signalr npm package
- Create SignalR service file structure
- Implement basic HubConnection setup
- Verify connection to backend hub
- Setup logging for development
Detailed Steps
Step 1: Install SignalR Client SDK (15 min)
# 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.jsoncontains@microsoft/signalr: ^8.0.0 - Run
npm list @microsoft/signalrto 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:
- src/services/signalr/config.ts:
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'
};
- src/services/signalr/types.ts:
// 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'
}
- src/utils/signalr-logger.ts:
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
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<void> {
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<void> {
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<void> {
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
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:
npm test -- SignalRService.test.ts
Step 5: Manual Testing (15 min)
- Update App Entry Point (
src/index.tsxorsrc/App.tsx):
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));
- Open Browser Console:
- Look for:
[SignalR] SignalR connected successfully - Verify:
[SignalR] Joined tenant group: <tenant-id>
- Test Reconnection:
- Open DevTools Network tab
- Throttle to "Offline"
- Wait 5 seconds
- Switch back to "Online"
- Verify:
[SignalR] Reconnected to SignalR
Acceptance Criteria
@microsoft/signalrpackage 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
- ✅ SignalR SDK installed
- ✅ Service files created (SignalRService.ts, config.ts, types.ts, logger.ts)
- ✅ Basic connection working
- ✅ Unit tests passing
- ✅ 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