Files
ColaFlow/docs/plans/sprint_1_story_1_task_1.md
Yaojia Wang 08b317e789
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Add trace files.
2025-11-04 23:28:56 +01:00

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

  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)

# 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:
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'
};
  1. 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'
}
  1. 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)

  1. Update App Entry Point (src/index.tsx or src/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));
  1. Open Browser Console:
  • Look for: [SignalR] SignalR connected successfully
  • Verify: [SignalR] Joined tenant group: <tenant-id>
  1. 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