@oxyhq/services 5.13.25 → 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.
Files changed (85) hide show
  1. package/lib/commonjs/core/HttpService.js +481 -0
  2. package/lib/commonjs/core/HttpService.js.map +1 -0
  3. package/lib/commonjs/core/OxyServices.base.js +29 -26
  4. package/lib/commonjs/core/OxyServices.base.js.map +1 -1
  5. package/lib/commonjs/core/OxyServices.js +1 -2
  6. package/lib/commonjs/core/OxyServices.js.map +1 -1
  7. package/lib/commonjs/core/mixins/OxyServices.assets.js +3 -2
  8. package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
  9. package/lib/commonjs/core/mixins/OxyServices.user.js +9 -5
  10. package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
  11. package/lib/commonjs/core/mixins/OxyServices.utility.js +1 -0
  12. package/lib/commonjs/core/mixins/OxyServices.utility.js.map +1 -1
  13. package/lib/commonjs/utils/errorUtils.js +35 -15
  14. package/lib/commonjs/utils/errorUtils.js.map +1 -1
  15. package/lib/module/core/HttpService.js +476 -0
  16. package/lib/module/core/HttpService.js.map +1 -0
  17. package/lib/module/core/OxyServices.base.js +29 -26
  18. package/lib/module/core/OxyServices.base.js.map +1 -1
  19. package/lib/module/core/OxyServices.js +1 -2
  20. package/lib/module/core/OxyServices.js.map +1 -1
  21. package/lib/module/core/mixins/OxyServices.assets.js +3 -2
  22. package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
  23. package/lib/module/core/mixins/OxyServices.user.js +9 -5
  24. package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
  25. package/lib/module/core/mixins/OxyServices.utility.js +1 -0
  26. package/lib/module/core/mixins/OxyServices.utility.js.map +1 -1
  27. package/lib/module/utils/errorUtils.js +36 -15
  28. package/lib/module/utils/errorUtils.js.map +1 -1
  29. package/lib/typescript/core/HttpService.d.ts +111 -0
  30. package/lib/typescript/core/HttpService.d.ts.map +1 -0
  31. package/lib/typescript/core/OxyServices.base.d.ts +6 -8
  32. package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
  33. package/lib/typescript/core/OxyServices.d.ts +1 -2
  34. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  35. package/lib/typescript/core/mixins/OxyServices.analytics.d.ts +4 -5
  36. package/lib/typescript/core/mixins/OxyServices.analytics.d.ts.map +1 -1
  37. package/lib/typescript/core/mixins/OxyServices.assets.d.ts +4 -5
  38. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
  39. package/lib/typescript/core/mixins/OxyServices.auth.d.ts +4 -5
  40. package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -1
  41. package/lib/typescript/core/mixins/OxyServices.developer.d.ts +4 -5
  42. package/lib/typescript/core/mixins/OxyServices.developer.d.ts.map +1 -1
  43. package/lib/typescript/core/mixins/OxyServices.devices.d.ts +4 -5
  44. package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -1
  45. package/lib/typescript/core/mixins/OxyServices.karma.d.ts +4 -5
  46. package/lib/typescript/core/mixins/OxyServices.karma.d.ts.map +1 -1
  47. package/lib/typescript/core/mixins/OxyServices.language.d.ts +4 -5
  48. package/lib/typescript/core/mixins/OxyServices.language.d.ts.map +1 -1
  49. package/lib/typescript/core/mixins/OxyServices.location.d.ts +4 -5
  50. package/lib/typescript/core/mixins/OxyServices.location.d.ts.map +1 -1
  51. package/lib/typescript/core/mixins/OxyServices.payment.d.ts +4 -5
  52. package/lib/typescript/core/mixins/OxyServices.payment.d.ts.map +1 -1
  53. package/lib/typescript/core/mixins/OxyServices.privacy.d.ts +4 -5
  54. package/lib/typescript/core/mixins/OxyServices.privacy.d.ts.map +1 -1
  55. package/lib/typescript/core/mixins/OxyServices.totp.d.ts +4 -5
  56. package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +1 -1
  57. package/lib/typescript/core/mixins/OxyServices.user.d.ts +4 -5
  58. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
  59. package/lib/typescript/core/mixins/OxyServices.utility.d.ts +4 -5
  60. package/lib/typescript/core/mixins/OxyServices.utility.d.ts.map +1 -1
  61. package/lib/typescript/core/mixins/index.d.ts +52 -65
  62. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  63. package/lib/typescript/utils/errorUtils.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/core/HttpService.ts +523 -0
  66. package/src/core/OxyServices.base.ts +36 -34
  67. package/src/core/OxyServices.ts +1 -2
  68. package/src/core/mixins/OxyServices.assets.ts +2 -1
  69. package/src/core/mixins/OxyServices.user.ts +7 -6
  70. package/src/core/mixins/OxyServices.utility.ts +1 -0
  71. package/src/utils/errorUtils.ts +65 -19
  72. package/lib/commonjs/core/HttpClient.js +0 -317
  73. package/lib/commonjs/core/HttpClient.js.map +0 -1
  74. package/lib/commonjs/core/RequestManager.js +0 -170
  75. package/lib/commonjs/core/RequestManager.js.map +0 -1
  76. package/lib/module/core/HttpClient.js +0 -311
  77. package/lib/module/core/HttpClient.js.map +0 -1
  78. package/lib/module/core/RequestManager.js +0 -165
  79. package/lib/module/core/RequestManager.js.map +0 -1
  80. package/lib/typescript/core/HttpClient.d.ts +0 -110
  81. package/lib/typescript/core/HttpClient.d.ts.map +0 -1
  82. package/lib/typescript/core/RequestManager.d.ts +0 -63
  83. package/lib/typescript/core/RequestManager.d.ts.map +0 -1
  84. package/src/core/HttpClient.ts +0 -346
  85. package/src/core/RequestManager.ts +0 -205
