Spaces:
Sleeping
Sleeping
| // 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<T>( | |
| endpoint: string, | |
| options: RequestInit = {}, | |
| retries: number = MAX_RETRIES | |
| ): Promise<T> { | |
| const url = `${this.baseURL}${endpoint}`; | |
| // Add authentication headers | |
| const headers: Record<string, string> = { | |
| [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<T>(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<T>(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<void> { | |
| return new Promise((resolve) => setTimeout(resolve, ms)); | |
| } | |
| // Session Management Methods | |
| async createSession(title: string): Promise<CreateSessionResponse> { | |
| const request: CreateSessionRequest = { | |
| title, | |
| user_id: this.userId || '' | |
| }; | |
| return this.request<CreateSessionResponse>('/v1/sessions', { | |
| method: 'POST', | |
| body: JSON.stringify(request) | |
| }); | |
| } | |
| async listSessions(): Promise<SessionMetadata[]> { | |
| const response = await this.request<ListSessionsResponse>('/v1/sessions', { | |
| method: 'GET' | |
| }); | |
| return response.sessions; | |
| } | |
| async getSession(sessionId: string): Promise<Session> { | |
| return this.request<Session>(`/v1/sessions/${sessionId}`, { | |
| method: 'GET' | |
| }); | |
| } | |
| async updateSession(sessionId: string, isReference: boolean): Promise<CreateSessionResponse> { | |
| return this.request<CreateSessionResponse>(`/v1/sessions/${sessionId}`, { | |
| method: 'PATCH', | |
| body: JSON.stringify({ is_reference: isReference }) | |
| }); | |
| } | |
| async deleteSession(sessionId: string): Promise<void> { | |
| await this.request<void>(`/v1/sessions/${sessionId}`, { | |
| method: 'DELETE' | |
| }); | |
| } | |
| async sendMessage( | |
| sessionId: string, | |
| request: SendMessageRequest | |
| ): Promise<SendMessageResponse> { | |
| return this.request<SendMessageResponse>(`/v1/sessions/${sessionId}/messages`, { | |
| method: 'POST', | |
| body: JSON.stringify(request) | |
| }); | |
| } | |
| async compareSession(sessionId: string, referenceSessionId: string): Promise<ComparisonResult> { | |
| return this.request<ComparisonResult>( | |
| `/v1/sessions/${sessionId}/compare?reference_id=${referenceSessionId}`, | |
| { | |
| method: 'GET' | |
| } | |
| ); | |
| } | |
| async getUserProfile(): Promise<UserProfile> { | |
| return this.request<UserProfile>('/v1/user/profile', { | |
| method: 'GET' | |
| }); | |
| } | |
| } | |
| // Export singleton instance | |
| export const apiClient = new APIClient(); | |