@thorprovider/adapters 3.0.2
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/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-test.log +1 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +271 -0
- package/README.md +634 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +2524 -0
- package/dist/index.d.ts +2524 -0
- package/dist/index.js +8136 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +8045 -0
- package/dist/index.mjs.map +1 -0
- package/docs/RETRY_SOLUTION_SUMMARY.md +55 -0
- package/docs/authentication.md +165 -0
- package/docs/capabilities-system.md +143 -0
- package/docs/category-customization.md +309 -0
- package/docs/fetch-reliability-solution.md +399 -0
- package/docs/getting-started.md +154 -0
- package/docs/multi-tenant-channel-scoping.md +225 -0
- package/docs/profile-management.md +147 -0
- package/docs/providers-overview.md +85 -0
- package/docs/services-overview.md +135 -0
- package/docs/update-documentation.md +113 -0
- package/package.json +43 -0
- package/publish.log +93 -0
- package/quick-reference.md +169 -0
- package/src/admin/AdminAuthService.ts +88 -0
- package/src/admin/AdminSessionService.ts +91 -0
- package/src/admin/MetricsService.ts +145 -0
- package/src/admin/index.ts +13 -0
- package/src/backend/BackendMetadataAdapter.ts +88 -0
- package/src/backend/factory.ts +107 -0
- package/src/backend/index.ts +12 -0
- package/src/backend/medusa/MedusaMetadataAdapter.ts +139 -0
- package/src/core/errors.js +161 -0
- package/src/core/errors.ts +132 -0
- package/src/core/fetch-utils.js +240 -0
- package/src/core/fetch-utils.ts +227 -0
- package/src/core/index.ts +8 -0
- package/src/core/logger.js +63 -0
- package/src/core/logger.ts +37 -0
- package/src/factory/index.ts +102 -0
- package/src/index.ts +205 -0
- package/src/lib/money-transform.ts +80 -0
- package/src/metadata/channel-scope-validation.js +136 -0
- package/src/metadata/channel-scope-validation.ts +151 -0
- package/src/metadata/channel-scope.js +174 -0
- package/src/metadata/channel-scope.ts +215 -0
- package/src/metadata/index.js +31 -0
- package/src/metadata/index.ts +37 -0
- package/src/providers/bigcommerce/bigcommerce.ts +6 -0
- package/src/providers/bigcommerce/index.ts +315 -0
- package/src/providers/bigcommerce/transforms.ts +227 -0
- package/src/providers/index.ts +22 -0
- package/src/providers/magento/index.ts +387 -0
- package/src/providers/magento/magento.ts +5 -0
- package/src/providers/magento/queries.ts +267 -0
- package/src/providers/magento/transforms.ts +249 -0
- package/src/providers/medusa/auth.js +601 -0
- package/src/providers/medusa/auth.ts +496 -0
- package/src/providers/medusa/fulfillment-planner.ts +225 -0
- package/src/providers/medusa/index.js +2485 -0
- package/src/providers/medusa/index.ts +4239 -0
- package/src/providers/medusa/medusa.ts +8 -0
- package/src/providers/medusa/payment.js +265 -0
- package/src/providers/medusa/payment.ts +269 -0
- package/src/providers/medusa/transforms.js +505 -0
- package/src/providers/medusa/transforms.ts +625 -0
- package/src/providers/shopify/index.ts +444 -0
- package/src/providers/shopify/mutations.ts +102 -0
- package/src/providers/shopify/queries.ts +324 -0
- package/src/providers/shopify/shopify.ts +7 -0
- package/src/providers/shopify/transforms.ts +270 -0
- package/src/providers/spree/index.ts +344 -0
- package/src/providers/spree/spree.ts +5 -0
- package/src/providers/spree/transforms.ts +254 -0
- package/src/providers/woocommerce/index.ts +310 -0
- package/src/providers/woocommerce/transforms.ts +254 -0
- package/src/providers/woocommerce/woocommerce.ts +5 -0
- package/src/resilience/README.md +299 -0
- package/src/resilience/circuit-breaker.ts +222 -0
- package/src/resilience/index.ts +10 -0
- package/src/types/capabilities.types.js +141 -0
- package/src/types/capabilities.types.ts +366 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider.js +9 -0
- package/src/types/provider.ts +9 -0
- package/src/types/ui-responses.ts +261 -0
- package/src/utils/city-coordinates.ts +40 -0
- package/src/utils/geolocation.ts +52 -0
- package/src/utils/payment-utils.js +152 -0
- package/src/utils/payment-utils.ts +203 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @thorprovider/adapters v2.0
|
|
3
|
+
* Core errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base error class for commerce providers
|
|
8
|
+
*/
|
|
9
|
+
export class CommerceError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
message: string,
|
|
12
|
+
public readonly code?: string,
|
|
13
|
+
public readonly status?: number
|
|
14
|
+
) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'CommerceError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provider not found error
|
|
22
|
+
*/
|
|
23
|
+
export class ProviderNotFoundError extends CommerceError {
|
|
24
|
+
constructor(providerName: string) {
|
|
25
|
+
super(`Provider not found: ${providerName}`, 'PROVIDER_NOT_FOUND');
|
|
26
|
+
this.name = 'ProviderNotFoundError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Configuration error
|
|
32
|
+
*/
|
|
33
|
+
export class ConfigurationError extends CommerceError {
|
|
34
|
+
constructor(message: string) {
|
|
35
|
+
super(message, 'CONFIGURATION_ERROR');
|
|
36
|
+
this.name = 'ConfigurationError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resource not found error
|
|
42
|
+
*/
|
|
43
|
+
export class ResourceNotFoundError extends CommerceError {
|
|
44
|
+
constructor(resource: string, identifier: string) {
|
|
45
|
+
super(`${resource} not found: ${identifier}`, 'RESOURCE_NOT_FOUND', 404);
|
|
46
|
+
this.name = 'ResourceNotFoundError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* API error from provider
|
|
52
|
+
*/
|
|
53
|
+
export class ProviderAPIError extends CommerceError {
|
|
54
|
+
constructor(
|
|
55
|
+
message: string,
|
|
56
|
+
public readonly provider: string,
|
|
57
|
+
status?: number
|
|
58
|
+
) {
|
|
59
|
+
super(message, 'PROVIDER_API_ERROR', status);
|
|
60
|
+
this.name = 'ProviderAPIError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract error message from various error types
|
|
66
|
+
* Handles standard Error objects, API responses, and unknown error types
|
|
67
|
+
*/
|
|
68
|
+
export function extractErrorMessage(error: unknown): string {
|
|
69
|
+
// Standard Error object
|
|
70
|
+
if (error instanceof Error && error.message) {
|
|
71
|
+
// Don't return generic messages if there's more data
|
|
72
|
+
if (error.message !== 'An unknown error occurred.') {
|
|
73
|
+
return error.message;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Object with message property
|
|
78
|
+
if (typeof error === 'object' && error !== null) {
|
|
79
|
+
const err = error as any;
|
|
80
|
+
|
|
81
|
+
// Check for response body (common in fetch errors)
|
|
82
|
+
if (err.body) {
|
|
83
|
+
if (typeof err.body === 'string') return err.body;
|
|
84
|
+
if (err.body.message) return String(err.body.message);
|
|
85
|
+
if (err.body.error?.message) return String(err.body.error.message);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for response object
|
|
89
|
+
if (err.response) {
|
|
90
|
+
if (err.response.data?.message) return String(err.response.data.message);
|
|
91
|
+
if (err.response.data?.error) return String(err.response.data.error);
|
|
92
|
+
if (err.response.statusText) return `${err.response.status || ''} ${err.response.statusText}`.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for data property
|
|
96
|
+
if (err.data?.message) return String(err.data.message);
|
|
97
|
+
if (err.data?.error) return String(err.data.error);
|
|
98
|
+
|
|
99
|
+
// Check for error property
|
|
100
|
+
if (err.error?.message) return String(err.error.message);
|
|
101
|
+
|
|
102
|
+
// Check for statusText with status code
|
|
103
|
+
if (err.status && err.statusText) {
|
|
104
|
+
return `HTTP ${err.status}: ${err.statusText}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (err.status && !err.statusText) {
|
|
108
|
+
return `HTTP ${err.status} Error`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for message property (not generic)
|
|
112
|
+
if (err.message && err.message !== 'An unknown error occurred.') {
|
|
113
|
+
return String(err.message);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Try to stringify the error object
|
|
117
|
+
try {
|
|
118
|
+
const stringified = JSON.stringify(error);
|
|
119
|
+
if (stringified !== '{}') return stringified;
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore JSON.stringify errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// String error
|
|
126
|
+
if (typeof error === 'string') {
|
|
127
|
+
return error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fallback
|
|
131
|
+
return 'An unknown error occurred';
|
|
132
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @thorprovider/adapters v2.0
|
|
4
|
+
* Fetch Utilities - Retry logic, timeouts, and error handling
|
|
5
|
+
*
|
|
6
|
+
* This module provides enhanced fetch capabilities with:
|
|
7
|
+
* - Automatic retries with exponential backoff
|
|
8
|
+
* - Configurable timeouts
|
|
9
|
+
* - Better error handling for network failures
|
|
10
|
+
*/
|
|
11
|
+
var __assign = (this && this.__assign) || function () {
|
|
12
|
+
__assign = Object.assign || function(t) {
|
|
13
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
14
|
+
s = arguments[i];
|
|
15
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
16
|
+
t[p] = s[p];
|
|
17
|
+
}
|
|
18
|
+
return t;
|
|
19
|
+
};
|
|
20
|
+
return __assign.apply(this, arguments);
|
|
21
|
+
};
|
|
22
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
23
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
24
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
25
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
26
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
27
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
28
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
32
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
33
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
34
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
35
|
+
function step(op) {
|
|
36
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
37
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
38
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
39
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
40
|
+
switch (op[0]) {
|
|
41
|
+
case 0: case 1: t = op; break;
|
|
42
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
43
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
44
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
45
|
+
default:
|
|
46
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
47
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
48
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
49
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
50
|
+
if (t[2]) _.ops.pop();
|
|
51
|
+
_.trys.pop(); continue;
|
|
52
|
+
}
|
|
53
|
+
op = body.call(thisArg, _);
|
|
54
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
55
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
exports.fetchWithRetry = fetchWithRetry;
|
|
60
|
+
exports.createFetchWithRetry = createFetchWithRetry;
|
|
61
|
+
var logger_1 = require("./logger");
|
|
62
|
+
var logger = (0, logger_1.createLogger)('FetchUtils');
|
|
63
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
64
|
+
maxRetries: 3,
|
|
65
|
+
initialDelayMs: 300,
|
|
66
|
+
backoffMultiplier: 2,
|
|
67
|
+
maxDelayMs: 5000,
|
|
68
|
+
timeoutMs: 10000, // 10 seconds
|
|
69
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Sleep utility for delays between retries
|
|
73
|
+
*/
|
|
74
|
+
function sleep(ms) {
|
|
75
|
+
return new Promise(function (resolve) { return setTimeout(resolve, ms); });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Calculate delay for exponential backoff
|
|
79
|
+
*/
|
|
80
|
+
function calculateDelay(attempt, config) {
|
|
81
|
+
var delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
|
|
82
|
+
return Math.min(delay, config.maxDelayMs);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if error is retryable
|
|
86
|
+
*/
|
|
87
|
+
function isRetryableError(error, config) {
|
|
88
|
+
var _a, _b;
|
|
89
|
+
// Network errors (fetch failed, connection refused, etc.)
|
|
90
|
+
if (error.name === 'TypeError' || ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('fetch failed'))) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
// Timeout errors
|
|
94
|
+
if (error.name === 'AbortError' || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('timeout'))) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// HTTP status codes
|
|
98
|
+
if (error.status && config.retryableStatusCodes.includes(error.status)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Enhanced fetch with retry logic and timeout
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* import { fetchWithRetry } from '@thorprovider/adapters/core/fetch-utils';
|
|
109
|
+
*
|
|
110
|
+
* // Basic usage with defaults (3 retries, 10s timeout)
|
|
111
|
+
* const response = await fetchWithRetry('https://api.example.com/products');
|
|
112
|
+
*
|
|
113
|
+
* // Custom configuration
|
|
114
|
+
* const response = await fetchWithRetry(
|
|
115
|
+
* 'https://api.example.com/products',
|
|
116
|
+
* {
|
|
117
|
+
* method: 'POST',
|
|
118
|
+
* body: JSON.stringify({ name: 'Product' }),
|
|
119
|
+
* },
|
|
120
|
+
* {
|
|
121
|
+
* maxRetries: 5,
|
|
122
|
+
* timeoutMs: 15000, // 15 seconds
|
|
123
|
+
* initialDelayMs: 500,
|
|
124
|
+
* }
|
|
125
|
+
* );
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
function fetchWithRetry(url, init, retryConfig) {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
130
|
+
var config, lastError, _loop_1, attempt, state_1;
|
|
131
|
+
return __generator(this, function (_a) {
|
|
132
|
+
switch (_a.label) {
|
|
133
|
+
case 0:
|
|
134
|
+
config = __assign(__assign({}, DEFAULT_RETRY_CONFIG), retryConfig);
|
|
135
|
+
_loop_1 = function (attempt) {
|
|
136
|
+
var controller_1, timeoutId, fetchOptions, response, error, error_1, shouldRetry, delay;
|
|
137
|
+
return __generator(this, function (_b) {
|
|
138
|
+
switch (_b.label) {
|
|
139
|
+
case 0:
|
|
140
|
+
_b.trys.push([0, 2, , 6]);
|
|
141
|
+
controller_1 = new AbortController();
|
|
142
|
+
timeoutId = setTimeout(function () { return controller_1.abort(); }, config.timeoutMs);
|
|
143
|
+
fetchOptions = __assign(__assign({}, init), { signal: controller_1.signal });
|
|
144
|
+
logger.debug('Fetch attempt', {
|
|
145
|
+
url: url,
|
|
146
|
+
attempt: attempt + 1,
|
|
147
|
+
maxAttempts: config.maxRetries + 1,
|
|
148
|
+
timeout: config.timeoutMs,
|
|
149
|
+
});
|
|
150
|
+
return [4 /*yield*/, fetch(url, fetchOptions)];
|
|
151
|
+
case 1:
|
|
152
|
+
response = _b.sent();
|
|
153
|
+
// Clear timeout
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
// Check if status code is retryable
|
|
156
|
+
if (!response.ok && config.retryableStatusCodes.includes(response.status)) {
|
|
157
|
+
error = new Error("HTTP ".concat(response.status, ": ").concat(response.statusText));
|
|
158
|
+
error.status = response.status;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
// Success!
|
|
162
|
+
logger.debug('Fetch successful', { url: url, attempt: attempt + 1 });
|
|
163
|
+
return [2 /*return*/, { value: response }];
|
|
164
|
+
case 2:
|
|
165
|
+
error_1 = _b.sent();
|
|
166
|
+
lastError = error_1;
|
|
167
|
+
// Clear timeout if it exists
|
|
168
|
+
if (error_1.name === 'AbortError') {
|
|
169
|
+
lastError = new Error("Request timeout after ".concat(config.timeoutMs, "ms"));
|
|
170
|
+
lastError.name = 'TimeoutError';
|
|
171
|
+
}
|
|
172
|
+
shouldRetry = attempt < config.maxRetries && isRetryableError(error_1, config);
|
|
173
|
+
if (!shouldRetry) return [3 /*break*/, 4];
|
|
174
|
+
delay = calculateDelay(attempt, config);
|
|
175
|
+
logger.warn('Fetch failed, retrying...', {
|
|
176
|
+
url: url,
|
|
177
|
+
attempt: attempt + 1,
|
|
178
|
+
maxAttempts: config.maxRetries + 1,
|
|
179
|
+
nextRetryIn: "".concat(delay, "ms"),
|
|
180
|
+
error: error_1.message,
|
|
181
|
+
});
|
|
182
|
+
return [4 /*yield*/, sleep(delay)];
|
|
183
|
+
case 3:
|
|
184
|
+
_b.sent();
|
|
185
|
+
return [3 /*break*/, 5];
|
|
186
|
+
case 4:
|
|
187
|
+
logger.error('Fetch failed after all retries', {
|
|
188
|
+
url: url,
|
|
189
|
+
totalAttempts: attempt + 1,
|
|
190
|
+
error: error_1.message,
|
|
191
|
+
});
|
|
192
|
+
return [2 /*return*/, "break"];
|
|
193
|
+
case 5: return [3 /*break*/, 6];
|
|
194
|
+
case 6: return [2 /*return*/];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
attempt = 0;
|
|
199
|
+
_a.label = 1;
|
|
200
|
+
case 1:
|
|
201
|
+
if (!(attempt <= config.maxRetries)) return [3 /*break*/, 4];
|
|
202
|
+
return [5 /*yield**/, _loop_1(attempt)];
|
|
203
|
+
case 2:
|
|
204
|
+
state_1 = _a.sent();
|
|
205
|
+
if (typeof state_1 === "object")
|
|
206
|
+
return [2 /*return*/, state_1.value];
|
|
207
|
+
if (state_1 === "break")
|
|
208
|
+
return [3 /*break*/, 4];
|
|
209
|
+
_a.label = 3;
|
|
210
|
+
case 3:
|
|
211
|
+
attempt++;
|
|
212
|
+
return [3 /*break*/, 1];
|
|
213
|
+
case 4:
|
|
214
|
+
// All retries exhausted, throw the last error
|
|
215
|
+
throw lastError;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Create a fetch function with custom retry configuration
|
|
222
|
+
* Useful for creating a configured fetch instance
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```typescript
|
|
226
|
+
* import { createFetchWithRetry } from '@thorprovider/adapters/core/fetch-utils';
|
|
227
|
+
*
|
|
228
|
+
* // Create configured fetch
|
|
229
|
+
* const customFetch = createFetchWithRetry({
|
|
230
|
+
* maxRetries: 5,
|
|
231
|
+
* timeoutMs: 15000,
|
|
232
|
+
* });
|
|
233
|
+
*
|
|
234
|
+
* // Use it like regular fetch
|
|
235
|
+
* const response = await customFetch('https://api.example.com/products');
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
function createFetchWithRetry(retryConfig) {
|
|
239
|
+
return function (url, init) { return fetchWithRetry(url, init, retryConfig); };
|
|
240
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @thorprovider/adapters v2.0
|
|
3
|
+
* Fetch Utilities - Retry logic, timeouts, and error handling
|
|
4
|
+
*
|
|
5
|
+
* This module provides enhanced fetch capabilities with:
|
|
6
|
+
* - Automatic retries with exponential backoff
|
|
7
|
+
* - Configurable timeouts
|
|
8
|
+
* - Better error handling for network failures
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createLogger } from './logger';
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('FetchUtils');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for fetch retry behavior
|
|
17
|
+
*/
|
|
18
|
+
export interface RetryConfig {
|
|
19
|
+
/**
|
|
20
|
+
* Maximum number of retry attempts (default: 3)
|
|
21
|
+
*/
|
|
22
|
+
maxRetries?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initial delay in milliseconds before first retry (default: 300ms)
|
|
26
|
+
*/
|
|
27
|
+
initialDelayMs?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Multiplier for exponential backoff (default: 2)
|
|
31
|
+
*/
|
|
32
|
+
backoffMultiplier?: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum delay between retries in milliseconds (default: 5000ms)
|
|
36
|
+
*/
|
|
37
|
+
maxDelayMs?: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Timeout for each fetch attempt in milliseconds (default: 10000ms - 10 seconds)
|
|
41
|
+
*/
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* HTTP status codes that should trigger a retry (default: [408, 429, 500, 502, 503, 504])
|
|
46
|
+
*/
|
|
47
|
+
retryableStatusCodes?: number[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
|
|
51
|
+
maxRetries: 3,
|
|
52
|
+
initialDelayMs: 300,
|
|
53
|
+
backoffMultiplier: 2,
|
|
54
|
+
maxDelayMs: 5000,
|
|
55
|
+
timeoutMs: 10000, // 10 seconds
|
|
56
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sleep utility for delays between retries
|
|
61
|
+
*/
|
|
62
|
+
function sleep(ms: number): Promise<void> {
|
|
63
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Calculate delay for exponential backoff
|
|
68
|
+
*/
|
|
69
|
+
function calculateDelay(
|
|
70
|
+
attempt: number,
|
|
71
|
+
config: Required<RetryConfig>
|
|
72
|
+
): number {
|
|
73
|
+
const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
|
|
74
|
+
return Math.min(delay, config.maxDelayMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if error is retryable
|
|
79
|
+
*/
|
|
80
|
+
function isRetryableError(error: any, config: Required<RetryConfig>): boolean {
|
|
81
|
+
// Network errors (fetch failed, connection refused, etc.)
|
|
82
|
+
if (error.name === 'TypeError' || error.message?.includes('fetch failed')) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Timeout errors
|
|
87
|
+
if (error.name === 'AbortError' || error.message?.includes('timeout')) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// HTTP status codes
|
|
92
|
+
if (error.status && config.retryableStatusCodes.includes(error.status)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Enhanced fetch with retry logic and timeout
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* import { fetchWithRetry } from '@thorprovider/adapters/core/fetch-utils';
|
|
105
|
+
*
|
|
106
|
+
* // Basic usage with defaults (3 retries, 10s timeout)
|
|
107
|
+
* const response = await fetchWithRetry('https://api.example.com/products');
|
|
108
|
+
*
|
|
109
|
+
* // Custom configuration
|
|
110
|
+
* const response = await fetchWithRetry(
|
|
111
|
+
* 'https://api.example.com/products',
|
|
112
|
+
* {
|
|
113
|
+
* method: 'POST',
|
|
114
|
+
* body: JSON.stringify({ name: 'Product' }),
|
|
115
|
+
* },
|
|
116
|
+
* {
|
|
117
|
+
* maxRetries: 5,
|
|
118
|
+
* timeoutMs: 15000, // 15 seconds
|
|
119
|
+
* initialDelayMs: 500,
|
|
120
|
+
* }
|
|
121
|
+
* );
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export async function fetchWithRetry(
|
|
125
|
+
url: string,
|
|
126
|
+
init?: RequestInit,
|
|
127
|
+
retryConfig?: RetryConfig
|
|
128
|
+
): Promise<Response> {
|
|
129
|
+
const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
|
|
130
|
+
let lastError: any;
|
|
131
|
+
|
|
132
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
// Create AbortController for timeout
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
137
|
+
|
|
138
|
+
const fetchOptions: RequestInit = {
|
|
139
|
+
...init,
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
logger.debug('Fetch attempt', {
|
|
144
|
+
url,
|
|
145
|
+
attempt: attempt + 1,
|
|
146
|
+
maxAttempts: config.maxRetries + 1,
|
|
147
|
+
timeout: config.timeoutMs,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Execute fetch
|
|
151
|
+
const response = await fetch(url, fetchOptions);
|
|
152
|
+
|
|
153
|
+
// Clear timeout
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
|
|
156
|
+
// Check if status code is retryable
|
|
157
|
+
if (!response.ok && config.retryableStatusCodes.includes(response.status)) {
|
|
158
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
159
|
+
(error as any).status = response.status;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Success!
|
|
164
|
+
logger.debug('Fetch successful', { url, attempt: attempt + 1 });
|
|
165
|
+
return response;
|
|
166
|
+
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
lastError = error;
|
|
169
|
+
|
|
170
|
+
// Clear timeout if it exists
|
|
171
|
+
if (error.name === 'AbortError') {
|
|
172
|
+
lastError = new Error(`Request timeout after ${config.timeoutMs}ms`);
|
|
173
|
+
lastError.name = 'TimeoutError';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if we should retry
|
|
177
|
+
const shouldRetry = attempt < config.maxRetries && isRetryableError(error, config);
|
|
178
|
+
|
|
179
|
+
if (shouldRetry) {
|
|
180
|
+
const delay = calculateDelay(attempt, config);
|
|
181
|
+
logger.warn('Fetch failed, retrying...', {
|
|
182
|
+
url,
|
|
183
|
+
attempt: attempt + 1,
|
|
184
|
+
maxAttempts: config.maxRetries + 1,
|
|
185
|
+
nextRetryIn: `${delay}ms`,
|
|
186
|
+
error: error.message,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await sleep(delay);
|
|
190
|
+
} else {
|
|
191
|
+
logger.error('Fetch failed after all retries', {
|
|
192
|
+
url,
|
|
193
|
+
totalAttempts: attempt + 1,
|
|
194
|
+
error: error.message,
|
|
195
|
+
});
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// All retries exhausted, throw the last error
|
|
202
|
+
throw lastError;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create a fetch function with custom retry configuration
|
|
207
|
+
* Useful for creating a configured fetch instance
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* import { createFetchWithRetry } from '@thorprovider/adapters/core/fetch-utils';
|
|
212
|
+
*
|
|
213
|
+
* // Create configured fetch
|
|
214
|
+
* const customFetch = createFetchWithRetry({
|
|
215
|
+
* maxRetries: 5,
|
|
216
|
+
* timeoutMs: 15000,
|
|
217
|
+
* });
|
|
218
|
+
*
|
|
219
|
+
* // Use it like regular fetch
|
|
220
|
+
* const response = await customFetch('https://api.example.com/products');
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export function createFetchWithRetry(
|
|
224
|
+
retryConfig?: RetryConfig
|
|
225
|
+
): (url: string, init?: RequestInit) => Promise<Response> {
|
|
226
|
+
return (url: string, init?: RequestInit) => fetchWithRetry(url, init, retryConfig);
|
|
227
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @thorprovider/adapters v2.0
|
|
4
|
+
* Core logger utility
|
|
5
|
+
*/
|
|
6
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
7
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
8
|
+
if (ar || !(i in from)) {
|
|
9
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
10
|
+
ar[i] = from[i];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.createLogger = createLogger;
|
|
17
|
+
/**
|
|
18
|
+
* Create a logger instance
|
|
19
|
+
*/
|
|
20
|
+
function createLogger(namespace, enabled) {
|
|
21
|
+
if (enabled === void 0) { enabled = false; }
|
|
22
|
+
var log = function (level, message) {
|
|
23
|
+
var args = [];
|
|
24
|
+
for (var _i = 2; _i < arguments.length; _i++) {
|
|
25
|
+
args[_i - 2] = arguments[_i];
|
|
26
|
+
}
|
|
27
|
+
if (!enabled && level !== 'error')
|
|
28
|
+
return;
|
|
29
|
+
var timestamp = new Date().toISOString();
|
|
30
|
+
var prefix = "[".concat(timestamp, "] [@thorprovider/adapters:").concat(namespace, "] ").concat(level.toUpperCase(), ":");
|
|
31
|
+
console[level].apply(console, __spreadArray([prefix, message], args, false));
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
debug: function (message) {
|
|
35
|
+
var args = [];
|
|
36
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
37
|
+
args[_i - 1] = arguments[_i];
|
|
38
|
+
}
|
|
39
|
+
return log.apply(void 0, __spreadArray(['debug', message], args, false));
|
|
40
|
+
},
|
|
41
|
+
info: function (message) {
|
|
42
|
+
var args = [];
|
|
43
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
44
|
+
args[_i - 1] = arguments[_i];
|
|
45
|
+
}
|
|
46
|
+
return log.apply(void 0, __spreadArray(['info', message], args, false));
|
|
47
|
+
},
|
|
48
|
+
warn: function (message) {
|
|
49
|
+
var args = [];
|
|
50
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
51
|
+
args[_i - 1] = arguments[_i];
|
|
52
|
+
}
|
|
53
|
+
return log.apply(void 0, __spreadArray(['warn', message], args, false));
|
|
54
|
+
},
|
|
55
|
+
error: function (message) {
|
|
56
|
+
var args = [];
|
|
57
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
58
|
+
args[_i - 1] = arguments[_i];
|
|
59
|
+
}
|
|
60
|
+
return log.apply(void 0, __spreadArray(['error', message], args, false));
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|