Files
ColaFlow-Web/lib/signalr/ConnectionManager.ts
Yaojia Wang ea67d90880 fix(frontend): Fix critical type safety issues from code review
Address all Critical and High Priority issues identified in frontend code review report:

Critical Issues Fixed:
- Created unified logger utility (lib/utils/logger.ts) to replace all console.log statements
- Consolidated User type definitions - removed duplicate from authStore, using single source from types/user.ts
- Eliminated 'any' types in API client - added proper generic types with AxiosRequestConfig
- Fixed SignalR ConnectionManager - replaced 'any' with generic types <T>
- Created API error types (lib/types/errors.ts) with ApiError and getErrorMessage helper
- Fixed IssueCard component - removed all type assertions, created discriminated union types for Kanban items
- Added React.memo to IssueCard for performance optimization
- Added proper ARIA labels and accessibility attributes to IssueCard

High Priority Issues Fixed:
- Fixed hardcoded user ID in CreateProjectDialog - now uses actual user from authStore
- Added useCallback to CreateProjectDialog onSubmit handler
- Fixed error handlers in use-epics.ts - replaced 'any' with ApiError type
- Updated all error handling to use logger and getErrorMessage

Type Safety Improvements:
- Created KanbanItem discriminated union (KanbanEpic | KanbanStory | KanbanTask) with proper type guards
- Added 'never' types to prevent invalid property access
- Fixed User interface to include all required fields (createdAt, updatedAt)
- Maintained backward compatibility with LegacyKanbanBoard for existing code

Files Changed:
- lib/utils/logger.ts - New centralized logging utility
- lib/types/errors.ts - New API error types and helpers
- types/user.ts - Consolidated User type with TenantRole
- types/kanban.ts - New discriminated union types for type-safe Kanban items
- components/features/kanban/IssueCard.tsx - Type-safe with React.memo
- components/features/projects/CreateProjectDialog.tsx - Fixed hardcoded user ID, added useCallback
- lib/api/client.ts - Eliminated 'any', added proper generics
- lib/signalr/ConnectionManager.ts - Replaced console.log, added generics
- lib/hooks/use-epics.ts - Fixed error handler types
- stores/authStore.ts - Removed duplicate User type
- lib/hooks/useAuth.ts - Added createdAt field to User

TypeScript compilation:  All type checks passing (0 errors)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 19:11:48 +01:00

171 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as signalR from '@microsoft/signalr';
import { tokenManager } from '@/lib/api/client';
import { SIGNALR_CONFIG } from './config';
import { logger } from '@/lib/utils/logger';
export type ConnectionState =
| 'disconnected'
| 'connecting'
| 'connected'
| 'reconnecting';
export class SignalRConnectionManager {
private connection: signalR.HubConnection | null = null;
private hubUrl: string;
private reconnectAttempt = 0;
private stateListeners: Array<(state: ConnectionState) => void> = [];
constructor(hubUrl: string) {
this.hubUrl = hubUrl;
}
async start(): Promise<void> {
if (
this.connection &&
this.connection.state === signalR.HubConnectionState.Connected
) {
logger.debug('[SignalR] Already connected');
return;
}
const token = tokenManager.getAccessToken();
if (!token) {
logger.warn('[SignalR] No access token found, cannot connect');
return;
}
this.connection = new signalR.HubConnectionBuilder()
.withUrl(this.hubUrl, {
accessTokenFactory: () => token,
// 备用方案:使用 query stringWebSocket 升级需要)
// transport: signalR.HttpTransportType.WebSockets,
})
.configureLogging(
signalR.LogLevel[
SIGNALR_CONFIG.LOG_LEVEL as keyof typeof signalR.LogLevel
]
)
.withAutomaticReconnect(SIGNALR_CONFIG.RECONNECT_DELAYS)
.build();
this.setupConnectionHandlers();
try {
this.notifyStateChange('connecting');
await this.connection.start();
logger.info(`[SignalR] Connected to ${this.hubUrl}`);
this.notifyStateChange('connected');
this.reconnectAttempt = 0;
} catch (error) {
logger.error('[SignalR] Connection error', error);
this.notifyStateChange('disconnected');
this.scheduleReconnect();
}
}
async stop(): Promise<void> {
if (this.connection) {
await this.connection.stop();
this.connection = null;
this.notifyStateChange('disconnected');
logger.info('[SignalR] Disconnected');
}
}
on<T = unknown>(methodName: string, callback: (data: T) => void): void {
if (this.connection) {
this.connection.on(methodName, callback);
}
}
off<T = unknown>(methodName: string, callback?: (data: T) => void): void {
if (this.connection && callback) {
this.connection.off(methodName, callback);
} else if (this.connection) {
this.connection.off(methodName);
}
}
async invoke<T = unknown>(methodName: string, ...args: unknown[]): Promise<T> {
if (
!this.connection ||
this.connection.state !== signalR.HubConnectionState.Connected
) {
throw new Error('SignalR connection is not established');
}
return await this.connection.invoke<T>(methodName, ...args);
}
onStateChange(listener: (state: ConnectionState) => void): () => void {
this.stateListeners.push(listener);
// 返回 unsubscribe 函数
return () => {
this.stateListeners = this.stateListeners.filter((l) => l !== listener);
};
}
private setupConnectionHandlers(): void {
if (!this.connection) return;
this.connection.onclose((error) => {
logger.info('[SignalR] Connection closed', error);
this.notifyStateChange('disconnected');
this.scheduleReconnect();
});
this.connection.onreconnecting((error) => {
logger.info('[SignalR] Reconnecting...', error);
this.notifyStateChange('reconnecting');
});
this.connection.onreconnected((connectionId) => {
logger.info('[SignalR] Reconnected', connectionId);
this.notifyStateChange('connected');
this.reconnectAttempt = 0;
});
}
private scheduleReconnect(): void {
if (this.reconnectAttempt >= SIGNALR_CONFIG.RECONNECT_DELAYS.length) {
logger.error('[SignalR] Max reconnect attempts reached');
return;
}
const delay = SIGNALR_CONFIG.RECONNECT_DELAYS[this.reconnectAttempt];
this.reconnectAttempt++;
logger.info(
`[SignalR] Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempt})`
);
setTimeout(() => {
this.start();
}, delay);
}
private notifyStateChange(state: ConnectionState): void {
this.stateListeners.forEach((listener) => listener(state));
}
get connectionId(): string | null {
return this.connection?.connectionId ?? null;
}
get state(): ConnectionState {
if (!this.connection) return 'disconnected';
switch (this.connection.state) {
case signalR.HubConnectionState.Connected:
return 'connected';
case signalR.HubConnectionState.Connecting:
return 'connecting';
case signalR.HubConnectionState.Reconnecting:
return 'reconnecting';
default:
return 'disconnected';
}
}
}