@http-client-toolkit/core 0.0.1
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/LICENSE +15 -0
- package/README.md +160 -0
- package/lib/index.cjs +615 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +382 -0
- package/lib/index.d.ts +382 -0
- package/lib/index.js +604 -0
- package/lib/index.js.map +1 -0
- package/package.json +76 -0
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
10
|
+
|
|
11
|
+
var __async = (__this, __arguments, generator) => {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
var fulfilled = (value) => {
|
|
14
|
+
try {
|
|
15
|
+
step(generator.next(value));
|
|
16
|
+
} catch (e) {
|
|
17
|
+
reject(e);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var rejected = (value) => {
|
|
21
|
+
try {
|
|
22
|
+
step(generator.throw(value));
|
|
23
|
+
} catch (e) {
|
|
24
|
+
reject(e);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
28
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/errors/http-client-error.ts
|
|
33
|
+
var HttpClientError = class extends Error {
|
|
34
|
+
constructor(message, statusCode) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "HttpClientError";
|
|
37
|
+
this.statusCode = statusCode;
|
|
38
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var AdaptiveConfigSchema = zod.z.object({
|
|
42
|
+
monitoringWindowMs: zod.z.number().positive().default(15 * 60 * 1e3),
|
|
43
|
+
// 15 minutes
|
|
44
|
+
highActivityThreshold: zod.z.number().min(0).default(10),
|
|
45
|
+
// requests per window
|
|
46
|
+
moderateActivityThreshold: zod.z.number().min(0).default(3),
|
|
47
|
+
recalculationIntervalMs: zod.z.number().positive().default(3e4),
|
|
48
|
+
// 30 seconds
|
|
49
|
+
sustainedInactivityThresholdMs: zod.z.number().positive().default(30 * 60 * 1e3),
|
|
50
|
+
// 30 minutes
|
|
51
|
+
backgroundPauseOnIncreasingTrend: zod.z.boolean().default(true),
|
|
52
|
+
maxUserScaling: zod.z.number().positive().default(2),
|
|
53
|
+
// don't exceed 2x capacity
|
|
54
|
+
minUserReserved: zod.z.number().min(0).default(5)
|
|
55
|
+
// requests minimum
|
|
56
|
+
}).refine(
|
|
57
|
+
(data) => {
|
|
58
|
+
return data.moderateActivityThreshold < data.highActivityThreshold;
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
message: "moderateActivityThreshold must be less than highActivityThreshold"
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
function hashRequest(endpoint, params = {}) {
|
|
65
|
+
const requestString = JSON.stringify({
|
|
66
|
+
endpoint,
|
|
67
|
+
params: sortObject(params)
|
|
68
|
+
});
|
|
69
|
+
return crypto.createHash("sha256").update(requestString).digest("hex");
|
|
70
|
+
}
|
|
71
|
+
function sortObject(obj) {
|
|
72
|
+
if (obj === null) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const objType = typeof obj;
|
|
76
|
+
if (objType === "undefined" || objType === "string") {
|
|
77
|
+
return obj;
|
|
78
|
+
}
|
|
79
|
+
if (objType === "number" || objType === "boolean") {
|
|
80
|
+
return String(obj);
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(obj)) {
|
|
83
|
+
return obj.map(sortObject);
|
|
84
|
+
}
|
|
85
|
+
const sorted = {};
|
|
86
|
+
const keys = Object.keys(obj).sort();
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
const value = obj[key];
|
|
89
|
+
const normalisedValue = sortObject(value);
|
|
90
|
+
if (normalisedValue !== void 0) {
|
|
91
|
+
sorted[key] = normalisedValue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return sorted;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/stores/rate-limit-config.ts
|
|
98
|
+
var DEFAULT_RATE_LIMIT = {
|
|
99
|
+
limit: 60,
|
|
100
|
+
windowMs: 6e4
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/stores/adaptive-capacity-calculator.ts
|
|
104
|
+
var AdaptiveCapacityCalculator = class {
|
|
105
|
+
constructor(config = {}) {
|
|
106
|
+
this.config = AdaptiveConfigSchema.parse(config);
|
|
107
|
+
}
|
|
108
|
+
calculateDynamicCapacity(resource, totalLimit, activityMetrics) {
|
|
109
|
+
const recentUserActivity = this.getRecentActivity(
|
|
110
|
+
activityMetrics.recentUserRequests
|
|
111
|
+
);
|
|
112
|
+
const activityTrend = this.calculateActivityTrend(
|
|
113
|
+
activityMetrics.recentUserRequests
|
|
114
|
+
);
|
|
115
|
+
if (recentUserActivity >= this.config.highActivityThreshold) {
|
|
116
|
+
const userCapacity = Math.min(
|
|
117
|
+
totalLimit * 0.9,
|
|
118
|
+
Math.floor(totalLimit * 0.5 * this.config.maxUserScaling)
|
|
119
|
+
// 50% base * scaling factor
|
|
120
|
+
);
|
|
121
|
+
return {
|
|
122
|
+
userReserved: userCapacity,
|
|
123
|
+
backgroundMax: totalLimit - userCapacity,
|
|
124
|
+
backgroundPaused: this.config.backgroundPauseOnIncreasingTrend && activityTrend === "increasing",
|
|
125
|
+
reason: `High user activity (${recentUserActivity} requests/${this.config.monitoringWindowMs / 6e4}min) - prioritizing users`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (recentUserActivity >= this.config.moderateActivityThreshold) {
|
|
129
|
+
const userMultiplier = this.getUserMultiplier(
|
|
130
|
+
recentUserActivity,
|
|
131
|
+
activityTrend
|
|
132
|
+
);
|
|
133
|
+
const baseUserCapacity2 = Math.floor(totalLimit * 0.4);
|
|
134
|
+
const dynamicUserCapacity = Math.min(
|
|
135
|
+
totalLimit * 0.7,
|
|
136
|
+
baseUserCapacity2 * userMultiplier
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
userReserved: dynamicUserCapacity,
|
|
140
|
+
backgroundMax: totalLimit - dynamicUserCapacity,
|
|
141
|
+
backgroundPaused: false,
|
|
142
|
+
reason: `Moderate user activity - dynamic scaling (${userMultiplier.toFixed(1)}x user capacity)`
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (recentUserActivity === 0) {
|
|
146
|
+
if (activityMetrics.recentUserRequests.length === 0 && activityMetrics.recentBackgroundRequests.length === 0) {
|
|
147
|
+
const baseUserCapacity2 = Math.floor(totalLimit * 0.3);
|
|
148
|
+
return {
|
|
149
|
+
userReserved: Math.max(baseUserCapacity2, this.config.minUserReserved),
|
|
150
|
+
backgroundMax: totalLimit - Math.max(baseUserCapacity2, this.config.minUserReserved),
|
|
151
|
+
backgroundPaused: false,
|
|
152
|
+
reason: "Initial state - default capacity allocation"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (activityMetrics.recentUserRequests.length === 0) {
|
|
156
|
+
return {
|
|
157
|
+
userReserved: this.config.minUserReserved,
|
|
158
|
+
// Minimal safety buffer
|
|
159
|
+
backgroundMax: totalLimit - this.config.minUserReserved,
|
|
160
|
+
backgroundPaused: false,
|
|
161
|
+
reason: "No user activity yet - background scale up with minimal user buffer"
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const sustainedInactivity = this.getSustainedInactivityPeriod(
|
|
165
|
+
activityMetrics.recentUserRequests
|
|
166
|
+
);
|
|
167
|
+
if (sustainedInactivity > this.config.sustainedInactivityThresholdMs) {
|
|
168
|
+
return {
|
|
169
|
+
userReserved: 0,
|
|
170
|
+
// No reservation - background gets everything!
|
|
171
|
+
backgroundMax: totalLimit,
|
|
172
|
+
// Full capacity available
|
|
173
|
+
backgroundPaused: false,
|
|
174
|
+
reason: `Sustained zero activity (${Math.floor(sustainedInactivity / 6e4)}+ min) - full capacity to background`
|
|
175
|
+
};
|
|
176
|
+
} else {
|
|
177
|
+
return {
|
|
178
|
+
userReserved: this.config.minUserReserved,
|
|
179
|
+
// Minimal safety buffer
|
|
180
|
+
backgroundMax: totalLimit - this.config.minUserReserved,
|
|
181
|
+
backgroundPaused: false,
|
|
182
|
+
reason: "Recent zero activity - background scale up with minimal user buffer"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const baseUserCapacity = Math.floor(totalLimit * 0.3);
|
|
187
|
+
return {
|
|
188
|
+
userReserved: Math.max(baseUserCapacity, this.config.minUserReserved),
|
|
189
|
+
backgroundMax: totalLimit - Math.max(baseUserCapacity, this.config.minUserReserved),
|
|
190
|
+
backgroundPaused: false,
|
|
191
|
+
reason: `Low user activity (${recentUserActivity} requests/${this.config.monitoringWindowMs / 6e4}min) - background scale up`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
getRecentActivity(requests) {
|
|
195
|
+
const cutoff = Date.now() - this.config.monitoringWindowMs;
|
|
196
|
+
return requests.filter((timestamp) => timestamp > cutoff).length;
|
|
197
|
+
}
|
|
198
|
+
calculateActivityTrend(requests) {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
const windowSize = this.config.monitoringWindowMs / 3;
|
|
201
|
+
const recent = requests.filter((t) => t > now - windowSize).length;
|
|
202
|
+
const previous = requests.filter(
|
|
203
|
+
(t) => t > now - 2 * windowSize && t <= now - windowSize
|
|
204
|
+
).length;
|
|
205
|
+
if (recent === 0 && previous === 0) return "none";
|
|
206
|
+
if (recent > previous * 1.5) return "increasing";
|
|
207
|
+
if (recent < previous * 0.5) return "decreasing";
|
|
208
|
+
return "stable";
|
|
209
|
+
}
|
|
210
|
+
getUserMultiplier(activity, trend) {
|
|
211
|
+
let base = Math.min(
|
|
212
|
+
this.config.maxUserScaling,
|
|
213
|
+
1 + activity / this.config.highActivityThreshold
|
|
214
|
+
);
|
|
215
|
+
if (trend === "increasing") base *= 1.2;
|
|
216
|
+
if (trend === "decreasing") base *= 0.8;
|
|
217
|
+
return Math.max(1, base);
|
|
218
|
+
}
|
|
219
|
+
getSustainedInactivityPeriod(requests) {
|
|
220
|
+
if (requests.length === 0) {
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
const lastRequest = Math.max(...requests);
|
|
224
|
+
return Date.now() - lastRequest;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/http-client/http-client.ts
|
|
229
|
+
var DEFAULT_RATE_LIMIT_HEADER_NAMES = {
|
|
230
|
+
retryAfter: ["retry-after"],
|
|
231
|
+
limit: ["ratelimit-limit", "x-ratelimit-limit", "rate-limit-limit"],
|
|
232
|
+
remaining: [
|
|
233
|
+
"ratelimit-remaining",
|
|
234
|
+
"x-ratelimit-remaining",
|
|
235
|
+
"rate-limit-remaining"
|
|
236
|
+
],
|
|
237
|
+
reset: ["ratelimit-reset", "x-ratelimit-reset", "rate-limit-reset"],
|
|
238
|
+
combined: ["ratelimit"]
|
|
239
|
+
};
|
|
240
|
+
function wait(ms, signal) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const timer = setTimeout(() => {
|
|
243
|
+
if (signal) {
|
|
244
|
+
signal.removeEventListener("abort", onAbort);
|
|
245
|
+
}
|
|
246
|
+
resolve();
|
|
247
|
+
}, ms);
|
|
248
|
+
function onAbort() {
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
const err = new Error("Aborted");
|
|
251
|
+
err.name = "AbortError";
|
|
252
|
+
reject(err);
|
|
253
|
+
}
|
|
254
|
+
if (signal) {
|
|
255
|
+
if (signal.aborted) {
|
|
256
|
+
onAbort();
|
|
257
|
+
} else {
|
|
258
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
var HttpClient = class {
|
|
264
|
+
constructor(stores = {}, options = {}) {
|
|
265
|
+
this.serverCooldowns = /* @__PURE__ */ new Map();
|
|
266
|
+
var _a, _b, _c;
|
|
267
|
+
this._http = axios__default.default.create();
|
|
268
|
+
this.stores = stores;
|
|
269
|
+
this.options = {
|
|
270
|
+
defaultCacheTTL: (_a = options.defaultCacheTTL) != null ? _a : 3600,
|
|
271
|
+
throwOnRateLimit: (_b = options.throwOnRateLimit) != null ? _b : true,
|
|
272
|
+
maxWaitTime: (_c = options.maxWaitTime) != null ? _c : 6e4,
|
|
273
|
+
responseTransformer: options.responseTransformer,
|
|
274
|
+
errorHandler: options.errorHandler,
|
|
275
|
+
responseHandler: options.responseHandler,
|
|
276
|
+
rateLimitHeaders: this.normalizeRateLimitHeaders(
|
|
277
|
+
options.rateLimitHeaders
|
|
278
|
+
)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
normalizeRateLimitHeaders(customHeaders) {
|
|
282
|
+
return {
|
|
283
|
+
retryAfter: this.normalizeHeaderNames(
|
|
284
|
+
customHeaders == null ? void 0 : customHeaders.retryAfter,
|
|
285
|
+
DEFAULT_RATE_LIMIT_HEADER_NAMES.retryAfter
|
|
286
|
+
),
|
|
287
|
+
limit: this.normalizeHeaderNames(
|
|
288
|
+
customHeaders == null ? void 0 : customHeaders.limit,
|
|
289
|
+
DEFAULT_RATE_LIMIT_HEADER_NAMES.limit
|
|
290
|
+
),
|
|
291
|
+
remaining: this.normalizeHeaderNames(
|
|
292
|
+
customHeaders == null ? void 0 : customHeaders.remaining,
|
|
293
|
+
DEFAULT_RATE_LIMIT_HEADER_NAMES.remaining
|
|
294
|
+
),
|
|
295
|
+
reset: this.normalizeHeaderNames(
|
|
296
|
+
customHeaders == null ? void 0 : customHeaders.reset,
|
|
297
|
+
DEFAULT_RATE_LIMIT_HEADER_NAMES.reset
|
|
298
|
+
),
|
|
299
|
+
combined: this.normalizeHeaderNames(
|
|
300
|
+
customHeaders == null ? void 0 : customHeaders.combined,
|
|
301
|
+
DEFAULT_RATE_LIMIT_HEADER_NAMES.combined
|
|
302
|
+
)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
normalizeHeaderNames(providedNames, defaultNames) {
|
|
306
|
+
if (!providedNames || providedNames.length === 0) {
|
|
307
|
+
return [...defaultNames];
|
|
308
|
+
}
|
|
309
|
+
const customNames = providedNames.map((name) => name.trim().toLowerCase()).filter(Boolean);
|
|
310
|
+
if (customNames.length === 0) {
|
|
311
|
+
return [...defaultNames];
|
|
312
|
+
}
|
|
313
|
+
return [.../* @__PURE__ */ new Set([...customNames, ...defaultNames])];
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Infer the resource name from the endpoint URL
|
|
317
|
+
* @param url The full URL or endpoint path
|
|
318
|
+
* @returns The resource name for rate limiting
|
|
319
|
+
*/
|
|
320
|
+
inferResource(url) {
|
|
321
|
+
try {
|
|
322
|
+
const urlObj = new URL(url);
|
|
323
|
+
const segments = urlObj.pathname.split("/").filter(Boolean);
|
|
324
|
+
return segments[segments.length - 1] || "unknown";
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return "unknown";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Extract endpoint and params from URL for request hashing
|
|
331
|
+
* @param url The full URL
|
|
332
|
+
* @returns Object with endpoint and params for hashing
|
|
333
|
+
*/
|
|
334
|
+
parseUrlForHashing(url) {
|
|
335
|
+
const urlObj = new URL(url);
|
|
336
|
+
const endpoint = `${urlObj.origin}${urlObj.pathname}`;
|
|
337
|
+
const params = {};
|
|
338
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
339
|
+
const existing = params[key];
|
|
340
|
+
if (existing === void 0) {
|
|
341
|
+
params[key] = value;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (Array.isArray(existing)) {
|
|
345
|
+
existing.push(value);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
params[key] = [existing, value];
|
|
349
|
+
});
|
|
350
|
+
return { endpoint, params };
|
|
351
|
+
}
|
|
352
|
+
getOriginScope(url) {
|
|
353
|
+
try {
|
|
354
|
+
return new URL(url).origin;
|
|
355
|
+
} catch (e) {
|
|
356
|
+
return "unknown";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
getHeaderValue(headers, names) {
|
|
360
|
+
var _a;
|
|
361
|
+
if (!headers) {
|
|
362
|
+
return void 0;
|
|
363
|
+
}
|
|
364
|
+
for (const rawName of names) {
|
|
365
|
+
const name = rawName.toLowerCase();
|
|
366
|
+
const value = (_a = headers[name]) != null ? _a : headers[rawName];
|
|
367
|
+
if (typeof value === "string") {
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
371
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
372
|
+
if (typeof first === "string") {
|
|
373
|
+
return first;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return void 0;
|
|
378
|
+
}
|
|
379
|
+
parseIntegerHeader(value) {
|
|
380
|
+
if (!value) {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
384
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
return parsed;
|
|
388
|
+
}
|
|
389
|
+
parseRetryAfterMs(value) {
|
|
390
|
+
if (!value) {
|
|
391
|
+
return void 0;
|
|
392
|
+
}
|
|
393
|
+
const numeric = Number.parseInt(value.trim(), 10);
|
|
394
|
+
if (Number.isFinite(numeric) && numeric >= 0) {
|
|
395
|
+
return numeric * 1e3;
|
|
396
|
+
}
|
|
397
|
+
const dateMs = Date.parse(value);
|
|
398
|
+
if (!Number.isFinite(dateMs)) {
|
|
399
|
+
return void 0;
|
|
400
|
+
}
|
|
401
|
+
return Math.max(0, dateMs - Date.now());
|
|
402
|
+
}
|
|
403
|
+
parseResetMs(value) {
|
|
404
|
+
const parsed = this.parseIntegerHeader(value);
|
|
405
|
+
if (parsed === void 0) {
|
|
406
|
+
return void 0;
|
|
407
|
+
}
|
|
408
|
+
if (parsed === 0) {
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
412
|
+
if (parsed > nowSeconds + 1) {
|
|
413
|
+
return Math.max(0, (parsed - nowSeconds) * 1e3);
|
|
414
|
+
}
|
|
415
|
+
return parsed * 1e3;
|
|
416
|
+
}
|
|
417
|
+
parseCombinedRateLimitHeader(value) {
|
|
418
|
+
if (!value) {
|
|
419
|
+
return {};
|
|
420
|
+
}
|
|
421
|
+
const remainingMatch = value.match(/(?:^|[;,])\s*r\s*=\s*(\d+)/i);
|
|
422
|
+
const resetMatch = value.match(/(?:^|[;,])\s*t\s*=\s*(\d+)/i);
|
|
423
|
+
return {
|
|
424
|
+
remaining: remainingMatch ? this.parseIntegerHeader(remainingMatch[1]) : void 0,
|
|
425
|
+
resetMs: resetMatch ? this.parseResetMs(resetMatch[1]) : void 0
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
applyServerRateLimitHints(url, headers, statusCode) {
|
|
429
|
+
if (!headers) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const config = this.options.rateLimitHeaders;
|
|
433
|
+
const retryAfterRaw = this.getHeaderValue(headers, config.retryAfter);
|
|
434
|
+
const resetRaw = this.getHeaderValue(headers, config.reset);
|
|
435
|
+
const remainingRaw = this.getHeaderValue(headers, config.remaining);
|
|
436
|
+
const combinedRaw = this.getHeaderValue(headers, config.combined);
|
|
437
|
+
const retryAfterMs = this.parseRetryAfterMs(retryAfterRaw);
|
|
438
|
+
const resetMs = this.parseResetMs(resetRaw);
|
|
439
|
+
const remaining = this.parseIntegerHeader(remainingRaw);
|
|
440
|
+
const combined = this.parseCombinedRateLimitHeader(combinedRaw);
|
|
441
|
+
const effectiveRemaining = remaining != null ? remaining : combined.remaining;
|
|
442
|
+
const effectiveResetMs = resetMs != null ? resetMs : combined.resetMs;
|
|
443
|
+
const hasRateLimitErrorStatus = statusCode === 429 || statusCode === 503;
|
|
444
|
+
let waitMs;
|
|
445
|
+
if (retryAfterMs !== void 0) {
|
|
446
|
+
waitMs = retryAfterMs;
|
|
447
|
+
} else if (effectiveResetMs !== void 0 && (hasRateLimitErrorStatus || effectiveRemaining !== void 0 && effectiveRemaining <= 0)) {
|
|
448
|
+
waitMs = effectiveResetMs;
|
|
449
|
+
}
|
|
450
|
+
if (waitMs === void 0 || waitMs <= 0) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const scope = this.getOriginScope(url);
|
|
454
|
+
this.serverCooldowns.set(scope, Date.now() + waitMs);
|
|
455
|
+
}
|
|
456
|
+
enforceServerCooldown(url, signal) {
|
|
457
|
+
return __async(this, null, function* () {
|
|
458
|
+
const scope = this.getOriginScope(url);
|
|
459
|
+
const startedAt = Date.now();
|
|
460
|
+
while (true) {
|
|
461
|
+
const cooldownUntil = this.serverCooldowns.get(scope);
|
|
462
|
+
if (!cooldownUntil) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const waitMs = cooldownUntil - Date.now();
|
|
466
|
+
if (waitMs <= 0) {
|
|
467
|
+
this.serverCooldowns.delete(scope);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (this.options.throwOnRateLimit) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Rate limit exceeded for origin '${scope}'. Wait ${waitMs}ms before retrying.`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const elapsedMs = Date.now() - startedAt;
|
|
476
|
+
const remainingWaitBudgetMs = this.options.maxWaitTime - elapsedMs;
|
|
477
|
+
if (remainingWaitBudgetMs <= 0) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`Rate limit wait exceeded maxWaitTime (${this.options.maxWaitTime}ms) for origin '${scope}'.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
yield wait(Math.min(waitMs, remainingWaitBudgetMs), signal);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
enforceStoreRateLimit(resource, priority, signal) {
|
|
487
|
+
return __async(this, null, function* () {
|
|
488
|
+
const rateLimit = this.stores.rateLimit;
|
|
489
|
+
const startedAt = Date.now();
|
|
490
|
+
if (this.options.throwOnRateLimit) {
|
|
491
|
+
const canProceed = yield rateLimit.canProceed(resource, priority);
|
|
492
|
+
if (!canProceed) {
|
|
493
|
+
const waitTime = yield rateLimit.getWaitTime(resource, priority);
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Rate limit exceeded for resource '${resource}'. Wait ${waitTime}ms before retrying.`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
while (!(yield rateLimit.canProceed(resource, priority))) {
|
|
501
|
+
const suggestedWaitMs = yield rateLimit.getWaitTime(resource, priority);
|
|
502
|
+
const elapsedMs = Date.now() - startedAt;
|
|
503
|
+
const remainingWaitBudgetMs = this.options.maxWaitTime - elapsedMs;
|
|
504
|
+
if (remainingWaitBudgetMs <= 0) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Rate limit wait exceeded maxWaitTime (${this.options.maxWaitTime}ms) for resource '${resource}'.`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const waitTime = suggestedWaitMs > 0 ? Math.min(suggestedWaitMs, remainingWaitBudgetMs) : Math.min(25, remainingWaitBudgetMs);
|
|
510
|
+
yield wait(waitTime, signal);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
generateClientError(err) {
|
|
515
|
+
var _a, _b, _c;
|
|
516
|
+
if (this.options.errorHandler) {
|
|
517
|
+
return this.options.errorHandler(err);
|
|
518
|
+
}
|
|
519
|
+
if (err instanceof HttpClientError) {
|
|
520
|
+
return err;
|
|
521
|
+
}
|
|
522
|
+
const error = err;
|
|
523
|
+
const statusCode = (_a = error.response) == null ? void 0 : _a.status;
|
|
524
|
+
const errorMessage = (_c = (_b = error.response) == null ? void 0 : _b.data) == null ? void 0 : _c.message;
|
|
525
|
+
const message = `${error.message}${errorMessage ? `, ${errorMessage}` : ""}`;
|
|
526
|
+
return new HttpClientError(message, statusCode);
|
|
527
|
+
}
|
|
528
|
+
get(_0) {
|
|
529
|
+
return __async(this, arguments, function* (url, options = {}) {
|
|
530
|
+
const { signal, priority = "background" } = options;
|
|
531
|
+
const { endpoint, params } = this.parseUrlForHashing(url);
|
|
532
|
+
const hash = hashRequest(endpoint, params);
|
|
533
|
+
const resource = this.inferResource(url);
|
|
534
|
+
try {
|
|
535
|
+
yield this.enforceServerCooldown(url, signal);
|
|
536
|
+
if (this.stores.cache) {
|
|
537
|
+
const cachedResult = yield this.stores.cache.get(hash);
|
|
538
|
+
if (cachedResult !== void 0) {
|
|
539
|
+
return cachedResult;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (this.stores.dedupe) {
|
|
543
|
+
const existingResult = yield this.stores.dedupe.waitFor(hash);
|
|
544
|
+
if (existingResult !== void 0) {
|
|
545
|
+
return existingResult;
|
|
546
|
+
}
|
|
547
|
+
if (this.stores.dedupe.registerOrJoin) {
|
|
548
|
+
const registration = yield this.stores.dedupe.registerOrJoin(hash);
|
|
549
|
+
if (!registration.isOwner) {
|
|
550
|
+
const joinedResult = yield this.stores.dedupe.waitFor(hash);
|
|
551
|
+
if (joinedResult !== void 0) {
|
|
552
|
+
return joinedResult;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
yield this.stores.dedupe.register(hash);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (this.stores.rateLimit) {
|
|
560
|
+
yield this.enforceStoreRateLimit(resource, priority, signal);
|
|
561
|
+
}
|
|
562
|
+
const response = yield this._http.get(url, { signal });
|
|
563
|
+
this.applyServerRateLimitHints(
|
|
564
|
+
url,
|
|
565
|
+
response.headers,
|
|
566
|
+
response.status
|
|
567
|
+
);
|
|
568
|
+
let data = response.data;
|
|
569
|
+
if (this.options.responseTransformer && data) {
|
|
570
|
+
data = this.options.responseTransformer(data);
|
|
571
|
+
}
|
|
572
|
+
if (this.options.responseHandler) {
|
|
573
|
+
data = this.options.responseHandler(data);
|
|
574
|
+
}
|
|
575
|
+
const result = data;
|
|
576
|
+
if (this.stores.rateLimit) {
|
|
577
|
+
const rateLimit = this.stores.rateLimit;
|
|
578
|
+
yield rateLimit.record(resource, priority);
|
|
579
|
+
}
|
|
580
|
+
if (this.stores.cache) {
|
|
581
|
+
yield this.stores.cache.set(hash, result, this.options.defaultCacheTTL);
|
|
582
|
+
}
|
|
583
|
+
if (this.stores.dedupe) {
|
|
584
|
+
yield this.stores.dedupe.complete(hash, result);
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
const axiosError = error;
|
|
589
|
+
if (axiosError.response) {
|
|
590
|
+
this.applyServerRateLimitHints(
|
|
591
|
+
url,
|
|
592
|
+
axiosError.response.headers,
|
|
593
|
+
axiosError.response.status
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (this.stores.dedupe) {
|
|
597
|
+
yield this.stores.dedupe.fail(hash, error);
|
|
598
|
+
}
|
|
599
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
throw this.generateClientError(error);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
exports.AdaptiveCapacityCalculator = AdaptiveCapacityCalculator;
|
|
609
|
+
exports.AdaptiveConfigSchema = AdaptiveConfigSchema;
|
|
610
|
+
exports.DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT;
|
|
611
|
+
exports.HttpClient = HttpClient;
|
|
612
|
+
exports.HttpClientError = HttpClientError;
|
|
613
|
+
exports.hashRequest = hashRequest;
|
|
614
|
+
//# sourceMappingURL=index.cjs.map
|
|
615
|
+
//# sourceMappingURL=index.cjs.map
|