@profplum700/etsy-v3-api-client 2.3.12 → 2.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/README.md +5 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.esm.js +212 -40
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +1 -1
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +212 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +52 -2
- package/dist/index.esm.js +212 -40
- package/dist/index.esm.js.map +1 -1
- package/dist/node.cjs +212 -40
- package/dist/node.cjs.map +1 -1
- package/dist/node.esm.js +212 -40
- package/dist/node.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -238,14 +238,18 @@ class EtsyAuthError extends Error {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
class EtsyRateLimitError extends Error {
|
|
241
|
-
constructor(message, _retryAfter) {
|
|
241
|
+
constructor(message, _retryAfter, errorType = 'unknown') {
|
|
242
242
|
super(message);
|
|
243
243
|
this._retryAfter = _retryAfter;
|
|
244
244
|
this.name = 'EtsyRateLimitError';
|
|
245
|
+
this.errorType = errorType;
|
|
245
246
|
}
|
|
246
247
|
get retryAfter() {
|
|
247
248
|
return this._retryAfter;
|
|
248
249
|
}
|
|
250
|
+
isRetryable() {
|
|
251
|
+
return this.errorType !== 'qpd_exhausted';
|
|
252
|
+
}
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
const isBrowser$1 = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
@@ -678,10 +682,18 @@ class EtsyRateLimiter {
|
|
|
678
682
|
this.requestCount = 0;
|
|
679
683
|
this.dailyReset = new Date();
|
|
680
684
|
this.lastRequestTime = 0;
|
|
685
|
+
this.isHeaderBasedLimiting = false;
|
|
686
|
+
this.currentRetryCount = 0;
|
|
681
687
|
this.config = {
|
|
682
688
|
maxRequestsPerDay: 10000,
|
|
683
689
|
maxRequestsPerSecond: 10,
|
|
684
690
|
minRequestInterval: 100,
|
|
691
|
+
maxRetries: 3,
|
|
692
|
+
baseDelayMs: 1000,
|
|
693
|
+
maxDelayMs: 30000,
|
|
694
|
+
jitter: 0.1,
|
|
695
|
+
qpdWarningThreshold: 80,
|
|
696
|
+
onApproachingLimit: undefined,
|
|
685
697
|
...config
|
|
686
698
|
};
|
|
687
699
|
this.setNextDailyReset();
|
|
@@ -692,19 +704,124 @@ class EtsyRateLimiter {
|
|
|
692
704
|
this.dailyReset.setUTCDate(this.dailyReset.getUTCDate() + 1);
|
|
693
705
|
this.dailyReset.setUTCHours(0, 0, 0, 0);
|
|
694
706
|
}
|
|
707
|
+
updateFromHeaders(headers) {
|
|
708
|
+
if (!headers) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const parsed = this.parseRateLimitHeaders(headers);
|
|
712
|
+
if (parsed.limitPerSecond !== undefined) {
|
|
713
|
+
this.headerLimitPerSecond = parsed.limitPerSecond;
|
|
714
|
+
this.isHeaderBasedLimiting = true;
|
|
715
|
+
}
|
|
716
|
+
if (parsed.remainingThisSecond !== undefined) {
|
|
717
|
+
this.headerRemainingThisSecond = parsed.remainingThisSecond;
|
|
718
|
+
}
|
|
719
|
+
if (parsed.limitPerDay !== undefined) {
|
|
720
|
+
this.headerLimitPerDay = parsed.limitPerDay;
|
|
721
|
+
this.isHeaderBasedLimiting = true;
|
|
722
|
+
}
|
|
723
|
+
if (parsed.remainingToday !== undefined) {
|
|
724
|
+
this.headerRemainingToday = parsed.remainingToday;
|
|
725
|
+
this.checkApproachingLimit();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
parseRateLimitHeaders(headers) {
|
|
729
|
+
const getHeader = (name) => {
|
|
730
|
+
if (headers instanceof Headers) {
|
|
731
|
+
return headers.get(name);
|
|
732
|
+
}
|
|
733
|
+
const lowerName = name.toLowerCase();
|
|
734
|
+
for (const key of Object.keys(headers)) {
|
|
735
|
+
if (key.toLowerCase() === lowerName) {
|
|
736
|
+
return headers[key] ?? null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
};
|
|
741
|
+
const parseNumber = (value) => {
|
|
742
|
+
if (value === null)
|
|
743
|
+
return undefined;
|
|
744
|
+
const num = parseInt(value, 10);
|
|
745
|
+
return isNaN(num) ? undefined : num;
|
|
746
|
+
};
|
|
747
|
+
return {
|
|
748
|
+
limitPerSecond: parseNumber(getHeader('x-limit-per-second')),
|
|
749
|
+
remainingThisSecond: parseNumber(getHeader('x-remaining-this-second')),
|
|
750
|
+
limitPerDay: parseNumber(getHeader('x-limit-per-day')),
|
|
751
|
+
remainingToday: parseNumber(getHeader('x-remaining-today')),
|
|
752
|
+
retryAfter: parseNumber(getHeader('retry-after'))
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
checkApproachingLimit() {
|
|
756
|
+
if (!this.config.onApproachingLimit)
|
|
757
|
+
return;
|
|
758
|
+
const limit = this.headerLimitPerDay ?? this.config.maxRequestsPerDay;
|
|
759
|
+
const remaining = this.headerRemainingToday ?? this.getRemainingRequests();
|
|
760
|
+
const used = limit - remaining;
|
|
761
|
+
const percentageUsed = (used / limit) * 100;
|
|
762
|
+
if (percentageUsed >= this.config.qpdWarningThreshold) {
|
|
763
|
+
this.config.onApproachingLimit(remaining, limit, percentageUsed);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async handleRateLimitResponse(headers) {
|
|
767
|
+
const parsed = headers ? this.parseRateLimitHeaders(headers) : {};
|
|
768
|
+
this.updateFromHeaders(headers);
|
|
769
|
+
if (this.headerRemainingToday === 0) {
|
|
770
|
+
throw new EtsyRateLimitError('Daily rate limit exhausted. No requests remaining until limit resets.', parsed.retryAfter, 'qpd_exhausted');
|
|
771
|
+
}
|
|
772
|
+
this.currentRetryCount++;
|
|
773
|
+
if (this.currentRetryCount > this.config.maxRetries) {
|
|
774
|
+
this.currentRetryCount = 0;
|
|
775
|
+
throw new EtsyRateLimitError(`Max retries (${this.config.maxRetries}) exceeded for rate limit`, parsed.retryAfter, 'qps_exhausted');
|
|
776
|
+
}
|
|
777
|
+
const delayMs = this.calculateBackoffDelay(this.currentRetryCount, parsed.retryAfter);
|
|
778
|
+
return { shouldRetry: true, delayMs };
|
|
779
|
+
}
|
|
780
|
+
calculateBackoffDelay(attempt, retryAfterSeconds) {
|
|
781
|
+
const serverSuggestedMs = retryAfterSeconds ? retryAfterSeconds * 1000 : 0;
|
|
782
|
+
const exponentialDelay = this.config.baseDelayMs * Math.pow(2, attempt - 1);
|
|
783
|
+
let delay = Math.min(exponentialDelay, this.config.maxDelayMs);
|
|
784
|
+
delay = Math.max(delay, serverSuggestedMs);
|
|
785
|
+
if (this.config.jitter > 0) {
|
|
786
|
+
const jitterAmount = delay * this.config.jitter;
|
|
787
|
+
const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount;
|
|
788
|
+
delay += randomJitter;
|
|
789
|
+
}
|
|
790
|
+
return Math.max(0, Math.floor(delay));
|
|
791
|
+
}
|
|
792
|
+
resetRetryCount() {
|
|
793
|
+
this.currentRetryCount = 0;
|
|
794
|
+
}
|
|
795
|
+
setApproachingLimitCallback(callback) {
|
|
796
|
+
this.config.onApproachingLimit = callback;
|
|
797
|
+
}
|
|
798
|
+
setWarningThreshold(threshold) {
|
|
799
|
+
this.config.qpdWarningThreshold = threshold;
|
|
800
|
+
}
|
|
801
|
+
getEffectiveMinInterval() {
|
|
802
|
+
if (this.headerLimitPerSecond !== undefined && this.headerLimitPerSecond > 0) {
|
|
803
|
+
return Math.ceil(1000 / this.headerLimitPerSecond);
|
|
804
|
+
}
|
|
805
|
+
return this.config.minRequestInterval;
|
|
806
|
+
}
|
|
695
807
|
async waitForRateLimit() {
|
|
696
808
|
const now = Date.now();
|
|
697
809
|
if (now >= this.dailyReset.getTime()) {
|
|
698
810
|
this.requestCount = 0;
|
|
699
811
|
this.setNextDailyReset();
|
|
700
812
|
}
|
|
701
|
-
|
|
813
|
+
const effectiveDailyLimit = this.headerLimitPerDay ?? this.config.maxRequestsPerDay;
|
|
814
|
+
const effectiveRemaining = this.headerRemainingToday ??
|
|
815
|
+
(effectiveDailyLimit - this.requestCount);
|
|
816
|
+
if (effectiveRemaining <= 0) {
|
|
702
817
|
const timeUntilReset = this.dailyReset.getTime() - now;
|
|
703
|
-
throw new EtsyRateLimitError(`Daily rate limit
|
|
818
|
+
throw new EtsyRateLimitError(`Daily rate limit exhausted (${effectiveDailyLimit} requests). ` +
|
|
819
|
+
`Reset in approximately ${Math.ceil(timeUntilReset / 1000 / 60)} minutes.`, Math.ceil(timeUntilReset / 1000), 'qpd_exhausted');
|
|
704
820
|
}
|
|
821
|
+
const minInterval = this.getEffectiveMinInterval();
|
|
705
822
|
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
706
|
-
if (timeSinceLastRequest <
|
|
707
|
-
const waitTime =
|
|
823
|
+
if (timeSinceLastRequest < minInterval) {
|
|
824
|
+
const waitTime = minInterval - timeSinceLastRequest;
|
|
708
825
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
709
826
|
}
|
|
710
827
|
this.requestCount++;
|
|
@@ -716,19 +833,32 @@ class EtsyRateLimiter {
|
|
|
716
833
|
this.requestCount = 0;
|
|
717
834
|
this.setNextDailyReset();
|
|
718
835
|
}
|
|
836
|
+
const effectiveRemaining = this.headerRemainingToday ??
|
|
837
|
+
Math.max(0, this.config.maxRequestsPerDay - this.requestCount);
|
|
719
838
|
return {
|
|
720
|
-
remainingRequests:
|
|
839
|
+
remainingRequests: effectiveRemaining,
|
|
721
840
|
resetTime: this.dailyReset,
|
|
722
|
-
canMakeRequest:
|
|
723
|
-
(now - this.lastRequestTime) >= this.
|
|
841
|
+
canMakeRequest: effectiveRemaining > 0 &&
|
|
842
|
+
(now - this.lastRequestTime) >= this.getEffectiveMinInterval(),
|
|
843
|
+
isFromHeaders: this.isHeaderBasedLimiting,
|
|
844
|
+
limitPerSecond: this.headerLimitPerSecond,
|
|
845
|
+
remainingThisSecond: this.headerRemainingThisSecond,
|
|
846
|
+
limitPerDay: this.headerLimitPerDay
|
|
724
847
|
};
|
|
725
848
|
}
|
|
726
849
|
getRemainingRequests() {
|
|
727
|
-
return
|
|
850
|
+
return this.headerRemainingToday ??
|
|
851
|
+
Math.max(0, this.config.maxRequestsPerDay - this.requestCount);
|
|
728
852
|
}
|
|
729
853
|
reset() {
|
|
730
854
|
this.requestCount = 0;
|
|
731
855
|
this.lastRequestTime = 0;
|
|
856
|
+
this.currentRetryCount = 0;
|
|
857
|
+
this.headerLimitPerSecond = undefined;
|
|
858
|
+
this.headerRemainingThisSecond = undefined;
|
|
859
|
+
this.headerLimitPerDay = undefined;
|
|
860
|
+
this.headerRemainingToday = undefined;
|
|
861
|
+
this.isHeaderBasedLimiting = false;
|
|
732
862
|
this.setNextDailyReset();
|
|
733
863
|
}
|
|
734
864
|
canMakeRequest() {
|
|
@@ -737,18 +867,21 @@ class EtsyRateLimiter {
|
|
|
737
867
|
this.requestCount = 0;
|
|
738
868
|
this.setNextDailyReset();
|
|
739
869
|
}
|
|
740
|
-
|
|
870
|
+
const effectiveRemaining = this.headerRemainingToday ??
|
|
871
|
+
(this.config.maxRequestsPerDay - this.requestCount);
|
|
872
|
+
if (effectiveRemaining <= 0) {
|
|
741
873
|
return false;
|
|
742
874
|
}
|
|
743
|
-
return (now - this.lastRequestTime) >= this.
|
|
875
|
+
return (now - this.lastRequestTime) >= this.getEffectiveMinInterval();
|
|
744
876
|
}
|
|
745
877
|
getTimeUntilNextRequest() {
|
|
746
878
|
const now = Date.now();
|
|
747
879
|
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
748
|
-
|
|
880
|
+
const minInterval = this.getEffectiveMinInterval();
|
|
881
|
+
if (timeSinceLastRequest >= minInterval) {
|
|
749
882
|
return 0;
|
|
750
883
|
}
|
|
751
|
-
return
|
|
884
|
+
return minInterval - timeSinceLastRequest;
|
|
752
885
|
}
|
|
753
886
|
getConfig() {
|
|
754
887
|
return { ...this.config };
|
|
@@ -1207,11 +1340,18 @@ class EtsyClient {
|
|
|
1207
1340
|
this.baseUrl = config.baseUrl || 'https://api.etsy.com/v3/application';
|
|
1208
1341
|
this.logger = new DefaultLogger();
|
|
1209
1342
|
this.keystring = config.keystring;
|
|
1343
|
+
this.sharedSecret = config.sharedSecret;
|
|
1210
1344
|
if (config.rateLimiting?.enabled !== false) {
|
|
1211
1345
|
this.rateLimiter = new EtsyRateLimiter({
|
|
1212
1346
|
maxRequestsPerDay: config.rateLimiting?.maxRequestsPerDay || 10000,
|
|
1213
1347
|
maxRequestsPerSecond: config.rateLimiting?.maxRequestsPerSecond || 10,
|
|
1214
|
-
minRequestInterval: config.rateLimiting?.minRequestInterval ?? 100
|
|
1348
|
+
minRequestInterval: config.rateLimiting?.minRequestInterval ?? 100,
|
|
1349
|
+
maxRetries: config.rateLimiting?.maxRetries,
|
|
1350
|
+
baseDelayMs: config.rateLimiting?.baseDelayMs,
|
|
1351
|
+
maxDelayMs: config.rateLimiting?.maxDelayMs,
|
|
1352
|
+
jitter: config.rateLimiting?.jitter,
|
|
1353
|
+
qpdWarningThreshold: config.rateLimiting?.qpdWarningThreshold,
|
|
1354
|
+
onApproachingLimit: config.rateLimiting?.onApproachingLimit
|
|
1215
1355
|
});
|
|
1216
1356
|
}
|
|
1217
1357
|
else {
|
|
@@ -1248,36 +1388,57 @@ class EtsyClient {
|
|
|
1248
1388
|
'Accept': 'application/json',
|
|
1249
1389
|
...requestOptions.headers
|
|
1250
1390
|
};
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1391
|
+
return this.executeWithRetry(url, { ...requestOptions, headers }, cacheKey, useCache);
|
|
1392
|
+
}
|
|
1393
|
+
async executeWithRetry(url, options, cacheKey, useCache) {
|
|
1394
|
+
while (true) {
|
|
1395
|
+
try {
|
|
1396
|
+
const response = await this.fetch(url, options);
|
|
1397
|
+
if (response.status === 429) {
|
|
1398
|
+
const { shouldRetry, delayMs } = await this.rateLimiter.handleRateLimitResponse(response.headers);
|
|
1399
|
+
if (shouldRetry) {
|
|
1400
|
+
this.logger.warn(`Rate limited. Retrying in ${delayMs}ms...`);
|
|
1401
|
+
await this.sleep(delayMs);
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (!response.ok) {
|
|
1406
|
+
const errorText = await response.text();
|
|
1407
|
+
throw new EtsyApiError(`Etsy API error: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
1408
|
+
}
|
|
1409
|
+
this.rateLimiter.updateFromHeaders(response.headers);
|
|
1410
|
+
this.rateLimiter.resetRetryCount();
|
|
1411
|
+
if (response.status === 204) {
|
|
1412
|
+
return undefined;
|
|
1413
|
+
}
|
|
1414
|
+
const contentLength = response.headers?.get?.('content-length');
|
|
1415
|
+
if (contentLength === '0') {
|
|
1416
|
+
return undefined;
|
|
1417
|
+
}
|
|
1418
|
+
const data = await response.json();
|
|
1419
|
+
if (useCache && this.cache && options.method === 'GET') {
|
|
1420
|
+
await this.cache.set(cacheKey, JSON.stringify(data), this.cacheTtl);
|
|
1421
|
+
}
|
|
1422
|
+
return data;
|
|
1270
1423
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1424
|
+
catch (error) {
|
|
1425
|
+
if (error instanceof EtsyRateLimitError) {
|
|
1426
|
+
throw error;
|
|
1427
|
+
}
|
|
1428
|
+
if (error instanceof EtsyApiError || error instanceof EtsyAuthError) {
|
|
1429
|
+
throw error;
|
|
1430
|
+
}
|
|
1431
|
+
throw new EtsyApiError(`Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 0, error);
|
|
1276
1432
|
}
|
|
1277
|
-
throw new EtsyApiError(`Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 0, error);
|
|
1278
1433
|
}
|
|
1279
1434
|
}
|
|
1435
|
+
sleep(ms) {
|
|
1436
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1437
|
+
}
|
|
1280
1438
|
getApiKey() {
|
|
1439
|
+
if (this.sharedSecret) {
|
|
1440
|
+
return `${this.keystring}:${this.sharedSecret}`;
|
|
1441
|
+
}
|
|
1281
1442
|
return this.keystring;
|
|
1282
1443
|
}
|
|
1283
1444
|
async getUser() {
|
|
@@ -1652,6 +1813,12 @@ class EtsyClient {
|
|
|
1652
1813
|
getRateLimitStatus() {
|
|
1653
1814
|
return this.rateLimiter.getRateLimitStatus();
|
|
1654
1815
|
}
|
|
1816
|
+
onApproachingRateLimit(callback, threshold) {
|
|
1817
|
+
if (threshold !== undefined) {
|
|
1818
|
+
this.rateLimiter.setWarningThreshold(threshold);
|
|
1819
|
+
}
|
|
1820
|
+
this.rateLimiter.setApproachingLimitCallback(callback);
|
|
1821
|
+
}
|
|
1655
1822
|
async clearCache() {
|
|
1656
1823
|
if (this.cache) {
|
|
1657
1824
|
await this.cache.clear();
|
|
@@ -2049,7 +2216,12 @@ class GlobalRequestQueue {
|
|
|
2049
2216
|
return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2050
2217
|
}
|
|
2051
2218
|
delay(ms) {
|
|
2052
|
-
return new Promise(resolve =>
|
|
2219
|
+
return new Promise(resolve => {
|
|
2220
|
+
const timer = setTimeout(resolve, ms);
|
|
2221
|
+
if (typeof timer.unref === 'function') {
|
|
2222
|
+
timer.unref();
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2053
2225
|
}
|
|
2054
2226
|
}
|
|
2055
2227
|
GlobalRequestQueue.instance = null;
|