/
/
/
1import type {
2 ApiResponse,
3 RecordingConfig,
4 RecordingStatus,
5 TopicsResponse,
6 RecordingsResponse,
7 ApiClientConfig,
8 BatchDeleteResponse
9} from './types';
10import { ApiError } from './types';
11
12class ApiClient {
13 private baseUrl: string;
14 private timeout: number;
15 private retryAttempts: number;
16 private retryDelay: number;
17
18 constructor(config: ApiClientConfig = {}) {
19 // Use the provided baseUrl or default based on environment
20 this.baseUrl = config.baseUrl || this.getDefaultBaseUrl();
21 this.timeout = config.timeout || 10000;
22 this.retryAttempts = config.retryAttempts || 3;
23 this.retryDelay = config.retryDelay || 1000;
24 }
25
26 private getDefaultBaseUrl(): string {
27 // In development, connect directly to backend
28 if (import.meta.env.DEV) {
29 return 'http://localhost:8080/api';
30 }
31 // In production, use nginx proxy
32 return '/api';
33 }
34
35 private async fetchWithRetry(
36 url: string,
37 options: RequestInit = {},
38 attempt: number = 1
39 ): Promise<Response> {
40 const controller = new AbortController();
41 const timeoutId = setTimeout(() => controller.abort(), this.timeout);
42
43 try {
44 const response = await fetch(url, {
45 ...options,
46 signal: controller.signal,
47 headers: {
48 'Content-Type': 'application/json',
49 ...options.headers,
50 },
51 });
52
53 clearTimeout(timeoutId);
54
55 if (!response.ok) {
56 throw new ApiError(
57 `HTTP ${response.status}: ${response.statusText}`,
58 response.status,
59 response
60 );
61 }
62
63 return response;
64 } catch (error) {
65 clearTimeout(timeoutId);
66
67 if (attempt < this.retryAttempts && this.shouldRetry(error)) {
68 await this.delay(this.retryDelay * attempt);
69 return this.fetchWithRetry(url, options, attempt + 1);
70 }
71
72 throw error;
73 }
74 }
75
76 private shouldRetry(error: any): boolean {
77 // Retry on network errors, timeouts, and 5xx status codes
78 return (
79 error.name === 'AbortError' ||
80 error.name === 'TypeError' ||
81 (error instanceof ApiError && typeof error.status === 'number' && error.status >= 500)
82 );
83 }
84
85 private delay(ms: number): Promise<void> {
86 return new Promise(resolve => setTimeout(resolve, ms));
87 }
88
89 private async parseResponse<T>(response: Response): Promise<T> {
90 const contentType = response.headers.get('content-type');
91
92 if (contentType && contentType.includes('application/json')) {
93 return response.json();
94 }
95
96 const text = await response.text();
97 try {
98 return JSON.parse(text);
99 } catch {
100 throw new ApiError('Invalid JSON response', response.status);
101 }
102 }
103
104 // Status API
105 async getStatus(): Promise<RecordingStatus> {
106 const response = await this.fetchWithRetry(`${this.baseUrl}/status`);
107 return this.parseResponse<RecordingStatus>(response);
108 }
109
110 // Topics API
111 async getTopics(): Promise<string[]> {
112 const response = await this.fetchWithRetry(`${this.baseUrl}/topics`);
113 const data = await this.parseResponse<TopicsResponse>(response);
114 return data.topics;
115 }
116
117 // Recording Control API
118 async startRecording(config: RecordingConfig): Promise<ApiResponse> {
119 const response = await this.fetchWithRetry(`${this.baseUrl}/recording/start`, {
120 method: 'POST',
121 body: JSON.stringify(config),
122 });
123 return this.parseResponse<ApiResponse>(response);
124 }
125
126 async stopRecording(): Promise<ApiResponse> {
127 const response = await this.fetchWithRetry(`${this.baseUrl}/recording/stop`, {
128 method: 'POST',
129 });
130 return this.parseResponse<ApiResponse>(response);
131 }
132
133 // Configuration API
134 async getConfig(): Promise<any> {
135 const response = await this.fetchWithRetry(`${this.baseUrl}/config`);
136 return this.parseResponse(response);
137 }
138
139 async setConfig(config: any): Promise<ApiResponse> {
140 const response = await this.fetchWithRetry(`${this.baseUrl}/config`, {
141 method: 'POST',
142 body: JSON.stringify(config),
143 });
144 return this.parseResponse<ApiResponse>(response);
145 }
146
147 // Recordings Management API
148 async getRecordings(): Promise<RecordingsResponse> {
149 const response = await this.fetchWithRetry(`${this.baseUrl}/recordings`);
150 return this.parseResponse<RecordingsResponse>(response);
151 }
152
153 async deleteRecording(filename: string): Promise<ApiResponse> {
154 const response = await this.fetchWithRetry(`${this.baseUrl}/recordings/${encodeURIComponent(filename)}`, {
155 method: 'DELETE',
156 });
157 return this.parseResponse<ApiResponse>(response);
158 }
159
160 // Batch Operations API
161 async batchDownloadRecordings(filenames: string[]): Promise<Blob> {
162 const response = await this.fetchWithRetry(`${this.baseUrl}/recordings/download`, {
163 method: 'POST',
164 body: JSON.stringify({ filenames }),
165 });
166
167 if (!response.ok) {
168 throw new ApiError(
169 `Failed to download recordings: ${response.statusText}`,
170 response.status,
171 response
172 );
173 }
174
175 return response.blob();
176 }
177
178 async batchDeleteRecordings(filenames: string[]): Promise<BatchDeleteResponse> {
179 const response = await this.fetchWithRetry(`${this.baseUrl}/recordings/delete`, {
180 method: 'POST',
181 body: JSON.stringify({ filenames }),
182 });
183 return this.parseResponse<BatchDeleteResponse>(response);
184 }
185
186 // Single file download API
187 async downloadRecording(filename: string): Promise<Blob> {
188 const response = await this.fetchWithRetry(`${this.baseUrl}/recordings/${encodeURIComponent(filename)}/download`, {
189 method: 'GET',
190 });
191
192 if (!response.ok) {
193 throw new ApiError(
194 `Failed to download recording: ${response.statusText}`,
195 response.status,
196 response
197 );
198 }
199
200 return response.blob();
201 }
202
203 // Helper method to trigger browser download from blob
204 static triggerDownload(blob: Blob, filename: string): void {
205 const url = URL.createObjectURL(blob);
206 const link = document.createElement('a');
207 link.href = url;
208 link.download = filename;
209 document.body.appendChild(link);
210 link.click();
211 document.body.removeChild(link);
212 URL.revokeObjectURL(url);
213 }
214
215 // Helper method to trigger download from URL (for batch downloads)
216 static triggerDownloadFromUrl(url: string, filename: string = 'recordings.zip'): void {
217 const link = document.createElement('a');
218 link.href = url;
219 link.download = filename;
220 link.target = '_blank';
221 document.body.appendChild(link);
222 link.click();
223 document.body.removeChild(link);
224 }
225
226 // Utility Methods
227 async healthCheck(): Promise<boolean> {
228 try {
229 await this.getStatus();
230 return true;
231 } catch {
232 return false;
233 }
234 }
235
236 setBaseUrl(baseUrl: string): void {
237 this.baseUrl = baseUrl;
238 }
239
240 setTimeout(timeout: number): void {
241 this.timeout = timeout;
242 }
243}
244
245// Create and export singleton instance
246export const apiClient = new ApiClient();
247
248// Export class for custom instances
249export { ApiClient };
250
251// Helper function for error handling
252export function isApiError(error: any): error is ApiError {
253 return error instanceof ApiError;
254}
255
256// Helper function to format file sizes
257export function formatFileSize(bytes: number): string {
258 const units = ['B', 'KB', 'MB', 'GB', 'TB'];
259 let size = bytes;
260 let unitIndex = 0;
261
262 while (size >= 1024 && unitIndex < units.length - 1) {
263 size /= 1024;
264 unitIndex++;
265 }
266
267 return `${size.toFixed(1)} ${units[unitIndex]}`;
268}
269
270// Helper function to format duration
271export function formatDuration(seconds: number): string {
272 const hours = Math.floor(seconds / 3600);
273 const minutes = Math.floor((seconds % 3600) / 60);
274 const secs = Math.floor(seconds % 60);
275
276 if (hours > 0) {
277 return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
278 }
279 return `${minutes}:${secs.toString().padStart(2, '0')}`;
280}