@oxyhq/services 5.13.24 → 5.13.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/core/HttpService.js +481 -0
- package/lib/commonjs/core/HttpService.js.map +1 -0
- package/lib/commonjs/core/OxyServices.base.js +29 -26
- package/lib/commonjs/core/OxyServices.base.js.map +1 -1
- package/lib/commonjs/core/OxyServices.js +1 -2
- package/lib/commonjs/core/OxyServices.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.assets.js +3 -2
- package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.user.js +9 -5
- package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.utility.js +1 -0
- package/lib/commonjs/core/mixins/OxyServices.utility.js.map +1 -1
- package/lib/commonjs/ui/components/FollowButton.js +0 -53
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/utils/asyncUtils.js +23 -3
- package/lib/commonjs/utils/asyncUtils.js.map +1 -1
- package/lib/commonjs/utils/errorUtils.js +48 -33
- package/lib/commonjs/utils/errorUtils.js.map +1 -1
- package/lib/commonjs/utils/validationUtils.js +15 -0
- package/lib/commonjs/utils/validationUtils.js.map +1 -1
- package/lib/module/core/HttpService.js +476 -0
- package/lib/module/core/HttpService.js.map +1 -0
- package/lib/module/core/OxyServices.base.js +29 -26
- package/lib/module/core/OxyServices.base.js.map +1 -1
- package/lib/module/core/OxyServices.js +1 -2
- package/lib/module/core/OxyServices.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.assets.js +3 -2
- package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.user.js +9 -5
- package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.utility.js +1 -0
- package/lib/module/core/mixins/OxyServices.utility.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +1 -54
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/utils/asyncUtils.js +24 -3
- package/lib/module/utils/asyncUtils.js.map +1 -1
- package/lib/module/utils/errorUtils.js +43 -33
- package/lib/module/utils/errorUtils.js.map +1 -1
- package/lib/module/utils/validationUtils.js +14 -0
- package/lib/module/utils/validationUtils.js.map +1 -1
- package/lib/typescript/core/HttpService.d.ts +111 -0
- package/lib/typescript/core/HttpService.d.ts.map +1 -0
- package/lib/typescript/core/OxyServices.base.d.ts +6 -8
- package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
- package/lib/typescript/core/OxyServices.d.ts +1 -2
- package/lib/typescript/core/OxyServices.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.analytics.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.analytics.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.developer.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.developer.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.karma.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.karma.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.language.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.language.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.location.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.location.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.payment.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.payment.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.privacy.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.privacy.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.user.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.utility.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.utility.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +52 -65
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/utils/asyncUtils.d.ts +4 -0
- package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
- package/lib/typescript/utils/errorUtils.d.ts +9 -1
- package/lib/typescript/utils/errorUtils.d.ts.map +1 -1
- package/lib/typescript/utils/validationUtils.d.ts +6 -0
- package/lib/typescript/utils/validationUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/HttpService.ts +523 -0
- package/src/core/OxyServices.base.ts +36 -34
- package/src/core/OxyServices.ts +1 -2
- package/src/core/mixins/OxyServices.assets.ts +2 -1
- package/src/core/mixins/OxyServices.user.ts +7 -6
- package/src/core/mixins/OxyServices.utility.ts +1 -0
- package/src/ui/components/FollowButton.tsx +0 -53
- package/src/utils/asyncUtils.ts +23 -3
- package/src/utils/errorUtils.ts +72 -45
- package/src/utils/validationUtils.ts +14 -0
- package/lib/commonjs/core/HttpClient.js +0 -317
- package/lib/commonjs/core/HttpClient.js.map +0 -1
- package/lib/commonjs/core/RequestManager.js +0 -199
- package/lib/commonjs/core/RequestManager.js.map +0 -1
- package/lib/module/core/HttpClient.js +0 -311
- package/lib/module/core/HttpClient.js.map +0 -1
- package/lib/module/core/RequestManager.js +0 -194
- package/lib/module/core/RequestManager.js.map +0 -1
- package/lib/typescript/core/HttpClient.d.ts +0 -110
- package/lib/typescript/core/HttpClient.d.ts.map +0 -1
- package/lib/typescript/core/RequestManager.d.ts +0 -67
- package/lib/typescript/core/RequestManager.d.ts.map +0 -1
- package/src/core/HttpClient.ts +0 -346
- package/src/core/RequestManager.ts +0 -240
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
import { jwtDecode } from 'jwt-decode';
|
|
7
7
|
import type { OxyConfig as OxyConfigBase, ApiError, User } from '../models/interfaces';
|
|
8
8
|
import { handleHttpError } from '../utils/errorUtils';
|
|
9
|
-
import {
|
|
10
|
-
import { RequestManager, type RequestOptions } from './RequestManager';
|
|
9
|
+
import { HttpService, type RequestOptions } from './HttpService';
|
|
11
10
|
import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
|
|
12
11
|
|
|
13
12
|
export interface OxyConfig extends OxyConfigBase {
|
|
@@ -26,8 +25,7 @@ interface JwtPayload {
|
|
|
26
25
|
* Base class for OxyServices with core infrastructure
|
|
27
26
|
*/
|
|
28
27
|
export class OxyServicesBase {
|
|
29
|
-
public
|
|
30
|
-
public requestManager: RequestManager;
|
|
28
|
+
public httpService: HttpService;
|
|
31
29
|
public cloudURL: string;
|
|
32
30
|
public config: OxyConfig;
|
|
33
31
|
|
|
@@ -39,16 +37,13 @@ export class OxyServicesBase {
|
|
|
39
37
|
this.config = config;
|
|
40
38
|
this.cloudURL = config.cloudURL || 'https://cloud.oxy.so';
|
|
41
39
|
|
|
42
|
-
// Initialize HTTP
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
// Initialize request manager (handles caching, deduplication, queuing, retry)
|
|
46
|
-
this.requestManager = new RequestManager(this.httpClient, config);
|
|
40
|
+
// Initialize unified HTTP service (handles auth, caching, deduplication, queuing, retry)
|
|
41
|
+
this.httpService = new HttpService(config);
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
// Test-only utility to reset global tokens between jest tests
|
|
50
45
|
static __resetTokensForTests(): void {
|
|
51
|
-
|
|
46
|
+
HttpService.__resetTokensForTests();
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
/**
|
|
@@ -61,7 +56,13 @@ export class OxyServicesBase {
|
|
|
61
56
|
data?: any,
|
|
62
57
|
options: RequestOptions = {}
|
|
63
58
|
): Promise<T> {
|
|
64
|
-
return this.
|
|
59
|
+
return this.httpService.request<T>({
|
|
60
|
+
method,
|
|
61
|
+
url,
|
|
62
|
+
data: method !== 'GET' ? data : undefined,
|
|
63
|
+
params: method === 'GET' ? data : undefined,
|
|
64
|
+
...options,
|
|
65
|
+
});
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// ============================================================================
|
|
@@ -72,43 +73,43 @@ export class OxyServicesBase {
|
|
|
72
73
|
* Get the configured Oxy API base URL
|
|
73
74
|
*/
|
|
74
75
|
public getBaseURL(): string {
|
|
75
|
-
return this.
|
|
76
|
+
return this.httpService.getBaseURL();
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* Get the HTTP
|
|
80
|
-
* Useful for advanced use cases where direct access to the HTTP
|
|
80
|
+
* Get the HTTP service instance
|
|
81
|
+
* Useful for advanced use cases where direct access to the HTTP service is needed
|
|
81
82
|
*/
|
|
82
|
-
public getClient():
|
|
83
|
-
return this.
|
|
83
|
+
public getClient(): HttpService {
|
|
84
|
+
return this.httpService;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
/**
|
|
87
88
|
* Get performance metrics
|
|
88
89
|
*/
|
|
89
90
|
public getMetrics() {
|
|
90
|
-
return this.
|
|
91
|
+
return this.httpService.getMetrics();
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* Clear request cache
|
|
95
96
|
*/
|
|
96
97
|
public clearCache(): void {
|
|
97
|
-
this.
|
|
98
|
+
this.httpService.clearCache();
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
102
|
* Clear specific cache entry
|
|
102
103
|
*/
|
|
103
104
|
public clearCacheEntry(key: string): void {
|
|
104
|
-
this.
|
|
105
|
+
this.httpService.clearCacheEntry(key);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
109
|
* Get cache statistics
|
|
109
110
|
*/
|
|
110
111
|
public getCacheStats() {
|
|
111
|
-
return this.
|
|
112
|
+
return this.httpService.getCacheStats();
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/**
|
|
@@ -122,21 +123,21 @@ export class OxyServicesBase {
|
|
|
122
123
|
* Set authentication tokens
|
|
123
124
|
*/
|
|
124
125
|
public setTokens(accessToken: string, refreshToken = ''): void {
|
|
125
|
-
this.
|
|
126
|
+
this.httpService.setTokens(accessToken, refreshToken);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
130
|
* Clear stored authentication tokens
|
|
130
131
|
*/
|
|
131
132
|
public clearTokens(): void {
|
|
132
|
-
this.
|
|
133
|
+
this.httpService.clearTokens();
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
137
|
* Get the current user ID from the access token
|
|
137
138
|
*/
|
|
138
139
|
public getCurrentUserId(): string | null {
|
|
139
|
-
const accessToken = this.
|
|
140
|
+
const accessToken = this.httpService.getAccessToken();
|
|
140
141
|
if (!accessToken) {
|
|
141
142
|
return null;
|
|
142
143
|
}
|
|
@@ -153,14 +154,14 @@ export class OxyServicesBase {
|
|
|
153
154
|
* Check if the client has a valid access token (public method)
|
|
154
155
|
*/
|
|
155
156
|
public hasValidToken(): boolean {
|
|
156
|
-
return this.
|
|
157
|
+
return this.httpService.hasAccessToken();
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
161
|
* Get the raw access token (for constructing anchor URLs when needed)
|
|
161
162
|
*/
|
|
162
163
|
public getAccessToken(): string | null {
|
|
163
|
-
return this.
|
|
164
|
+
return this.httpService.getAccessToken();
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
/**
|
|
@@ -183,7 +184,7 @@ export class OxyServicesBase {
|
|
|
183
184
|
*/
|
|
184
185
|
public async waitForAuth(timeoutMs = 5000): Promise<boolean> {
|
|
185
186
|
// Immediate synchronous check - no delay if token is ready
|
|
186
|
-
if (this.
|
|
187
|
+
if (this.httpService.hasAccessToken()) {
|
|
187
188
|
return true;
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -196,7 +197,7 @@ export class OxyServicesBase {
|
|
|
196
197
|
while (performance.now() < maxTime) {
|
|
197
198
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
198
199
|
|
|
199
|
-
if (this.
|
|
200
|
+
if (this.httpService.hasAccessToken()) {
|
|
200
201
|
return true;
|
|
201
202
|
}
|
|
202
203
|
|
|
@@ -232,7 +233,7 @@ export class OxyServicesBase {
|
|
|
232
233
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
233
234
|
try {
|
|
234
235
|
// First attempt: check if we have a token
|
|
235
|
-
if (!this.
|
|
236
|
+
if (!this.httpService.hasAccessToken()) {
|
|
236
237
|
if (attempt === 0) {
|
|
237
238
|
// On first attempt, wait briefly for authentication to complete
|
|
238
239
|
const authReady = await this.waitForAuth(authTimeoutMs);
|
|
@@ -252,11 +253,12 @@ export class OxyServicesBase {
|
|
|
252
253
|
// Execute the operation
|
|
253
254
|
return await operation();
|
|
254
255
|
|
|
255
|
-
} catch (error:
|
|
256
|
+
} catch (error: unknown) {
|
|
256
257
|
const isLastAttempt = attempt === maxRetries;
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
const errorObj = error && typeof error === 'object' ? error as { response?: { status?: number }; code?: string; message?: string } : null;
|
|
259
|
+
const isAuthError = errorObj?.response?.status === 401 ||
|
|
260
|
+
errorObj?.code === 'MISSING_TOKEN' ||
|
|
261
|
+
errorObj?.message?.includes('Authentication') ||
|
|
260
262
|
error instanceof OxyAuthenticationError;
|
|
261
263
|
|
|
262
264
|
if (isAuthError && !isLastAttempt && !(error instanceof OxyAuthenticationTimeoutError)) {
|
|
@@ -298,12 +300,12 @@ export class OxyServicesBase {
|
|
|
298
300
|
/**
|
|
299
301
|
* Centralized error handling
|
|
300
302
|
*/
|
|
301
|
-
public handleError(error:
|
|
303
|
+
public handleError(error: unknown): Error {
|
|
302
304
|
const api = handleHttpError(error);
|
|
303
305
|
const err = new Error(api.message) as Error & { code?: string; status?: number; details?: Record<string, unknown> };
|
|
304
306
|
err.code = api.code;
|
|
305
307
|
err.status = api.status;
|
|
306
|
-
err.details = api.details
|
|
308
|
+
err.details = api.details;
|
|
307
309
|
return err;
|
|
308
310
|
}
|
|
309
311
|
|
package/src/core/OxyServices.ts
CHANGED
|
@@ -68,8 +68,7 @@ import { composeOxyServices } from './mixins';
|
|
|
68
68
|
* This class provides all API functionality in one simple, easy-to-use interface.
|
|
69
69
|
*
|
|
70
70
|
* ## Architecture
|
|
71
|
-
* - **
|
|
72
|
-
* - **RequestManager**: Handles caching, deduplication, queuing, and retry
|
|
71
|
+
* - **HttpService**: Unified HTTP service handling authentication, caching, deduplication, queuing, and retry
|
|
73
72
|
* - **OxyServices**: Provides high-level API methods
|
|
74
73
|
*
|
|
75
74
|
* ## Mixin Composition
|
|
@@ -235,11 +235,12 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
|
|
|
235
235
|
// Fallback: direct upload via API to avoid CORS issues
|
|
236
236
|
const fd = new FormData();
|
|
237
237
|
fd.append('file', file);
|
|
238
|
-
// Use
|
|
238
|
+
// Use httpService directly for FormData uploads (bypasses caching for special handling)
|
|
239
239
|
await this.getClient().request({
|
|
240
240
|
method: 'POST',
|
|
241
241
|
url: `/api/assets/${encodeURIComponent(initResponse.fileId)}/upload-direct`,
|
|
242
242
|
data: fd,
|
|
243
|
+
cache: false,
|
|
243
244
|
});
|
|
244
245
|
}
|
|
245
246
|
|
|
@@ -189,14 +189,15 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
189
189
|
*/
|
|
190
190
|
async downloadAccountData(format: 'json' | 'csv' = 'json'): Promise<Blob> {
|
|
191
191
|
try {
|
|
192
|
-
// Use
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
// Use httpService for blob responses (it handles blob responses automatically)
|
|
193
|
+
const result = await this.getClient().request<Blob>({
|
|
194
|
+
method: 'GET',
|
|
195
|
+
url: `/api/users/me/data`,
|
|
196
|
+
params: { format },
|
|
197
|
+
cache: false,
|
|
197
198
|
});
|
|
198
199
|
|
|
199
|
-
return
|
|
200
|
+
return result;
|
|
200
201
|
} catch (error) {
|
|
201
202
|
throw this.handleError(error);
|
|
202
203
|
}
|
|
@@ -177,6 +177,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
177
177
|
const apiError = this.handleError(error) as any;
|
|
178
178
|
|
|
179
179
|
if (debug) {
|
|
180
|
+
// Debug logging - using console.log is acceptable here for development
|
|
180
181
|
console.log(`❌ Auth: Unexpected error:`, apiError);
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -233,59 +233,6 @@ const FollowButton: React.FC<FollowButtonProps> = ({
|
|
|
233
233
|
);
|
|
234
234
|
};
|
|
235
235
|
|
|
236
|
-
const styles = StyleSheet.create({
|
|
237
|
-
// Legacy styles kept for backward compatibility but not used in new implementation
|
|
238
|
-
buttonSmall: {
|
|
239
|
-
paddingVertical: 4,
|
|
240
|
-
paddingHorizontal: 12,
|
|
241
|
-
borderRadius: 16,
|
|
242
|
-
minWidth: 60,
|
|
243
|
-
alignItems: 'center',
|
|
244
|
-
justifyContent: 'center',
|
|
245
|
-
},
|
|
246
|
-
buttonMedium: {
|
|
247
|
-
paddingVertical: 8,
|
|
248
|
-
paddingHorizontal: 20,
|
|
249
|
-
borderRadius: 20,
|
|
250
|
-
minWidth: 90,
|
|
251
|
-
alignItems: 'center',
|
|
252
|
-
justifyContent: 'center',
|
|
253
|
-
},
|
|
254
|
-
buttonLarge: {
|
|
255
|
-
paddingVertical: 12,
|
|
256
|
-
paddingHorizontal: 32,
|
|
257
|
-
borderRadius: 24,
|
|
258
|
-
minWidth: 120,
|
|
259
|
-
alignItems: 'center',
|
|
260
|
-
justifyContent: 'center',
|
|
261
|
-
},
|
|
262
|
-
following: {
|
|
263
|
-
backgroundColor: '#007AFF',
|
|
264
|
-
},
|
|
265
|
-
notFollowing: {
|
|
266
|
-
backgroundColor: '#fff',
|
|
267
|
-
borderWidth: 1,
|
|
268
|
-
borderColor: '#007AFF',
|
|
269
|
-
},
|
|
270
|
-
textSmall: {
|
|
271
|
-
fontSize: 13,
|
|
272
|
-
fontFamily: fontFamilies.phuduMedium,
|
|
273
|
-
},
|
|
274
|
-
textMedium: {
|
|
275
|
-
fontSize: 15,
|
|
276
|
-
fontFamily: fontFamilies.phuduMedium,
|
|
277
|
-
},
|
|
278
|
-
textLarge: {
|
|
279
|
-
fontSize: 18,
|
|
280
|
-
fontFamily: fontFamilies.phuduMedium,
|
|
281
|
-
},
|
|
282
|
-
textFollowing: {
|
|
283
|
-
color: '#fff',
|
|
284
|
-
},
|
|
285
|
-
textNotFollowing: {
|
|
286
|
-
color: '#007AFF',
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
236
|
|
|
290
237
|
export { FollowButton };
|
|
291
238
|
export default FollowButton;
|
package/src/utils/asyncUtils.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { TTLCache, registerCacheForCleanup } from './cache';
|
|
6
|
+
import { logger } from './loggerUtils';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Wrapper for async operations with automatic error handling
|
|
10
|
+
* Returns null on error instead of throwing
|
|
9
11
|
*/
|
|
10
12
|
export async function withErrorHandling<T>(
|
|
11
13
|
operation: () => Promise<T>,
|
|
@@ -18,7 +20,10 @@ export async function withErrorHandling<T>(
|
|
|
18
20
|
if (errorHandler) {
|
|
19
21
|
errorHandler(error);
|
|
20
22
|
} else {
|
|
21
|
-
|
|
23
|
+
logger.error(`Error in ${context || 'operation'}`, error instanceof Error ? error : new Error(String(error)), {
|
|
24
|
+
component: 'asyncUtils',
|
|
25
|
+
method: 'withErrorHandling',
|
|
26
|
+
});
|
|
22
27
|
}
|
|
23
28
|
return null;
|
|
24
29
|
}
|
|
@@ -44,6 +49,9 @@ export async function parallelWithErrorHandling<T>(
|
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
51
|
* Retry an async operation with exponential backoff
|
|
52
|
+
*
|
|
53
|
+
* By default, does not retry on 4xx errors (client errors).
|
|
54
|
+
* Use shouldRetry callback to customize retry behavior.
|
|
47
55
|
*/
|
|
48
56
|
export async function retryAsync<T>(
|
|
49
57
|
operation: () => Promise<T>,
|
|
@@ -53,6 +61,17 @@ export async function retryAsync<T>(
|
|
|
53
61
|
): Promise<T> {
|
|
54
62
|
let lastError: any;
|
|
55
63
|
|
|
64
|
+
// Default shouldRetry: don't retry on 4xx errors
|
|
65
|
+
const defaultShouldRetry = (error: any): boolean => {
|
|
66
|
+
// Don't retry on 4xx errors (client errors)
|
|
67
|
+
if (error?.response?.status >= 400 && error?.response?.status < 500) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const retryCheck = shouldRetry || defaultShouldRetry;
|
|
74
|
+
|
|
56
75
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
57
76
|
try {
|
|
58
77
|
return await operation();
|
|
@@ -63,11 +82,12 @@ export async function retryAsync<T>(
|
|
|
63
82
|
break;
|
|
64
83
|
}
|
|
65
84
|
|
|
66
|
-
if (
|
|
85
|
+
if (!retryCheck(error)) {
|
|
67
86
|
break;
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
// Calculate delay with exponential backoff and jitter
|
|
90
|
+
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
|
|
71
91
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
72
92
|
}
|
|
73
93
|
}
|
package/src/utils/errorUtils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiError } from '../models/interfaces';
|
|
2
|
+
import { logger } from './loggerUtils';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Error handling utilities for consistent error processing
|
|
@@ -16,6 +17,7 @@ export const ErrorCodes = {
|
|
|
16
17
|
|
|
17
18
|
// Validation errors
|
|
18
19
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
20
|
+
BAD_REQUEST: 'BAD_REQUEST',
|
|
19
21
|
MISSING_PARAMETER: 'MISSING_PARAMETER',
|
|
20
22
|
INVALID_FORMAT: 'INVALID_FORMAT',
|
|
21
23
|
|
|
@@ -60,32 +62,72 @@ export function handleHttpError(error: unknown): ApiError {
|
|
|
60
62
|
return error as ApiError;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
// Handle
|
|
64
|
-
if (error &&
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
// Handle AbortError (timeout or cancelled requests)
|
|
66
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
67
|
+
return createApiError(
|
|
68
|
+
'Request timeout or cancelled',
|
|
69
|
+
ErrorCodes.TIMEOUT,
|
|
70
|
+
0
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle TypeError (network failures, CORS, etc.)
|
|
75
|
+
if (error instanceof TypeError) {
|
|
76
|
+
// Check if it's a network-related TypeError
|
|
77
|
+
if (error.message.includes('fetch') || error.message.includes('network') || error.message.includes('Failed to fetch')) {
|
|
69
78
|
return createApiError(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
data
|
|
79
|
+
'Network error - failed to connect to server',
|
|
80
|
+
ErrorCodes.NETWORK_ERROR,
|
|
81
|
+
0
|
|
74
82
|
);
|
|
75
83
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Handle network errors - check if it looks like a network error
|
|
79
|
-
if (error && typeof error === 'object' && 'request' in error) {
|
|
80
84
|
return createApiError(
|
|
81
|
-
'Network error
|
|
85
|
+
error.message || 'Network error occurred',
|
|
82
86
|
ErrorCodes.NETWORK_ERROR,
|
|
83
87
|
0
|
|
84
88
|
);
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
// Handle fetch Response errors - check if it has response property with status
|
|
92
|
+
if (error && typeof error === 'object' && 'response' in error) {
|
|
93
|
+
const fetchError = error as {
|
|
94
|
+
response?: {
|
|
95
|
+
status: number;
|
|
96
|
+
statusText?: string;
|
|
97
|
+
};
|
|
98
|
+
status?: number;
|
|
99
|
+
message?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const status = fetchError.response?.status || fetchError.status;
|
|
103
|
+
if (status) {
|
|
104
|
+
return createApiError(
|
|
105
|
+
fetchError.message || `HTTP ${status} error`,
|
|
106
|
+
getErrorCodeFromStatus(status),
|
|
107
|
+
status
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
// Handle standard errors
|
|
88
113
|
if (error instanceof Error) {
|
|
114
|
+
// Check for common error patterns
|
|
115
|
+
if (error.message.includes('timeout') || error.message.includes('aborted')) {
|
|
116
|
+
return createApiError(
|
|
117
|
+
'Request timeout',
|
|
118
|
+
ErrorCodes.TIMEOUT,
|
|
119
|
+
0
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (error.message.includes('network') || error.message.includes('fetch')) {
|
|
124
|
+
return createApiError(
|
|
125
|
+
error.message || 'Network error occurred',
|
|
126
|
+
ErrorCodes.NETWORK_ERROR,
|
|
127
|
+
0
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
89
131
|
return createApiError(
|
|
90
132
|
error.message || 'Unknown error occurred',
|
|
91
133
|
ErrorCodes.INTERNAL_ERROR,
|
|
@@ -103,11 +145,12 @@ export function handleHttpError(error: unknown): ApiError {
|
|
|
103
145
|
|
|
104
146
|
/**
|
|
105
147
|
* Get error code from HTTP status
|
|
148
|
+
* Exported for use in other modules
|
|
106
149
|
*/
|
|
107
|
-
function getErrorCodeFromStatus(status: number): string {
|
|
150
|
+
export function getErrorCodeFromStatus(status: number): string {
|
|
108
151
|
switch (status) {
|
|
109
152
|
case 400:
|
|
110
|
-
return ErrorCodes.
|
|
153
|
+
return ErrorCodes.BAD_REQUEST;
|
|
111
154
|
case 401:
|
|
112
155
|
return ErrorCodes.UNAUTHORIZED;
|
|
113
156
|
case 403:
|
|
@@ -146,39 +189,23 @@ export function validateRequiredFields(data: Record<string, unknown>, fields: st
|
|
|
146
189
|
* Safe error logging with context
|
|
147
190
|
*/
|
|
148
191
|
export function logError(error: unknown, context?: string): void {
|
|
149
|
-
const prefix = context ? `[${context}]` : '[Error]';
|
|
150
|
-
|
|
151
192
|
if (error instanceof Error) {
|
|
152
|
-
|
|
193
|
+
logger.error(error.message, {
|
|
194
|
+
component: context || 'errorUtils',
|
|
195
|
+
method: 'logError',
|
|
196
|
+
stack: error.stack,
|
|
197
|
+
});
|
|
153
198
|
} else {
|
|
154
|
-
|
|
199
|
+
logger.error(String(error), {
|
|
200
|
+
component: context || 'errorUtils',
|
|
201
|
+
method: 'logError',
|
|
202
|
+
});
|
|
155
203
|
}
|
|
156
204
|
}
|
|
157
205
|
|
|
158
206
|
/**
|
|
159
207
|
* Retry function with exponential backoff
|
|
208
|
+
* Re-exports retryAsync for backward compatibility
|
|
209
|
+
* @deprecated Use retryAsync from asyncUtils instead
|
|
160
210
|
*/
|
|
161
|
-
export
|
|
162
|
-
fn: () => Promise<T>,
|
|
163
|
-
maxRetries = 3,
|
|
164
|
-
baseDelay = 1000
|
|
165
|
-
): Promise<T> {
|
|
166
|
-
let lastError: unknown;
|
|
167
|
-
|
|
168
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
169
|
-
try {
|
|
170
|
-
return await fn();
|
|
171
|
-
} catch (error) {
|
|
172
|
-
lastError = error;
|
|
173
|
-
|
|
174
|
-
if (attempt === maxRetries) {
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const delay = baseDelay * 2 ** attempt;
|
|
179
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
throw lastError;
|
|
184
|
-
}
|
|
211
|
+
export { retryAsync as retryWithBackoff } from './asyncUtils';
|
|
@@ -137,6 +137,20 @@ export function sanitizeHTML(input: string): string {
|
|
|
137
137
|
.replace(/'/g, ''');
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Validate MongoDB ObjectId format
|
|
142
|
+
* Note: This is a basic format check. For full validation, use mongoose.Types.ObjectId.isValid()
|
|
143
|
+
* This function works in environments where mongoose may not be available (e.g., client-side)
|
|
144
|
+
*/
|
|
145
|
+
export function isValidObjectId(id: string): boolean {
|
|
146
|
+
if (typeof id !== 'string') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
// MongoDB ObjectId is 24 hex characters
|
|
150
|
+
const OBJECT_ID_REGEX = /^[0-9a-fA-F]{24}$/;
|
|
151
|
+
return OBJECT_ID_REGEX.test(id);
|
|
152
|
+
}
|
|
153
|
+
|
|
140
154
|
/**
|
|
141
155
|
* Validate and sanitize user input
|
|
142
156
|
*/
|