@@ -0,0 +1,523 @@
1
+ /**
2
+ * Unified HTTP Service
3
+ *
4
+ * Consolidates HttpClient + RequestManager into a single efficient class.
5
+ * Uses native fetch instead of axios for smaller bundle size.
6
+ *
7
+ * Handles:
8
+ * - Authentication (token management, auto-refresh)
9
+ * - Caching (TTL-based)
10
+ * - Deduplication (concurrent requests)
11
+ * - Retry logic
12
+ * - Error handling
13
+ * - Request queuing
14
+ */
15
+
16
+ import { TTLCache, registerCacheForCleanup } from '../utils/cache';
17
+ import { RequestDeduplicator, RequestQueue, SimpleLogger } from '../utils/requestUtils';
18
+ import { retryAsync } from '../utils/asyncUtils';
19
+ import { handleHttpError } from '../utils/errorUtils';
20
+ import { jwtDecode } from 'jwt-decode';
21
+ import type { OxyConfig } from '../models/interfaces';
22
+
23
+ interface JwtPayload {
24
+ exp?: number;
25
+ userId?: string;
26
+ id?: string;
27
+ sessionId?: string;
28
+ [key: string]: any;
29
+ }
30
+
31
+ export interface RequestOptions {
32
+ cache?: boolean;
33
+ cacheTTL?: number;
34
+ deduplicate?: boolean;
35
+ retry?: boolean;
36
+ maxRetries?: number;
37
+ timeout?: number;
38
+ signal?: AbortSignal;
39
+ }
40
+
41
+ interface RequestConfig extends RequestOptions {
42
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
43
+ url: string;
44
+ data?: unknown;
45
+ params?: Record<string, unknown>;
46
+ }
47
+
48
+ /**
49
+ * Token store for authentication (singleton)
50
+ */
51
+ class TokenStore {
52
+ private static instance: TokenStore;
53
+ private accessToken: string | null = null;
54
+ private refreshToken: string | null = null;
55
+
56
+ private constructor() {}
57
+
58
+ static getInstance(): TokenStore {
59
+ if (!TokenStore.instance) {
60
+ TokenStore.instance = new TokenStore();
61
+ }
62
+ return TokenStore.instance;
63
+ }
64
+
65
+ setTokens(accessToken: string, refreshToken = ''): void {
66
+ this.accessToken = accessToken;
67
+ this.refreshToken = refreshToken;
68
+ }
69
+
70
+ getAccessToken(): string | null {
71
+ return this.accessToken;
72
+ }
73
+
74
+ getRefreshToken(): string | null {
75
+ return this.refreshToken;
76
+ }
77
+
78
+ clearTokens(): void {
79
+ this.accessToken = null;
80
+ this.refreshToken = null;
81
+ }
82
+
83
+ hasAccessToken(): boolean {
84
+ return !!this.accessToken;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Unified HTTP Service
90
+ *
91
+ * Consolidates HttpClient + RequestManager into a single efficient class.
92
+ * Uses native fetch instead of axios for smaller bundle size.
93
+ */
94
+ export class HttpService {
95
+ private baseURL: string;
96
+ private tokenStore: TokenStore;
97
+ private cache: TTLCache<any>;
98
+ private deduplicator: RequestDeduplicator;
99
+ private requestQueue: RequestQueue;
100
+ private logger: SimpleLogger;
101
+ private config: OxyConfig;
102
+
103
+ // Performance monitoring
104
+ private requestMetrics = {
105
+ totalRequests: 0,
106
+ successfulRequests: 0,
107
+ failedRequests: 0,
108
+ cacheHits: 0,
109
+ cacheMisses: 0,
110
+ averageResponseTime: 0,
111
+ };
112
+
113
+ constructor(config: OxyConfig) {
114
+ this.config = config;
115
+ this.baseURL = config.baseURL;
116
+ this.tokenStore = TokenStore.getInstance();
117
+
118
+ this.logger = new SimpleLogger(
119
+ config.enableLogging || false,
120
+ config.logLevel || 'error',
121
+ 'HttpService'
122
+ );
123
+
124
+ // Initialize performance infrastructure
125
+ this.cache = new TTLCache<any>(config.cacheTTL || 5 * 60 * 1000);
126
+ registerCacheForCleanup(this.cache);
127
+ this.deduplicator = new RequestDeduplicator();
128
+ this.requestQueue = new RequestQueue(
129
+ config.maxConcurrentRequests || 10,
130
+ config.requestQueueSize || 100
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Main request method - handles everything in one place
136
+ */
137
+ async request<T = unknown>(config: RequestConfig): Promise<T> {
138
+ const {
139
+ method,
140
+ url,
141
+ data,
142
+ params,
143
+ timeout = this.config.requestTimeout || 5000,
144
+ signal,
145
+ cache = method === 'GET',
146
+ cacheTTL,
147
+ deduplicate = true,
148
+ retry = this.config.enableRetry !== false,
149
+ maxRetries = this.config.maxRetries || 3,
150
+ } = config;
151
+
152
+ // Generate cache key (optimized for large objects)
153
+ const cacheKey = cache ? this.generateCacheKey(method, url, data || params) : null;
154
+
155
+ // Check cache first
156
+ if (cache && cacheKey) {
157
+ const cached = this.cache.get(cacheKey) as T | null;
158
+ if (cached !== null) {
159
+ this.requestMetrics.cacheHits++;
160
+ this.logger.debug('Cache hit:', url);
161
+ return cached;
162
+ }
163
+ this.requestMetrics.cacheMisses++;
164
+ }
165
+
166
+ // Request function
167
+ const requestFn = async (): Promise<T> => {
168
+ const startTime = Date.now();
169
+ try {
170
+ // Build URL with params
171
+ const fullUrl = this.buildURL(url, params);
172
+
173
+ // Get auth token (with auto-refresh)
174
+ const authHeader = await this.getAuthHeader();
175
+
176
+ // Determine if data is FormData
177
+ const isFormData = data instanceof FormData;
178
+
179
+ // Make fetch request
180
+ const controller = new AbortController();
181
+ const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
182
+
183
+ if (signal) {
184
+ signal.addEventListener('abort', () => controller.abort());
185
+ }
186
+
187
+ // Build headers
188
+ const headers: Record<string, string> = {
189
+ 'Accept': 'application/json',
190
+ };
191
+
192
+ // Only set Content-Type for non-FormData requests (FormData sets it automatically with boundary)
193
+ if (!isFormData) {
194
+ headers['Content-Type'] = 'application/json';
195
+ }
196
+
197
+ if (authHeader) {
198
+ headers['Authorization'] = authHeader;
199
+ }
200
+
201
+ const response = await fetch(fullUrl, {
202
+ method,
203
+ headers,
204
+ body: method !== 'GET' && data ? (isFormData ? data : JSON.stringify(data)) : undefined,
205
+ signal: controller.signal,
206
+ });
207
+
208
+ if (timeoutId) clearTimeout(timeoutId);
209
+
210
+ // Handle response
211
+ if (!response.ok) {
212
+ if (response.status === 401) {
213
+ this.tokenStore.clearTokens();
214
+ }
215
+
216
+ // Try to parse error response (handle empty/malformed JSON)
217
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
218
+ const contentType = response.headers.get('content-type');
219
+ if (contentType && contentType.includes('application/json')) {
220
+ try {
221
+ const errorData = await response.json() as { message?: string } | null;
222
+ if (errorData?.message) {
223
+ errorMessage = errorData.message;
224
+ }
225
+ } catch (parseError) {
226
+ // Malformed JSON or empty response - use status text
227
+ this.logger.warn('Failed to parse error response JSON:', parseError);
228
+ }
229
+ }
230
+
231
+ const error = new Error(errorMessage) as Error & {
232
+ status?: number;
233
+ response?: { status: number; statusText: string }
234
+ };
235
+ error.status = response.status;
236
+ error.response = { status: response.status, statusText: response.statusText };
237
+ throw error;
238
+ }
239
+
240
+ // Handle different response types (optimized - read response once)
241
+ const contentType = response.headers.get('content-type');
242
+ let responseData: unknown;
243
+
244
+ if (contentType && contentType.includes('application/json')) {
245
+ // Use response.json() directly for better performance
246
+ try {
247
+ responseData = await response.json();
248
+ // Handle null/undefined responses
249
+ if (responseData === null || responseData === undefined) {
250
+ responseData = null;
251
+ } else {
252
+ // Unwrap standardized API response format for JSON
253
+ responseData = this.unwrapResponse(responseData);
254
+ }
255
+ } catch (parseError) {
256
+ // Handle malformed JSON or empty responses gracefully
257
+ // Note: Once response.json() is called, the body is consumed and cannot be read again
258
+ // So we check the error type to determine if it's empty or malformed
259
+ if (parseError instanceof SyntaxError) {
260
+ this.logger.warn('Failed to parse JSON response (malformed or empty):', parseError);
261
+ // SyntaxError typically means empty or malformed JSON
262
+ // For empty responses, return null; for malformed JSON, throw descriptive error
263
+ responseData = null; // Treat as empty response for safety
264
+ } else {
265
+ this.logger.warn('Failed to read response:', parseError);
266
+ throw new Error('Failed to read response from server');
267
+ }
268
+ }
269
+ } else if (contentType && (contentType.includes('application/octet-stream') || contentType.includes('image/') || contentType.includes('video/') || contentType.includes('audio/'))) {
270
+ // For binary responses (blobs), return the blob directly without unwrapping
271
+ responseData = await response.blob();
272
+ } else {
273
+ // For other responses, return as text
274
+ const text = await response.text();
275
+ responseData = text || null;
276
+ }
277
+
278
+ const duration = Date.now() - startTime;
279
+ this.updateMetrics(true, duration);
280
+ this.config.onRequestEnd?.(url, method, duration, true);
281
+
282
+ return responseData as T;
283
+ } catch (error: unknown) {
284
+ const duration = Date.now() - startTime;
285
+ this.updateMetrics(false, duration);
286
+ this.config.onRequestEnd?.(url, method, duration, false);
287
+ this.config.onRequestError?.(url, method, error instanceof Error ? error : new Error(String(error)));
288
+
289
+ // Handle AbortError specifically for better error messages
290
+ if (error instanceof Error && error.name === 'AbortError') {
291
+ throw handleHttpError(error);
292
+ }
293
+
294
+ throw handleHttpError(error);
295
+ }
296
+ };
297
+
298
+ // Wrap with retry if enabled
299
+ const requestWithRetry = retry
300
+ ? () => retryAsync(requestFn, maxRetries, this.config.retryDelay || 1000)
301
+ : requestFn;
302
+
303
+ // Wrap with deduplication if enabled (use optimized key generation)
304
+ const dedupeKey = deduplicate ? this.generateCacheKey(method, url, data || params) : null;
305
+ const finalRequest = dedupeKey
306
+ ? () => this.deduplicator.deduplicate(dedupeKey, requestWithRetry)
307
+ : requestWithRetry;
308
+
309
+ // Execute request (with queue if needed)
310
+ const result = await this.requestQueue.enqueue(finalRequest);
311
+
312
+ // Cache the result if caching is enabled
313
+ if (cache && cacheKey && result) {
314
+ this.cache.set(cacheKey, result, cacheTTL);
315
+ }
316
+
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * Generate cache key efficiently
322
+ * Uses simple hash for large objects to avoid expensive JSON.stringify
323
+ */
324
+ private generateCacheKey(method: string, url: string, data?: unknown): string {
325
+ if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
326
+ return `${method}:${url}`;
327
+ }
328
+
329
+ // For small objects, use JSON.stringify
330
+ const dataStr = JSON.stringify(data);
331
+ if (dataStr.length < 1000) {
332
+ return `${method}:${url}:${dataStr}`;
333
+ }
334
+
335
+ // For large objects, use a simple hash based on keys and values length
336
+ // This avoids expensive serialization while still being unique enough
337
+ const hash = typeof data === 'object' && data !== null
338
+ ? Object.keys(data).sort().join(',') + ':' + dataStr.length
339
+ : String(data).substring(0, 100);
340
+
341
+ return `${method}:${url}:${hash}`;
342
+ }
343
+
344
+ /**
345
+ * Build full URL with query params
346
+ */
347
+ private buildURL(url: string, params?: Record<string, unknown>): string {
348
+ const base = url.startsWith('http') ? url : `${this.baseURL}${url}`;
349
+
350
+ if (!params || Object.keys(params).length === 0) {
351
+ return base;
352
+ }
353
+
354
+ const searchParams = new URLSearchParams();
355
+ Object.entries(params).forEach(([key, value]) => {
356
+ if (value !== undefined && value !== null) {
357
+ searchParams.append(key, String(value));
358
+ }
359
+ });
360
+
361
+ const queryString = searchParams.toString();
362
+ return queryString ? `${base}${base.includes('?') ? '&' : '?'}${queryString}` : base;
363
+ }
364
+
365
+ /**
366
+ * Get auth header with automatic token refresh
367
+ */
368
+ private async getAuthHeader(): Promise<string | null> {
369
+ const accessToken = this.tokenStore.getAccessToken();
370
+ if (!accessToken) {
371
+ return null;
372
+ }
373
+
374
+ try {
375
+ const decoded = jwtDecode<JwtPayload>(accessToken);
376
+ const currentTime = Math.floor(Date.now() / 1000);
377
+
378
+ // If token expires in less than 60 seconds, refresh it
379
+ if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
380
+ try {
381
+ const refreshUrl = `${this.baseURL}/api/session/token/${decoded.sessionId}`;
382
+
383
+ // Use AbortSignal.timeout for consistent timeout handling
384
+ const response = await fetch(refreshUrl, {
385
+ method: 'GET',
386
+ headers: { 'Accept': 'application/json' },
387
+ signal: AbortSignal.timeout(5000),
388
+ });
389
+
390
+ if (response.ok) {
391
+ const { accessToken: newToken } = await response.json();
392
+ this.tokenStore.setTokens(newToken);
393
+ this.logger.debug('Token refreshed');
394
+ return `Bearer ${newToken}`;
395
+ }
396
+ } catch (refreshError) {
397
+ this.logger.warn('Token refresh failed, using current token');
398
+ }
399
+ }
400
+
401
+ return `Bearer ${accessToken}`;
402
+ } catch (error) {
403
+ this.logger.error('Error processing token:', error);
404
+ return `Bearer ${accessToken}`;
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Unwrap standardized API response format
410
+ */
411
+ private unwrapResponse(responseData: unknown): unknown {
412
+ // Handle paginated responses: { data: [...], pagination: {...} }
413
+ if (responseData && typeof responseData === 'object' && 'data' in responseData && 'pagination' in responseData) {
414
+ return responseData;
415
+ }
416
+
417
+ // Handle regular success responses: { data: ... }
418
+ if (responseData && typeof responseData === 'object' && 'data' in responseData && !Array.isArray(responseData)) {
419
+ return responseData.data;
420
+ }
421
+
422
+ // Return as-is for responses that don't use sendSuccess wrapper
423
+ return responseData;
424
+ }
425
+
426
+ /**
427
+ * Update request metrics
428
+ */
429
+ private updateMetrics(success: boolean, duration: number): void {
430
+ this.requestMetrics.totalRequests++;
431
+ if (success) {
432
+ this.requestMetrics.successfulRequests++;
433
+ } else {
434
+ this.requestMetrics.failedRequests++;
435
+ }
436
+
437
+ const alpha = 0.1;
438
+ this.requestMetrics.averageResponseTime =
439
+ this.requestMetrics.averageResponseTime * (1 - alpha) + duration * alpha;
440
+ }
441
+
442
+ // Convenience methods (for backward compatibility)
443
+ async get<T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<{ data: T }> {
444
+ const result = await this.request<T>({ method: 'GET', url, ...config });
445
+ return { data: result as T };
446
+ }
447
+
448
+ async post<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'data'>): Promise<{ data: T }> {
449
+ const result = await this.request<T>({ method: 'POST', url, data, ...config });
450
+ return { data: result as T };
451
+ }
452
+
453
+ async put<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'data'>): Promise<{ data: T }> {
454
+ const result = await this.request<T>({ method: 'PUT', url, data, ...config });
455
+ return { data: result as T };
456
+ }
457
+
458
+ async patch<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'data'>): Promise<{ data: T }> {
459
+ const result = await this.request<T>({ method: 'PATCH', url, data, ...config });
460
+ return { data: result as T };
461
+ }
462
+
463
+ async delete<T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<{ data: T }> {
464
+ const result = await this.request<T>({ method: 'DELETE', url, ...config });
465
+ return { data: result as T };
466
+ }
467
+
468
+ // Token management
469
+ setTokens(accessToken: string, refreshToken = ''): void {
470
+ this.tokenStore.setTokens(accessToken, refreshToken);
471
+ }
472
+
473
+ clearTokens(): void {
474
+ this.tokenStore.clearTokens();
475
+ }
476
+
477
+ getAccessToken(): string | null {
478
+ return this.tokenStore.getAccessToken();
479
+ }
480
+
481
+ hasAccessToken(): boolean {
482
+ return this.tokenStore.hasAccessToken();
483
+ }
484
+
485
+ getBaseURL(): string {
486
+ return this.baseURL;
487
+ }
488
+
489
+ // Cache management
490
+ clearCache(): void {
491
+ this.cache.clear();
492
+ }
493
+
494
+ clearCacheEntry(key: string): void {
495
+ this.cache.delete(key);
496
+ }
497
+
498
+ getCacheStats() {
499
+ const cacheStats = this.cache.getStats();
500
+ const total = this.requestMetrics.cacheHits + this.requestMetrics.cacheMisses;
501
+ return {
502
+ size: cacheStats.size,
503
+ hits: this.requestMetrics.cacheHits,
504
+ misses: this.requestMetrics.cacheMisses,
505
+ hitRate: total > 0 ? this.requestMetrics.cacheHits / total : 0,
506
+ };
507
+ }
508
+
509
+ getMetrics() {
510
+ return { ...this.requestMetrics };
511
+ }
512
+
513
+ // Test-only utility
514
+ static __resetTokensForTests(): void {
515
+ try {
516
+ TokenStore.getInstance().clearTokens();
517
+ } catch (error) {
518
+ // Silently fail in test cleanup - this is expected behavior
519
+ // TokenStore might not be initialized in some test scenarios
520
+ }
521
+ }
522
+ }
523
+