A social knowledge tool for researchers built on ATProto
1import { ApiError, ApiErrorResponse } from '../types/errors';
2import { TokenManager } from '../../services/TokenManager';
3
4export abstract class BaseClient {
5 constructor(
6 protected baseUrl: string,
7 protected tokenManager?: TokenManager,
8 ) {}
9
10 protected async request<T>(
11 method: string,
12 endpoint: string,
13 data?: any,
14 ): Promise<T> {
15 const makeRequest = async (): Promise<T> => {
16 const url = `${this.baseUrl}${endpoint}`;
17 const token = this.tokenManager
18 ? await this.tokenManager.getAccessToken()
19 : null;
20
21 const headers: Record<string, string> = {
22 'Content-Type': 'application/json',
23 };
24
25 if (token) {
26 headers['Authorization'] = `Bearer ${token}`;
27 }
28
29 const config: RequestInit = {
30 method,
31 headers,
32 };
33
34 if (
35 data &&
36 (method === 'POST' || method === 'PUT' || method === 'PATCH')
37 ) {
38 config.body = JSON.stringify(data);
39 }
40
41 const response = await fetch(url, config);
42 return this.handleResponse<T>(response);
43 };
44
45 try {
46 return await makeRequest();
47 } catch (error) {
48 // Handle 401/403 errors with automatic token refresh (only if we have a token manager)
49 if (
50 this.tokenManager &&
51 error instanceof ApiError &&
52 (error.status === 401 || error.status === 403)
53 ) {
54 return this.tokenManager.handleAuthError(makeRequest);
55 }
56 throw error;
57 }
58 }
59
60 private async handleResponse<T>(response: Response): Promise<T> {
61 if (!response.ok) {
62 let errorData: ApiErrorResponse;
63
64 try {
65 errorData = await response.json();
66 } catch {
67 errorData = {
68 message: response.statusText || 'Unknown error',
69 };
70 }
71
72 throw new ApiError(
73 errorData.message,
74 response.status,
75 errorData.code,
76 errorData.details,
77 );
78 }
79
80 return response.json();
81 }
82}