Skip to content

Commit 47dfb75

Browse files
authored
Merge pull request #134 from Fale1278/main
Fixed Client.ts
2 parents c1aae6e + 1de57fd commit 47dfb75

1 file changed

Lines changed: 80 additions & 63 deletions

File tree

frontend/lib/api/client.ts

Lines changed: 80 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// frontend/lib/api/client.ts
12
import {
23
OfflineActionQueuedError,
34
isLikelyOfflineError,
@@ -6,22 +7,30 @@ import {
67
} from '@/lib/offline';
78

89
const API_BASE_URL =
9-
process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001/api/v1';
10-
11-
const DEFAULT_RETRY_CONFIG: RetryConfig = {
12-
maxRetries: 3,
13-
baseDelay: 1000,
14-
maxDelay: 10000,
15-
jitter: true,
16-
};
10+
process.env.NEXT_PUBLIC_API_URL ||
11+
process.env.NEXT_PUBLIC_BACKEND_URL ||
12+
'http://localhost:3001/api/v1';
1713

14+
/* =====================================================
15+
Retry Configuration
16+
===================================================== */
1817
export interface RetryConfig {
1918
maxRetries: number;
2019
baseDelay: number;
2120
maxDelay: number;
2221
jitter: boolean;
2322
}
2423

24+
const DEFAULT_RETRY_CONFIG: RetryConfig = {
25+
maxRetries: 3,
26+
baseDelay: 1000,
27+
maxDelay: 10000,
28+
jitter: true,
29+
};
30+
31+
/* =====================================================
32+
Custom API Error
33+
===================================================== */
2534
export class ApiError extends Error {
2635
status: number;
2736
response: Response;
@@ -36,29 +45,32 @@ export class ApiError extends Error {
3645
}
3746
}
3847

48+
/** Type guard for UI handling */
49+
export function isApiError(error: unknown): error is ApiError {
50+
return error instanceof ApiError;
51+
}
52+
53+
/* =====================================================
54+
URL Resolver
55+
===================================================== */
3956
export function resolveApiUrl(endpoint: string) {
4057
return `${API_BASE_URL}${endpoint}`;
4158
}
4259

60+
/* =====================================================
61+
Retry Helpers
62+
===================================================== */
4363
function shouldRetryStatus(status: number): boolean {
4464
return status >= 500 || status === 429;
4565
}
4666

4767
function shouldRetryError(error: unknown): boolean {
48-
if (error instanceof ApiError) {
49-
return shouldRetryStatus(error.status);
50-
}
68+
if (error instanceof ApiError) return shouldRetryStatus(error.status);
5169

52-
if (
53-
(error instanceof Error && error.name === 'AbortError') ||
54-
(typeof error === 'object' &&
55-
error !== null &&
56-
'name' in error &&
57-
error.name === 'AbortError')
58-
) {
59-
return false;
60-
}
70+
// Abort should NOT retry
71+
if (error instanceof Error && error.name === 'AbortError') return false;
6172

73+
// Network errors → retry
6274
return true;
6375
}
6476

@@ -68,43 +80,35 @@ function calculateDelay(attempt: number, config: RetryConfig): number {
6880
return config.jitter ? delay * (0.5 + Math.random()) : delay;
6981
}
7082

71-
function delay(ms: number): Promise<void> {
72-
return new Promise((resolve) => setTimeout(resolve, ms));
73-
}
83+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
7484

85+
/* =====================================================
86+
Error Normalization
87+
===================================================== */
7588
function normalizeError(error: unknown): Error {
76-
if (error instanceof Error) {
77-
return error;
78-
}
89+
if (error instanceof Error) return error;
7990

8091
const message =
81-
typeof error === 'object' &&
82-
error !== null &&
83-
'message' in error &&
84-
typeof error.message === 'string'
85-
? error.message
92+
typeof error === 'object' && error !== null && 'message' in error && typeof (error as any).message === 'string'
93+
? (error as any).message
8694
: String(error);
8795

8896
const normalized = new Error(message);
8997

90-
if (
91-
typeof error === 'object' &&
92-
error !== null &&
93-
'name' in error &&
94-
typeof error.name === 'string'
95-
) {
96-
normalized.name = error.name;
98+
if (typeof error === 'object' && error !== null && 'name' in error && typeof (error as any).name === 'string') {
99+
normalized.name = (error as any).name;
97100
}
98101

99102
return normalized;
100103
}
101104

105+
/* =====================================================
106+
Response Parsing
107+
===================================================== */
102108
async function parseResponseBody(response: Response): Promise<unknown> {
103109
const contentType = response.headers.get('content-type') || '';
104110

105-
if (response.status === 204) {
106-
return null;
107-
}
111+
if (response.status === 204) return null;
108112

109113
if (contentType.includes('application/json')) {
110114
return response.json();
@@ -114,18 +118,35 @@ async function parseResponseBody(response: Response): Promise<unknown> {
114118
return text.length > 0 ? text : null;
115119
}
116120

117-
function getErrorMessage(statusText: string, data: unknown): string {
118-
if (data && typeof data === 'object' && 'message' in data && typeof data.message === 'string') {
119-
return data.message;
121+
/* =====================================================
122+
Friendly Error Messages
123+
===================================================== */
124+
function getErrorMessage(status: number, statusText: string, data: unknown): string {
125+
if (data && typeof data === 'object' && 'message' in data && typeof (data as any).message === 'string') {
126+
return (data as any).message;
120127
}
121128

122-
if (typeof data === 'string' && data.trim().length > 0) {
123-
return data;
129+
switch (status) {
130+
case 400:
131+
return 'Invalid request. Please check your input.';
132+
case 401:
133+
return 'You are not authenticated. Please login again.';
134+
case 403:
135+
return 'You do not have permission to perform this action.';
136+
case 404:
137+
return 'Requested resource was not found.';
138+
case 429:
139+
return 'Too many requests. Please try again shortly.';
140+
case 500:
141+
return 'Server error. Please try again later.';
142+
default:
143+
return `Request failed: ${statusText}`;
124144
}
125-
126-
return `API Error: ${statusText}`;
127145
}
128146

147+
/* =====================================================
148+
Main API Call
149+
===================================================== */
129150
export async function apiCall<T = unknown>(
130151
endpoint: string,
131152
options: RequestInit = {},
@@ -135,6 +156,7 @@ export async function apiCall<T = unknown>(
135156
let lastError: Error | undefined;
136157
const shouldQueue = shouldQueueRequest(options);
137158

159+
// Queue immediately if offline
138160
if (shouldQueue && typeof navigator !== 'undefined' && navigator.onLine === false) {
139161
const action = queueOfflineAction(endpoint, options);
140162
throw new OfflineActionQueuedError(
@@ -150,41 +172,36 @@ export async function apiCall<T = unknown>(
150172
...options,
151173
headers: {
152174
'Content-Type': 'application/json',
153-
...options.headers,
175+
...(options.headers || {}),
154176
},
155177
});
156178

157179
const data = await parseResponseBody(response);
158180

159-
if (response.ok) {
160-
return data as T;
161-
}
181+
if (response.ok) return data as T;
162182

163-
throw new ApiError(
164-
getErrorMessage(response.statusText, data),
165-
response.status,
166-
response,
167-
data
168-
);
183+
throw new ApiError(getErrorMessage(response.status, response.statusText, data), response.status, response, data);
169184
} catch (error) {
170185
lastError = normalizeError(error);
171186

187+
// Debug logging
188+
console.error('[API ERROR]', { endpoint, attempt, error: lastError });
189+
190+
// Queue request if offline
172191
if (shouldQueue && isLikelyOfflineError(lastError)) {
173192
const action = queueOfflineAction(endpoint, options);
174193
throw new OfflineActionQueuedError(
175-
'The request was queued because the network is unavailable.',
194+
'Network unavailable. Request queued.',
176195
endpoint,
177196
action.id
178197
);
179198
}
180199

181-
if (attempt === config.maxRetries || !shouldRetryError(error)) {
182-
throw lastError;
183-
}
200+
if (attempt === config.maxRetries || !shouldRetryError(error)) throw lastError;
184201

185202
await delay(calculateDelay(attempt, config));
186203
}
187204
}
188205

189206
throw lastError || new Error('API call failed after retries');
190-
}
207+
}

0 commit comments

Comments
 (0)