Add trace files.
This commit is contained in:
499
docs/plans/sprint_1_story_1_task_1.md
Normal file
499
docs/plans/sprint_1_story_1_task_1.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# 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<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`
|
||||
|
||||
```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: <tenant-id>`
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user