Christian Kniep
new webapp
1fff71f
// 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();