@rendomnet/apiservice 1.3.8 → 1.4.0

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/dist/index.mjs ADDED
@@ -0,0 +1,651 @@
1
+ // src/ApiKeyAuthProvider.ts
2
+ var ApiKeyAuthProvider = class {
3
+ constructor(options) {
4
+ this.apiKey = options.apiKey;
5
+ this.headerName = options.headerName;
6
+ this.queryParamName = options.queryParamName;
7
+ }
8
+ async getAuthHeaders() {
9
+ if (this.headerName) {
10
+ return { [this.headerName]: this.apiKey };
11
+ }
12
+ return {};
13
+ }
14
+ // For API key, refresh is not supported
15
+ };
16
+
17
+ // src/CacheManager.ts
18
+ var CacheManager = class {
19
+ constructor() {
20
+ this.cache = /* @__PURE__ */ new Map();
21
+ this.cacheTime = 2e4;
22
+ }
23
+ // Default cache time of 20 seconds
24
+ /**
25
+ * Get data from cache if available and not expired
26
+ */
27
+ getFromCache(apiCallParams) {
28
+ var _a;
29
+ const requestKey = this.getRequestKey(apiCallParams);
30
+ const currentCacheTime = (_a = apiCallParams.cacheTime) != null ? _a : this.cacheTime;
31
+ const cached = this.cache.get(requestKey);
32
+ if (cached && Date.now() - cached.timestamp < currentCacheTime) {
33
+ return cached.data;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Save data to cache
39
+ */
40
+ saveToCache(apiCallParams, data) {
41
+ const requestKey = this.getRequestKey(apiCallParams);
42
+ this.cache.set(requestKey, {
43
+ data,
44
+ timestamp: Date.now()
45
+ });
46
+ }
47
+ /**
48
+ * Generate a unique key for caching based on request parameters
49
+ */
50
+ getRequestKey(apiCallParams) {
51
+ const { accountId, method, route, base, queryParams, body, data } = apiCallParams;
52
+ return JSON.stringify({
53
+ accountId,
54
+ method,
55
+ route,
56
+ base,
57
+ queryParams,
58
+ body: body || data
59
+ });
60
+ }
61
+ /**
62
+ * Set the default cache time in milliseconds
63
+ */
64
+ setCacheTime(milliseconds) {
65
+ this.cacheTime = milliseconds;
66
+ }
67
+ /**
68
+ * Clear the entire cache
69
+ */
70
+ clearCache() {
71
+ this.cache.clear();
72
+ }
73
+ };
74
+
75
+ // src/RetryManager.ts
76
+ var RetryManager = class {
77
+ constructor() {
78
+ this.defaultMaxDelay = 6e4;
79
+ // Default max delay of 1 minute
80
+ this.defaultMaxRetries = 4;
81
+ /**
82
+ * Default exponential backoff strategy with full jitter
83
+ */
84
+ this.defaultDelayStrategy = {
85
+ calculate: (attempt, response) => {
86
+ const retryAfter = this.getRetryAfterValue(response);
87
+ if (retryAfter) return retryAfter;
88
+ const baseDelay = 1e3;
89
+ const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
90
+ return Math.floor(Math.random() * exponentialDelay);
91
+ }
92
+ };
93
+ }
94
+ /**
95
+ * Calculate and wait for appropriate delay time before retry
96
+ */
97
+ async calculateAndDelay(params) {
98
+ const { attempt, response, hook } = params;
99
+ const delayStrategy = hook.delayStrategy || this.defaultDelayStrategy;
100
+ const maxDelay = hook.maxDelay || this.defaultMaxDelay;
101
+ const calculatedDelay = delayStrategy.calculate(attempt, response);
102
+ const finalDelay = Math.min(calculatedDelay, maxDelay);
103
+ console.log(`\u{1F504} Waiting for ${finalDelay / 1e3} seconds before retrying.`);
104
+ await new Promise((resolve) => setTimeout(resolve, finalDelay));
105
+ }
106
+ /**
107
+ * Extract retry-after value from response
108
+ */
109
+ getRetryAfterValue(response) {
110
+ var _a;
111
+ if (!((_a = response == null ? void 0 : response.headers) == null ? void 0 : _a.get)) return null;
112
+ const retryAfter = response.headers.get("Retry-After");
113
+ if (!retryAfter) return null;
114
+ const parsedDelay = parseInt(retryAfter, 10);
115
+ if (!isNaN(parsedDelay)) {
116
+ return parsedDelay * 1e3;
117
+ }
118
+ const date = new Date(retryAfter).getTime();
119
+ const now = Date.now();
120
+ if (date > now) {
121
+ return date - now;
122
+ }
123
+ return null;
124
+ }
125
+ /**
126
+ * Get the default maximum number of retries
127
+ */
128
+ getDefaultMaxRetries() {
129
+ return this.defaultMaxRetries;
130
+ }
131
+ /**
132
+ * Set the default maximum number of retries
133
+ */
134
+ setDefaultMaxRetries(maxRetries) {
135
+ this.defaultMaxRetries = maxRetries;
136
+ }
137
+ /**
138
+ * Set the default maximum delay between retries
139
+ */
140
+ setDefaultMaxDelay(maxDelay) {
141
+ this.defaultMaxDelay = maxDelay;
142
+ }
143
+ };
144
+
145
+ // src/HookManager.ts
146
+ var HookManager = class {
147
+ constructor() {
148
+ this.hooks = {};
149
+ this.hookPromises = {};
150
+ }
151
+ /**
152
+ * Set hooks for different status codes
153
+ */
154
+ setHooks(hooks) {
155
+ this.hooks = { ...this.hooks, ...hooks };
156
+ }
157
+ /**
158
+ * Get a hook for a specific status code
159
+ */
160
+ getHook(status) {
161
+ return this.hooks[status];
162
+ }
163
+ /**
164
+ * Process a hook for a specific status code
165
+ */
166
+ async processHook(accountId, status, error) {
167
+ const hook = this.hooks[status];
168
+ if (!hook || !hook.handler) return null;
169
+ const hookKey = `${accountId || "default"}-${status}`;
170
+ try {
171
+ if (hook.preventConcurrentCalls) {
172
+ if (!this.hookPromises[hookKey]) {
173
+ this.hookPromises[hookKey] = Promise.resolve(
174
+ hook.handler(accountId, error.response) || {}
175
+ );
176
+ }
177
+ const result = await this.hookPromises[hookKey];
178
+ delete this.hookPromises[hookKey];
179
+ return result;
180
+ }
181
+ return await hook.handler(accountId, error.response) || {};
182
+ } catch (hookError) {
183
+ console.error(`Hook handler failed for status ${status}:`, hookError);
184
+ if (hook.onHandlerError) {
185
+ await hook.onHandlerError(accountId, hookError);
186
+ }
187
+ throw hookError;
188
+ }
189
+ }
190
+ /**
191
+ * Handle a retry failure with the appropriate hook
192
+ */
193
+ async handleRetryFailure(accountId, status, error) {
194
+ const hook = this.hooks[status];
195
+ if (hook == null ? void 0 : hook.onMaxRetriesExceeded) {
196
+ await hook.onMaxRetriesExceeded(accountId, error);
197
+ }
198
+ }
199
+ /**
200
+ * Check if a hook exists and should retry for a given status
201
+ */
202
+ shouldRetry(status) {
203
+ const hook = this.hooks[status];
204
+ return !!hook && !!hook.shouldRetry;
205
+ }
206
+ };
207
+
208
+ // src/HttpClient.ts
209
+ import qs from "qs";
210
+
211
+ // src/FetchError.ts
212
+ var FetchError = class extends Error {
213
+ constructor(response, data = {}, code = "FETCH_ERROR") {
214
+ super();
215
+ this.name = "FetchError";
216
+ const defaultMessage = "An unspecified error occurred";
217
+ const getMessage = (data2, locations) => locations.map((item) => {
218
+ const parts = item.split(".");
219
+ let value = data2;
220
+ for (const part of parts) {
221
+ value = value == null ? void 0 : value[part];
222
+ }
223
+ return value;
224
+ }).find((message) => typeof message === "string") || null;
225
+ const messageFromData = getMessage(data, [
226
+ "error.errors.0.message",
227
+ "error.message",
228
+ "error",
229
+ "message"
230
+ ]);
231
+ this.message = messageFromData || response.statusText || defaultMessage;
232
+ this.status = response.status;
233
+ this.code = code;
234
+ this.data = data;
235
+ this.message = this.message || defaultMessage;
236
+ Object.setPrototypeOf(this, new.target.prototype);
237
+ }
238
+ };
239
+
240
+ // src/form.ts
241
+ function deserializeForm(src) {
242
+ const fd = new FormData();
243
+ switch (src.cls) {
244
+ case "FormData": {
245
+ for (const [key, items] of src.value) {
246
+ for (const item of items) {
247
+ let deserializedItem = deserializeForm(item);
248
+ if (deserializedItem instanceof FormData) {
249
+ const entries = deserializedItem;
250
+ for (const [subKey, subValue] of entries.entries()) {
251
+ fd.append(`${key}[${subKey}]`, subValue);
252
+ }
253
+ } else {
254
+ fd.append(key, deserializedItem);
255
+ }
256
+ }
257
+ }
258
+ break;
259
+ }
260
+ case "Blob":
261
+ case "File": {
262
+ const { type, name, lastModified } = src;
263
+ const binStr = atob(src.value);
264
+ const arr = new Uint8Array(binStr.length);
265
+ for (let i = 0; i < binStr.length; i++) arr[i] = binStr.charCodeAt(i);
266
+ const data = [arr.buffer];
267
+ const fileOrBlob = src.cls === "File" ? new File(data, name, { type, lastModified }) : new Blob(data, { type });
268
+ fd.append("file", fileOrBlob);
269
+ break;
270
+ }
271
+ case "json": {
272
+ fd.append("json", JSON.stringify(JSON.parse(src.value)));
273
+ break;
274
+ }
275
+ default:
276
+ throw new Error("Unsupported type for deserialization");
277
+ }
278
+ return fd;
279
+ }
280
+
281
+ // src/HttpClient.ts
282
+ var HttpClient = class {
283
+ /**
284
+ * Make an HTTP request
285
+ */
286
+ async makeRequest(apiParams, authToken) {
287
+ const {
288
+ accountId,
289
+ method,
290
+ route,
291
+ base,
292
+ body,
293
+ data,
294
+ headers,
295
+ queryParams,
296
+ contentType = "application/json",
297
+ accessToken: forcedAccessToken,
298
+ useAuth = true,
299
+ files,
300
+ abortSignal
301
+ } = apiParams;
302
+ const normalizedMethod = method == null ? void 0 : method.toUpperCase();
303
+ let finalQueryParams = {};
304
+ if (queryParams) {
305
+ if (queryParams instanceof URLSearchParams) {
306
+ queryParams.forEach((val, key) => {
307
+ finalQueryParams[key] = val;
308
+ });
309
+ } else {
310
+ finalQueryParams = { ...queryParams };
311
+ }
312
+ }
313
+ if (normalizedMethod === "GET") {
314
+ if (body) {
315
+ finalQueryParams = { ...finalQueryParams, ...body };
316
+ }
317
+ if (data) {
318
+ finalQueryParams = { ...finalQueryParams, ...data };
319
+ }
320
+ }
321
+ const url = this.buildUrl(base, route, finalQueryParams);
322
+ const requestBody = body || data;
323
+ const formData = this.prepareFormData(files);
324
+ const fetchOptions = this.buildFetchOptions({
325
+ method: normalizedMethod,
326
+ body: requestBody,
327
+ formData,
328
+ contentType,
329
+ authToken,
330
+ forcedAccessToken,
331
+ useAuth,
332
+ headers,
333
+ abortSignal
334
+ });
335
+ try {
336
+ console.log(`\u{1F504} Making API call to ${url}`);
337
+ const response = await fetch(url, fetchOptions);
338
+ return await this.handleResponse(response);
339
+ } catch (error) {
340
+ console.error("\u{1F504} Error making API call:", error, "status:", error == null ? void 0 : error.status);
341
+ throw error;
342
+ }
343
+ }
344
+ /**
345
+ * Build URL with query parameters
346
+ */
347
+ buildUrl(base, route, queryParams) {
348
+ const baseUrl = base || "";
349
+ let url = `${baseUrl}${route || ""}`;
350
+ if (queryParams && Object.keys(queryParams).length > 0) {
351
+ url += `?${qs.stringify(queryParams)}`;
352
+ }
353
+ return url;
354
+ }
355
+ /**
356
+ * Prepare form data for file uploads
357
+ */
358
+ prepareFormData(files) {
359
+ if (!files) return null;
360
+ const formData = deserializeForm(files);
361
+ const entries = formData;
362
+ for (let [key, value] of entries.entries()) {
363
+ console.log(`formdata ${key}:`, value);
364
+ }
365
+ return formData;
366
+ }
367
+ /**
368
+ * Build fetch options for request
369
+ */
370
+ buildFetchOptions({
371
+ method,
372
+ body,
373
+ formData,
374
+ contentType,
375
+ authToken,
376
+ forcedAccessToken,
377
+ useAuth,
378
+ headers,
379
+ abortSignal
380
+ }) {
381
+ const allowedMethods = ["POST", "PUT", "PATCH"];
382
+ return {
383
+ method,
384
+ signal: abortSignal,
385
+ headers: {
386
+ ...useAuth && {
387
+ Authorization: `Bearer ${forcedAccessToken || authToken.access_token}`
388
+ },
389
+ ...!formData && { "content-type": contentType },
390
+ ...headers || {}
391
+ },
392
+ body: body && allowedMethods.includes(method) ? JSON.stringify(body) : formData || null
393
+ };
394
+ }
395
+ /**
396
+ * Handle API response
397
+ */
398
+ async handleResponse(response) {
399
+ let data = null;
400
+ try {
401
+ data = await response.json();
402
+ console.log("\u{1F504} Response data:", data);
403
+ } catch (error) {
404
+ }
405
+ if (!response.ok) {
406
+ throw new FetchError(response, data);
407
+ }
408
+ return data;
409
+ }
410
+ };
411
+
412
+ // src/AccountManager.ts
413
+ var AccountManager = class {
414
+ constructor() {
415
+ this.accounts = {};
416
+ this.DEFAULT_ACCOUNT = "default";
417
+ }
418
+ /**
419
+ * Update account data for a specific account
420
+ */
421
+ updateAccountData(accountId = this.DEFAULT_ACCOUNT, data) {
422
+ this.accounts[accountId] = {
423
+ ...this.accounts[accountId],
424
+ ...data
425
+ };
426
+ }
427
+ /**
428
+ * Get account data for a specific account
429
+ */
430
+ getAccountData(accountId = this.DEFAULT_ACCOUNT) {
431
+ return this.accounts[accountId] || {};
432
+ }
433
+ /**
434
+ * Check if an account's last request failed
435
+ */
436
+ didLastRequestFail(accountId = this.DEFAULT_ACCOUNT) {
437
+ var _a;
438
+ return !!((_a = this.accounts[accountId]) == null ? void 0 : _a.lastFailed);
439
+ }
440
+ /**
441
+ * Set account's last request as failed
442
+ */
443
+ setLastRequestFailed(accountId = this.DEFAULT_ACCOUNT, failed = true) {
444
+ this.updateAccountData(accountId, { lastFailed: failed });
445
+ }
446
+ /**
447
+ * Update the last request time for an account
448
+ */
449
+ updateLastRequestTime(accountId = this.DEFAULT_ACCOUNT) {
450
+ this.updateAccountData(accountId, { lastRequestTime: Date.now() });
451
+ }
452
+ };
453
+
454
+ // src/index.ts
455
+ var ApiService = class {
456
+ constructor() {
457
+ this.baseUrl = "";
458
+ // Default max attempts for API calls
459
+ this.maxAttempts = 10;
460
+ this.provider = "";
461
+ this.authProvider = {};
462
+ this.cacheManager = new CacheManager();
463
+ this.retryManager = new RetryManager();
464
+ this.hookManager = new HookManager();
465
+ this.httpClient = new HttpClient();
466
+ this.accountManager = new AccountManager();
467
+ }
468
+ /**
469
+ * Setup the API service
470
+ */
471
+ setup({
472
+ provider,
473
+ authProvider,
474
+ hooks = {},
475
+ cacheTime,
476
+ baseUrl = ""
477
+ }) {
478
+ this.provider = provider;
479
+ this.authProvider = authProvider;
480
+ this.baseUrl = baseUrl;
481
+ const finalHooks = {};
482
+ if (hooks[401] === void 0 && typeof this.authProvider.refresh === "function") {
483
+ finalHooks[401] = this.createDefaultAuthRefreshHandler();
484
+ }
485
+ for (const [statusCode, hook] of Object.entries(hooks)) {
486
+ if (hook) {
487
+ finalHooks[statusCode] = hook;
488
+ }
489
+ }
490
+ if (Object.keys(finalHooks).length > 0) {
491
+ this.hookManager.setHooks(finalHooks);
492
+ }
493
+ if (typeof cacheTime !== "undefined") {
494
+ this.cacheManager.setCacheTime(cacheTime);
495
+ }
496
+ }
497
+ /**
498
+ * Create a default handler for 401 (Unauthorized) errors
499
+ * that implements standard credential refresh behavior
500
+ */
501
+ createDefaultAuthRefreshHandler() {
502
+ return {
503
+ shouldRetry: true,
504
+ useRetryDelay: true,
505
+ preventConcurrentCalls: true,
506
+ maxRetries: 1,
507
+ handler: async (accountId) => {
508
+ try {
509
+ console.log(`\u{1F504} Using default auth refresh handler for ${accountId}`);
510
+ if (!this.authProvider.refresh) {
511
+ throw new Error("No refresh method available on auth provider");
512
+ }
513
+ await this.authProvider.refresh(accountId);
514
+ return {};
515
+ } catch (error) {
516
+ console.error(`Auth refresh failed for ${accountId}:`, error);
517
+ throw error;
518
+ }
519
+ },
520
+ onMaxRetriesExceeded: async (accountId, error) => {
521
+ console.error(`Authentication failed after refresh attempt for ${accountId}:`, error);
522
+ }
523
+ };
524
+ }
525
+ /**
526
+ * Set the maximum number of retry attempts
527
+ */
528
+ setMaxAttempts(attempts) {
529
+ this.maxAttempts = attempts;
530
+ }
531
+ /**
532
+ * Update account data
533
+ */
534
+ updateAccountData(accountId, data) {
535
+ this.accountManager.updateAccountData(accountId, data);
536
+ }
537
+ /**
538
+ * Make an API call with all features (caching, retry, hooks)
539
+ */
540
+ async call(apiCallParams) {
541
+ const params = {
542
+ ...apiCallParams,
543
+ accountId: apiCallParams.accountId || "default",
544
+ base: apiCallParams.base || this.baseUrl
545
+ };
546
+ if (this.authProvider instanceof ApiKeyAuthProvider && this.authProvider.queryParamName) {
547
+ const queryParamName = this.authProvider.queryParamName;
548
+ const apiKey = this.authProvider.apiKey;
549
+ const urlParams = params.queryParams ? new URLSearchParams(params.queryParams) : new URLSearchParams();
550
+ urlParams.set(queryParamName, apiKey);
551
+ params.queryParams = urlParams;
552
+ }
553
+ console.log("\u{1F504} API call", this.provider, params.accountId, params.method, params.route);
554
+ const cachedData = this.cacheManager.getFromCache(params);
555
+ if (cachedData) return cachedData;
556
+ const result = await this.makeRequestWithRetry(params);
557
+ this.cacheManager.saveToCache(params, result);
558
+ return result;
559
+ }
560
+ /**
561
+ * Legacy method for backward compatibility
562
+ * @deprecated Use call() instead
563
+ */
564
+ async makeApiCall(apiCallParams) {
565
+ return this.call(apiCallParams);
566
+ }
567
+ /**
568
+ * Make a request with retry capability
569
+ */
570
+ async makeRequestWithRetry(apiCallParams) {
571
+ var _a;
572
+ const { accountId, abortSignal } = apiCallParams;
573
+ let attempts = 0;
574
+ const statusRetries = {};
575
+ let currentParams = { ...apiCallParams };
576
+ if (this.authProvider instanceof ApiKeyAuthProvider && this.authProvider.queryParamName) {
577
+ const queryParamName = this.authProvider.queryParamName;
578
+ const apiKey = this.authProvider.apiKey;
579
+ const urlParams = currentParams.queryParams ? new URLSearchParams(currentParams.queryParams) : new URLSearchParams();
580
+ urlParams.set(queryParamName, apiKey);
581
+ currentParams.queryParams = urlParams;
582
+ }
583
+ while (attempts < this.maxAttempts) {
584
+ if (abortSignal == null ? void 0 : abortSignal.aborted) {
585
+ throw new Error("Request aborted");
586
+ }
587
+ attempts++;
588
+ try {
589
+ const authHeaders = apiCallParams.useAuth !== false ? await this.authProvider.getAuthHeaders(accountId) : {};
590
+ currentParams.headers = {
591
+ ...currentParams.headers || {},
592
+ ...authHeaders
593
+ };
594
+ if (apiCallParams.useAuth !== false && Object.keys(authHeaders).length === 0 && !(this.authProvider instanceof ApiKeyAuthProvider && this.authProvider.queryParamName)) {
595
+ throw new Error(`${this.provider} credentials not found for account ID ${accountId}`);
596
+ }
597
+ const response = await this.httpClient.makeRequest(currentParams, {});
598
+ this.accountManager.setLastRequestFailed(accountId, false);
599
+ return response;
600
+ } catch (error) {
601
+ const status = error == null ? void 0 : error.status;
602
+ if (!this.hookManager.shouldRetry(status)) {
603
+ throw error;
604
+ }
605
+ statusRetries[status] = (statusRetries[status] || 0) + 1;
606
+ const activeHook = this.hookManager.getHook(status);
607
+ const maxRetries = (_a = activeHook == null ? void 0 : activeHook.maxRetries) != null ? _a : this.retryManager.getDefaultMaxRetries();
608
+ if (statusRetries[status] > maxRetries) {
609
+ await this.hookManager.handleRetryFailure(accountId, status, error);
610
+ this.accountManager.setLastRequestFailed(accountId, true);
611
+ throw error;
612
+ }
613
+ try {
614
+ const hookResult = await this.hookManager.processHook(accountId, status, error);
615
+ if (hookResult) {
616
+ currentParams = { ...currentParams, ...hookResult };
617
+ }
618
+ } catch (hookError) {
619
+ this.accountManager.setLastRequestFailed(accountId, true);
620
+ throw hookError;
621
+ }
622
+ if (activeHook == null ? void 0 : activeHook.useRetryDelay) {
623
+ await this.retryManager.calculateAndDelay({
624
+ attempt: statusRetries[status],
625
+ response: error.response,
626
+ hook: activeHook
627
+ });
628
+ }
629
+ }
630
+ }
631
+ this.accountManager.setLastRequestFailed(accountId, true);
632
+ throw new Error(`Exceeded maximum attempts (${this.maxAttempts}) for API call to ${accountId}`);
633
+ }
634
+ /**
635
+ * Set the cache time in milliseconds
636
+ */
637
+ setCacheTime(milliseconds) {
638
+ this.cacheManager.setCacheTime(milliseconds);
639
+ }
640
+ /**
641
+ * Clear the cache
642
+ */
643
+ clearCache() {
644
+ this.cacheManager.clearCache();
645
+ }
646
+ };
647
+ var index_default = ApiService;
648
+ export {
649
+ index_default as default
650
+ };
651
+ //# sourceMappingURL=index.mjs.map