// Base API client with fetch wrapper import { API_URL, API_TIMEOUT, HEADER_AUTHORIZATION, HEADER_USER_ID, HEADER_CONTENT_TYPE, MAX_RETRIES, RETRY_DELAY } from '../utils/constants'; import type { Session, SessionMetadata, ListSessionsResponse, CreateSessionRequest, CreateSessionResponse, SendMessageRequest, SendMessageResponse, UserProfile, APIError, ComparisonResult } from '../types/api'; import type { ClientError } from '../types/client'; class APIClient { private baseURL: string; private token: string | null = null; private userId: string | null = null; constructor(baseURL: string = API_URL) { this.baseURL = baseURL; } /** * Set authentication token and user ID */ setAuth(token: string, userId: string): void { this.token = token; this.userId = userId; } /** * Clear authentication */ clearAuth(): void { this.token = null; this.userId = null; } /** * Make HTTP request with retry logic and timeout */ private async request( endpoint: string, options: RequestInit = {}, retries: number = MAX_RETRIES ): Promise { const url = `${this.baseURL}${endpoint}`; // Add authentication headers const headers: Record = { [HEADER_CONTENT_TYPE]: 'application/json' }; // Merge existing headers if (options.headers) { Object.entries(options.headers).forEach(([key, value]) => { if (typeof value === 'string') { headers[key] = value; } }); } if (this.token) { headers[HEADER_AUTHORIZATION] = `Bearer ${this.token}`; } if (this.userId && !endpoint.includes('/user/profile')) { headers[HEADER_USER_ID] = this.userId; } // Create abort controller for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT); try { const response = await fetch(url, { ...options, headers, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const error: APIError = await response.json(); throw this.createClientError(error, response.status, false); } // Handle 204 No Content if (response.status === 204) { return {} as T; } return await response.json(); } catch (error) { clearTimeout(timeoutId); // Handle abort/timeout if (error instanceof Error && error.name === 'AbortError') { if (retries > 0) { await this.delay(RETRY_DELAY); return this.request(endpoint, options, retries - 1); } throw this.createClientError( { error: 'Request timeout', status: 408 }, 408, true ); } // Handle network errors if (error instanceof TypeError) { if (retries > 0) { await this.delay(RETRY_DELAY); return this.request(endpoint, options, retries - 1); } throw this.createClientError( { error: 'Network error', status: 0 }, 0, true ); } // Re-throw client errors if (this.isClientError(error)) { throw error; } // Unknown error throw this.createClientError({ error: String(error), status: 500 }, 500, false); } } /** * Create typed client error */ private createClientError(apiError: APIError, status: number, retryable: boolean): ClientError { let type: ClientError['type'] = 'unknown'; if (status === 401 || status === 403) { type = 'auth'; } else if (status >= 400 && status < 500) { type = 'validation'; } else if (status >= 500) { type = 'server'; } else if (status === 0 || status === 408) { type = 'network'; } return { message: apiError.error, type, cause: apiError, retryable }; } /** * Type guard for ClientError */ private isClientError(error: unknown): error is ClientError { return ( typeof error === 'object' && error !== null && 'message' in error && 'type' in error && 'retryable' in error ); } /** * Delay utility for retry logic */ private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } // Session Management Methods async createSession(title: string): Promise { const request: CreateSessionRequest = { title, user_id: this.userId || '' }; return this.request('/v1/sessions', { method: 'POST', body: JSON.stringify(request) }); } async listSessions(): Promise { const response = await this.request('/v1/sessions', { method: 'GET' }); return response.sessions; } async getSession(sessionId: string): Promise { return this.request(`/v1/sessions/${sessionId}`, { method: 'GET' }); } async updateSession(sessionId: string, isReference: boolean): Promise { return this.request(`/v1/sessions/${sessionId}`, { method: 'PATCH', body: JSON.stringify({ is_reference: isReference }) }); } async deleteSession(sessionId: string): Promise { await this.request(`/v1/sessions/${sessionId}`, { method: 'DELETE' }); } async sendMessage( sessionId: string, request: SendMessageRequest ): Promise { return this.request(`/v1/sessions/${sessionId}/messages`, { method: 'POST', body: JSON.stringify(request) }); } async compareSession(sessionId: string, referenceSessionId: string): Promise { return this.request( `/v1/sessions/${sessionId}/compare?reference_id=${referenceSessionId}`, { method: 'GET' } ); } async getUserProfile(): Promise { return this.request('/v1/user/profile', { method: 'GET' }); } } // Export singleton instance export const apiClient = new APIClient();