@profplum700/etsy-v3-api-client 2.3.17 → 2.4.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/dist/.tsbuildinfo +1 -1
- package/dist/browser.esm.js +230 -39
- 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 +230 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +59 -2
- package/dist/index.esm.js +230 -39
- package/dist/index.esm.js.map +1 -1
- package/dist/node.cjs +230 -39
- package/dist/node.cjs.map +1 -1
- package/dist/node.esm.js +230 -39
- package/dist/node.esm.js.map +1 -1
- package/package.json +31 -34
- package/dist/index.js +0 -8517
- package/dist/index.js.map +0 -1
- package/dist/index.umd.js +0 -1180
- package/dist/index.umd.js.map +0 -1
- package/dist/index.umd.min.js +0 -2
- package/dist/index.umd.min.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ interface EtsyClientConfig {
|
|
|
11
11
|
maxRequestsPerDay?: number;
|
|
12
12
|
maxRequestsPerSecond?: number;
|
|
13
13
|
minRequestInterval?: number;
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
baseDelayMs?: number;
|
|
16
|
+
maxDelayMs?: number;
|
|
17
|
+
jitter?: number;
|
|
18
|
+
qpdWarningThreshold?: number;
|
|
19
|
+
onApproachingLimit?: (remainingRequests: number, totalLimit: number, percentageUsed: number) => void;
|
|
14
20
|
};
|
|
15
21
|
caching?: {
|
|
16
22
|
enabled: boolean;
|
|
@@ -277,15 +283,34 @@ interface SearchParams {
|
|
|
277
283
|
location?: string;
|
|
278
284
|
shop_location?: string;
|
|
279
285
|
}
|
|
286
|
+
interface EtsyRateLimitHeaders {
|
|
287
|
+
limitPerSecond?: number;
|
|
288
|
+
remainingThisSecond?: number;
|
|
289
|
+
limitPerDay?: number;
|
|
290
|
+
remainingToday?: number;
|
|
291
|
+
retryAfter?: number;
|
|
292
|
+
}
|
|
293
|
+
type RateLimitErrorType = 'qpd_exhausted' | 'qps_exhausted' | 'unknown';
|
|
294
|
+
type ApproachingLimitCallback = (remainingRequests: number, totalLimit: number, percentageUsed: number) => void;
|
|
280
295
|
interface RateLimitConfig {
|
|
281
296
|
maxRequestsPerDay: number;
|
|
282
297
|
maxRequestsPerSecond: number;
|
|
283
298
|
minRequestInterval: number;
|
|
299
|
+
maxRetries?: number;
|
|
300
|
+
baseDelayMs?: number;
|
|
301
|
+
maxDelayMs?: number;
|
|
302
|
+
jitter?: number;
|
|
303
|
+
qpdWarningThreshold?: number;
|
|
304
|
+
onApproachingLimit?: ApproachingLimitCallback;
|
|
284
305
|
}
|
|
285
306
|
interface RateLimitStatus {
|
|
286
307
|
remainingRequests: number;
|
|
287
308
|
resetTime: Date;
|
|
288
309
|
canMakeRequest: boolean;
|
|
310
|
+
isFromHeaders: boolean;
|
|
311
|
+
limitPerSecond?: number;
|
|
312
|
+
remainingThisSecond?: number;
|
|
313
|
+
limitPerDay?: number;
|
|
289
314
|
}
|
|
290
315
|
interface EtsyErrorDetails {
|
|
291
316
|
statusCode: number;
|
|
@@ -328,8 +353,10 @@ declare class EtsyAuthError extends Error {
|
|
|
328
353
|
}
|
|
329
354
|
declare class EtsyRateLimitError extends Error {
|
|
330
355
|
_retryAfter?: number | undefined;
|
|
331
|
-
|
|
356
|
+
readonly errorType: RateLimitErrorType;
|
|
357
|
+
constructor(message: string, _retryAfter?: number | undefined, errorType?: RateLimitErrorType);
|
|
332
358
|
get retryAfter(): number | undefined;
|
|
359
|
+
isRetryable(): boolean;
|
|
333
360
|
}
|
|
334
361
|
interface CacheStorage {
|
|
335
362
|
get(_key: string): Promise<string | null>;
|
|
@@ -847,6 +874,14 @@ interface EtsyListingPropertyScale {
|
|
|
847
874
|
display_name: string;
|
|
848
875
|
description?: string;
|
|
849
876
|
}
|
|
877
|
+
interface UpdateListingPropertyParams {
|
|
878
|
+
shopId: string | number;
|
|
879
|
+
listingId: string | number;
|
|
880
|
+
propertyId: number;
|
|
881
|
+
valueIds: number[];
|
|
882
|
+
values: string[];
|
|
883
|
+
scaleId?: number;
|
|
884
|
+
}
|
|
850
885
|
interface EtsyBuyerTaxonomyNode {
|
|
851
886
|
id: number;
|
|
852
887
|
level: number;
|
|
@@ -1026,6 +1061,8 @@ declare class EtsyClient {
|
|
|
1026
1061
|
private bulkOperationManager;
|
|
1027
1062
|
constructor(config: EtsyClientConfig);
|
|
1028
1063
|
private makeRequest;
|
|
1064
|
+
private executeWithRetry;
|
|
1065
|
+
private sleep;
|
|
1029
1066
|
private getApiKey;
|
|
1030
1067
|
getUser(): Promise<EtsyUser>;
|
|
1031
1068
|
getShop(shopId?: string): Promise<EtsyShop>;
|
|
@@ -1081,9 +1118,11 @@ declare class EtsyClient {
|
|
|
1081
1118
|
getBuyerTaxonomyNodes(): Promise<EtsyBuyerTaxonomyNode[]>;
|
|
1082
1119
|
getPropertiesByTaxonomyId(taxonomyId: number): Promise<EtsyBuyerTaxonomyProperty[]>;
|
|
1083
1120
|
getListingProperties(shopId: string, listingId: string): Promise<EtsyListingProperty[]>;
|
|
1121
|
+
updateListingProperty(params: UpdateListingPropertyParams): Promise<EtsyListingProperty>;
|
|
1084
1122
|
getShopProductionPartners(shopId: string): Promise<EtsyShopProductionPartner[]>;
|
|
1085
1123
|
getRemainingRequests(): number;
|
|
1086
1124
|
getRateLimitStatus(): RateLimitStatus;
|
|
1125
|
+
onApproachingRateLimit(callback: ApproachingLimitCallback, threshold?: number): void;
|
|
1087
1126
|
clearCache(): Promise<void>;
|
|
1088
1127
|
getCurrentTokens(): EtsyTokens | null;
|
|
1089
1128
|
isTokenExpired(): boolean;
|
|
@@ -1214,8 +1253,26 @@ declare class EtsyRateLimiter {
|
|
|
1214
1253
|
private dailyReset;
|
|
1215
1254
|
private lastRequestTime;
|
|
1216
1255
|
private readonly config;
|
|
1256
|
+
private headerLimitPerSecond?;
|
|
1257
|
+
private headerRemainingThisSecond?;
|
|
1258
|
+
private headerLimitPerDay?;
|
|
1259
|
+
private headerRemainingToday?;
|
|
1260
|
+
private isHeaderBasedLimiting;
|
|
1261
|
+
private currentRetryCount;
|
|
1217
1262
|
constructor(config?: Partial<RateLimitConfig>);
|
|
1218
1263
|
private setNextDailyReset;
|
|
1264
|
+
updateFromHeaders(headers: Headers | Record<string, string> | undefined | null): void;
|
|
1265
|
+
private parseRateLimitHeaders;
|
|
1266
|
+
private checkApproachingLimit;
|
|
1267
|
+
handleRateLimitResponse(headers: Headers | Record<string, string> | undefined | null): Promise<{
|
|
1268
|
+
shouldRetry: boolean;
|
|
1269
|
+
delayMs: number;
|
|
1270
|
+
}>;
|
|
1271
|
+
private calculateBackoffDelay;
|
|
1272
|
+
resetRetryCount(): void;
|
|
1273
|
+
setApproachingLimitCallback(callback: ApproachingLimitCallback | undefined): void;
|
|
1274
|
+
setWarningThreshold(threshold: number): void;
|
|
1275
|
+
private getEffectiveMinInterval;
|
|
1219
1276
|
waitForRateLimit(): Promise<void>;
|
|
1220
1277
|
getRateLimitStatus(): RateLimitStatus;
|
|
1221
1278
|
getRemainingRequests(): number;
|
|
@@ -1702,4 +1759,4 @@ declare function getLibraryInfo(): {
|
|
|
1702
1759
|
};
|
|
1703
1760
|
|
|
1704
1761
|
export { AuthHelper, BatchQueryExecutor, BulkOperationManager, COMMON_SCOPE_COMBINATIONS, CacheWithInvalidation, CreateListingSchema, DEFAULT_RETRY_CONFIG, ETSY_SCOPES, EncryptedFileTokenStorage, EtsyApiError, EtsyAuthError, EtsyClient, EtsyRateLimitError, EtsyRateLimiter, EtsyWebhookHandler, FieldValidator, FileTokenStorage, GlobalRequestQueue, LFUCache, LIBRARY_NAME, LRUCache, ListingQueryBuilder, LocalStorageTokenStorage, MemoryTokenStorage, PaginatedResults, PluginManager, ReceiptQueryBuilder, RedisCacheStorage, RetryManager, SecureTokenStorage, SessionStorageTokenStorage, TokenManager, UpdateListingSchema, UpdateShopSchema, VERSION, ValidationException, Validator, WebhookSecurity, combineValidators, createAnalyticsPlugin, createAuthHelper, createBatchQuery, createBulkOperationManager, createCacheStorage, createCacheWithInvalidation, createCachingPlugin, createCodeChallenge, createDefaultTokenStorage, createEtsyClient, createListingQuery, createLoggingPlugin, createPaginatedResults, createRateLimitPlugin, createRateLimiter, createReceiptQuery, createRedisCacheStorage, createRetryPlugin, createTokenManager, createValidator, createWebhookHandler, createWebhookSecurity, decryptAES256GCM, EtsyClient as default, defaultRateLimiter, deriveKeyFromPassword, encryptAES256GCM, executeBulkOperation, field, generateCodeVerifier, generateEncryptionKey, generateRandomBase64Url, generateState, getAvailableStorage, getEnvironmentInfo, getGlobalQueue, getLibraryInfo, hasLocalStorage, hasSessionStorage, isBrowser, isNode, isSecureStorageSupported, sha256, sha256Base64Url, validate, validateEncryptionKey, validateOrThrow, withQueryBuilder, withQueue, withRetry };
|
|
1705
|
-
export type { AdvancedCachingConfig, AnalyticsPluginConfig, AuthHelperConfig, BulkImageUploadOperation, BulkOperationConfig, BulkOperationError, BulkOperationResult, BulkOperationSummary, BulkUpdateListingOperation, CacheEntry, CacheStats, CacheStorage, CacheStrategy, CachingPluginConfig, CreateDraftListingParams, CreateReceiptShipmentParams, CreateShippingProfileDestinationParams, CreateShippingProfileParams, CreateShopSectionParams, EncryptedData, EncryptedStorageConfig, EtsyApiResponse, EtsyBuyerTaxonomyNode, EtsyBuyerTaxonomyProperty, EtsyBuyerTaxonomyPropertyScale, EtsyBuyerTaxonomyPropertyValue, EtsyClientConfig, EtsyClientWithQueryBuilder, EtsyErrorDetails, EtsyListing, EtsyListingImage, EtsyListingInventory, EtsyListingOffering, EtsyListingProduct, EtsyListingProperty, EtsyListingPropertyScale, EtsyListingPropertyValue, EtsyPagination, EtsyPayment, EtsyPaymentAccountLedgerEntry, EtsyPaymentAdjustment, EtsyPlugin, EtsySellerTaxonomyNode, EtsyShippingProfile, EtsyShippingProfileDestination, EtsyShippingProfileUpgrade, EtsyShop, EtsyShopProductionPartner, EtsyShopReceipt, EtsyShopReceiptShipment, EtsyShopReceiptTransaction, EtsyShopRefund, EtsyShopSection, EtsyTokenResponse, EtsyTokens, EtsyTransactionVariation, EtsyUser, EtsyWebhookEvent, EtsyWebhookEventType, GetPaymentAccountLedgerEntriesParams, GetShopReceiptsParams, ListingParams, ListingSortOn, ListingState, LoggerInterface, LoggingPluginConfig, PageFetcher, PaginationOptions, PluginRequestConfig, PluginResponse, QueueOptions, RateLimitConfig, RateLimitPluginConfig, RateLimitStatus, RedisClientLike, RedisConfig, RequestPriority, RetryConfig, RetryOptions, RetryPluginConfig, SearchParams, SecureTokenStorageConfig, SortOrder, TokenRefreshCallback, TokenRotationCallback, TokenRotationConfig, TokenStorage, UpdateListingInventoryParams, UpdateListingParams, UpdateShippingProfileDestinationParams, UpdateShippingProfileParams, UpdateShopParams, UpdateShopReceiptParams, UpdateShopSectionParams, UploadListingFileParams, UploadListingImageParams, ValidationError, ValidationOptions, ValidationResult, ValidationSchema, ValidatorFunction, WebhookConfig, WebhookEventHandler, WebhookSecurityConfig };
|
|
1762
|
+
export type { AdvancedCachingConfig, AnalyticsPluginConfig, ApproachingLimitCallback, AuthHelperConfig, BulkImageUploadOperation, BulkOperationConfig, BulkOperationError, BulkOperationResult, BulkOperationSummary, BulkUpdateListingOperation, CacheEntry, CacheStats, CacheStorage, CacheStrategy, CachingPluginConfig, CreateDraftListingParams, CreateReceiptShipmentParams, CreateShippingProfileDestinationParams, CreateShippingProfileParams, CreateShopSectionParams, EncryptedData, EncryptedStorageConfig, EtsyApiResponse, EtsyBuyerTaxonomyNode, EtsyBuyerTaxonomyProperty, EtsyBuyerTaxonomyPropertyScale, EtsyBuyerTaxonomyPropertyValue, EtsyClientConfig, EtsyClientWithQueryBuilder, EtsyErrorDetails, EtsyListing, EtsyListingImage, EtsyListingInventory, EtsyListingOffering, EtsyListingProduct, EtsyListingProperty, EtsyListingPropertyScale, EtsyListingPropertyValue, EtsyPagination, EtsyPayment, EtsyPaymentAccountLedgerEntry, EtsyPaymentAdjustment, EtsyPlugin, EtsyRateLimitHeaders, EtsySellerTaxonomyNode, EtsyShippingProfile, EtsyShippingProfileDestination, EtsyShippingProfileUpgrade, EtsyShop, EtsyShopProductionPartner, EtsyShopReceipt, EtsyShopReceiptShipment, EtsyShopReceiptTransaction, EtsyShopRefund, EtsyShopSection, EtsyTokenResponse, EtsyTokens, EtsyTransactionVariation, EtsyUser, EtsyWebhookEvent, EtsyWebhookEventType, GetPaymentAccountLedgerEntriesParams, GetShopReceiptsParams, ListingParams, ListingSortOn, ListingState, LoggerInterface, LoggingPluginConfig, PageFetcher, PaginationOptions, PluginRequestConfig, PluginResponse, QueueOptions, RateLimitConfig, RateLimitErrorType, RateLimitPluginConfig, RateLimitStatus, RedisClientLike, RedisConfig, RequestPriority, RetryConfig, RetryOptions, RetryPluginConfig, SearchParams, SecureTokenStorageConfig, SortOrder, TokenRefreshCallback, TokenRotationCallback, TokenRotationConfig, TokenStorage, UpdateListingInventoryParams, UpdateListingParams, UpdateListingPropertyParams, UpdateShippingProfileDestinationParams, UpdateShippingProfileParams, UpdateShopParams, UpdateShopReceiptParams, UpdateShopSectionParams, UploadListingFileParams, UploadListingImageParams, ValidationError, ValidationOptions, ValidationResult, ValidationSchema, ValidatorFunction, WebhookConfig, WebhookEventHandler, WebhookSecurityConfig };
|
package/dist/index.esm.js
CHANGED
|
@@ -234,14 +234,18 @@ class EtsyAuthError extends Error {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
class EtsyRateLimitError extends Error {
|
|
237
|
-
constructor(message, _retryAfter) {
|
|
237
|
+
constructor(message, _retryAfter, errorType = 'unknown') {
|
|
238
238
|
super(message);
|
|
239
239
|
this._retryAfter = _retryAfter;
|
|
240
240
|
this.name = 'EtsyRateLimitError';
|
|
241
|
+
this.errorType = errorType;
|
|
241
242
|
}
|
|
242
243
|
get retryAfter() {
|
|
243
244
|
return this._retryAfter;
|
|
244
245
|
}
|
|
246
|
+
isRetryable() {
|
|
247
|
+
return this.errorType !== 'qpd_exhausted';
|
|
248
|
+
}
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
const isBrowser$1 = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
@@ -674,10 +678,18 @@ class EtsyRateLimiter {
|
|
|
674
678
|
this.requestCount = 0;
|
|
675
679
|
this.dailyReset = new Date();
|
|
676
680
|
this.lastRequestTime = 0;
|
|
681
|
+
this.isHeaderBasedLimiting = false;
|
|
682
|
+
this.currentRetryCount = 0;
|
|
677
683
|
this.config = {
|
|
678
684
|
maxRequestsPerDay: 10000,
|
|
679
685
|
maxRequestsPerSecond: 10,
|
|
680
686
|
minRequestInterval: 100,
|
|
687
|
+
maxRetries: 3,
|
|
688
|
+
baseDelayMs: 1000,
|
|
689
|
+
maxDelayMs: 30000,
|
|
690
|
+
jitter: 0.1,
|
|
691
|
+
qpdWarningThreshold: 80,
|
|
692
|
+
onApproachingLimit: undefined,
|
|
681
693
|
...config
|
|
682
694
|
};
|
|
683
695
|
this.setNextDailyReset();
|
|
@@ -688,19 +700,124 @@ class EtsyRateLimiter {
|
|
|
688
700
|
this.dailyReset.setUTCDate(this.dailyReset.getUTCDate() + 1);
|
|
689
701
|
this.dailyReset.setUTCHours(0, 0, 0, 0);
|
|
690
702
|
}
|
|
703
|
+
updateFromHeaders(headers) {
|
|
704
|
+
if (!headers) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const parsed = this.parseRateLimitHeaders(headers);
|
|
708
|
+
if (parsed.limitPerSecond !== undefined) {
|
|
709
|
+
this.headerLimitPerSecond = parsed.limitPerSecond;
|
|
710
|
+
this.isHeaderBasedLimiting = true;
|
|
711
|
+
}
|
|
712
|
+
if (parsed.remainingThisSecond !== undefined) {
|
|
713
|
+
this.headerRemainingThisSecond = parsed.remainingThisSecond;
|
|
714
|
+
}
|
|
715
|
+
if (parsed.limitPerDay !== undefined) {
|
|
716
|
+
this.headerLimitPerDay = parsed.limitPerDay;
|
|
717
|
+
this.isHeaderBasedLimiting = true;
|
|
718
|
+
}
|
|
719
|
+
if (parsed.remainingToday !== undefined) {
|
|
720
|
+
this.headerRemainingToday = parsed.remainingToday;
|
|
721
|
+
this.checkApproachingLimit();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
parseRateLimitHeaders(headers) {
|
|
725
|
+
const getHeader = (name) => {
|
|
726
|
+
if (headers instanceof Headers) {
|
|
727
|
+
return headers.get(name);
|
|
728
|
+
}
|
|
729
|
+
const lowerName = name.toLowerCase();
|
|
730
|
+
for (const key of Object.keys(headers)) {
|
|
731
|
+
if (key.toLowerCase() === lowerName) {
|
|
732
|
+
return headers[key] ?? null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
};
|
|
737
|
+
const parseNumber = (value) => {
|
|
738
|
+
if (value === null)
|
|
739
|
+
return undefined;
|
|
740
|
+
const num = parseInt(value, 10);
|
|
741
|
+
return isNaN(num) ? undefined : num;
|
|
742
|
+
};
|
|
743
|
+
return {
|
|
744
|
+
limitPerSecond: parseNumber(getHeader('x-limit-per-second')),
|
|
745
|
+
remainingThisSecond: parseNumber(getHeader('x-remaining-this-second')),
|
|
746
|
+
limitPerDay: parseNumber(getHeader('x-limit-per-day')),
|
|
747
|
+
remainingToday: parseNumber(getHeader('x-remaining-today')),
|
|
748
|
+
retryAfter: parseNumber(getHeader('retry-after'))
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
checkApproachingLimit() {
|
|
752
|
+
if (!this.config.onApproachingLimit)
|
|
753
|
+
return;
|
|
754
|
+
const limit = this.headerLimitPerDay ?? this.config.maxRequestsPerDay;
|
|
755
|
+
const remaining = this.headerRemainingToday ?? this.getRemainingRequests();
|
|
756
|
+
const used = limit - remaining;
|
|
757
|
+
const percentageUsed = (used / limit) * 100;
|
|
758
|
+
if (percentageUsed >= this.config.qpdWarningThreshold) {
|
|
759
|
+
this.config.onApproachingLimit(remaining, limit, percentageUsed);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async handleRateLimitResponse(headers) {
|
|
763
|
+
const parsed = headers ? this.parseRateLimitHeaders(headers) : {};
|
|
764
|
+
this.updateFromHeaders(headers);
|
|
765
|
+
if (this.headerRemainingToday === 0) {
|
|
766
|
+
throw new EtsyRateLimitError('Daily rate limit exhausted. No requests remaining until limit resets.', parsed.retryAfter, 'qpd_exhausted');
|
|
767
|
+
}
|
|
768
|
+
this.currentRetryCount++;
|
|
769
|
+
if (this.currentRetryCount > this.config.maxRetries) {
|
|
770
|
+
this.currentRetryCount = 0;
|
|
771
|
+
throw new EtsyRateLimitError(`Max retries (${this.config.maxRetries}) exceeded for rate limit`, parsed.retryAfter, 'qps_exhausted');
|
|
772
|
+
}
|
|
773
|
+
const delayMs = this.calculateBackoffDelay(this.currentRetryCount, parsed.retryAfter);
|
|
774
|
+
return { shouldRetry: true, delayMs };
|
|
775
|
+
}
|
|
776
|
+
calculateBackoffDelay(attempt, retryAfterSeconds) {
|
|
777
|
+
const serverSuggestedMs = retryAfterSeconds ? retryAfterSeconds * 1000 : 0;
|
|
778
|
+
const exponentialDelay = this.config.baseDelayMs * Math.pow(2, attempt - 1);
|
|
779
|
+
let delay = Math.min(exponentialDelay, this.config.maxDelayMs);
|
|
780
|
+
delay = Math.max(delay, serverSuggestedMs);
|
|
781
|
+
if (this.config.jitter > 0) {
|
|
782
|
+
const jitterAmount = delay * this.config.jitter;
|
|
783
|
+
const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount;
|
|
784
|
+
delay += randomJitter;
|
|
785
|
+
}
|
|
786
|
+
return Math.max(0, Math.floor(delay));
|
|
787
|
+
}
|
|
788
|
+
resetRetryCount() {
|
|
789
|
+
this.currentRetryCount = 0;
|
|
790
|
+
}
|
|
791
|
+
setApproachingLimitCallback(callback) {
|
|
792
|
+
this.config.onApproachingLimit = callback;
|
|
793
|
+
}
|
|
794
|
+
setWarningThreshold(threshold) {
|
|
795
|
+
this.config.qpdWarningThreshold = threshold;
|
|
796
|
+
}
|
|
797
|
+
getEffectiveMinInterval() {
|
|
798
|
+
if (this.headerLimitPerSecond !== undefined && this.headerLimitPerSecond > 0) {
|
|
799
|
+
return Math.ceil(1000 / this.headerLimitPerSecond);
|
|
800
|
+
}
|
|
801
|
+
return this.config.minRequestInterval;
|
|
802
|
+
}
|
|
691
803
|
async waitForRateLimit() {
|
|
692
804
|
const now = Date.now();
|
|
693
805
|
if (now >= this.dailyReset.getTime()) {
|
|
694
806
|
this.requestCount = 0;
|
|
695
807
|
this.setNextDailyReset();
|
|
696
808
|
}
|
|
697
|
-
|
|
809
|
+
const effectiveDailyLimit = this.headerLimitPerDay ?? this.config.maxRequestsPerDay;
|
|
810
|
+
const effectiveRemaining = this.headerRemainingToday ??
|
|
811
|
+
(effectiveDailyLimit - this.requestCount);
|
|
812
|
+
if (effectiveRemaining <= 0) {
|
|
698
813
|
const timeUntilReset = this.dailyReset.getTime() - now;
|
|
699
|
-
throw new EtsyRateLimitError(`Daily rate limit
|
|
814
|
+
throw new EtsyRateLimitError(`Daily rate limit exhausted (${effectiveDailyLimit} requests). ` +
|
|
815
|
+
`Reset in approximately ${Math.ceil(timeUntilReset / 1000 / 60)} minutes.`, Math.ceil(timeUntilReset / 1000), 'qpd_exhausted');
|
|
700
816
|
}
|
|
817
|
+
const minInterval = this.getEffectiveMinInterval();
|
|
701
818
|
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
702
|
-
if (timeSinceLastRequest <
|
|
703
|
-
const waitTime =
|
|
819
|
+
if (timeSinceLastRequest < minInterval) {
|
|
820
|
+
const waitTime = minInterval - timeSinceLastRequest;
|
|
704
821
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
705
822
|
}
|
|
706
823
|
this.requestCount++;
|
|
@@ -712,19 +829,32 @@ class EtsyRateLimiter {
|
|
|
712
829
|
this.requestCount = 0;
|
|
713
830
|
this.setNextDailyReset();
|
|
714
831
|
}
|
|
832
|
+
const effectiveRemaining = this.headerRemainingToday ??
|
|
833
|
+
Math.max(0, this.config.maxRequestsPerDay - this.requestCount);
|
|
715
834
|
return {
|
|
716
|
-
remainingRequests:
|
|
835
|
+
remainingRequests: effectiveRemaining,
|
|
717
836
|
resetTime: this.dailyReset,
|
|
718
|
-
canMakeRequest:
|
|
719
|
-
(now - this.lastRequestTime) >= this.
|
|
837
|
+
canMakeRequest: effectiveRemaining > 0 &&
|
|
838
|
+
(now - this.lastRequestTime) >= this.getEffectiveMinInterval(),
|
|
839
|
+
isFromHeaders: this.isHeaderBasedLimiting,
|
|
840
|
+
limitPerSecond: this.headerLimitPerSecond,
|
|
841
|
+
remainingThisSecond: this.headerRemainingThisSecond,
|
|
842
|
+
limitPerDay: this.headerLimitPerDay
|
|
720
843
|
};
|
|
721
844
|
}
|
|
722
845
|
getRemainingRequests() {
|
|
723
|
-
return
|
|
846
|
+
return this.headerRemainingToday ??
|
|
847
|
+
Math.max(0, this.config.maxRequestsPerDay - this.requestCount);
|
|
724
848
|
}
|
|
725
849
|
reset() {
|
|
726
850
|
this.requestCount = 0;
|
|
727
851
|
this.lastRequestTime = 0;
|
|
852
|
+
this.currentRetryCount = 0;
|
|
853
|
+
this.headerLimitPerSecond = undefined;
|
|
854
|
+
this.headerRemainingThisSecond = undefined;
|
|
855
|
+
this.headerLimitPerDay = undefined;
|
|
856
|
+
this.headerRemainingToday = undefined;
|
|
857
|
+
this.isHeaderBasedLimiting = false;
|
|
728
858
|
this.setNextDailyReset();
|
|
729
859
|
}
|
|
730
860
|
canMakeRequest() {
|
|
@@ -733,18 +863,21 @@ class EtsyRateLimiter {
|
|
|
733
863
|
this.requestCount = 0;
|
|
734
864
|
this.setNextDailyReset();
|
|
735
865
|
}
|
|
736
|
-
|
|
866
|
+
const effectiveRemaining = this.headerRemainingToday ??
|
|
867
|
+
(this.config.maxRequestsPerDay - this.requestCount);
|
|
868
|
+
if (effectiveRemaining <= 0) {
|
|
737
869
|
return false;
|
|
738
870
|
}
|
|
739
|
-
return (now - this.lastRequestTime) >= this.
|
|
871
|
+
return (now - this.lastRequestTime) >= this.getEffectiveMinInterval();
|
|
740
872
|
}
|
|
741
873
|
getTimeUntilNextRequest() {
|
|
742
874
|
const now = Date.now();
|
|
743
875
|
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
744
|
-
|
|
876
|
+
const minInterval = this.getEffectiveMinInterval();
|
|
877
|
+
if (timeSinceLastRequest >= minInterval) {
|
|
745
878
|
return 0;
|
|
746
879
|
}
|
|
747
|
-
return
|
|
880
|
+
return minInterval - timeSinceLastRequest;
|
|
748
881
|
}
|
|
749
882
|
getConfig() {
|
|
750
883
|
return { ...this.config };
|
|
@@ -1208,7 +1341,13 @@ class EtsyClient {
|
|
|
1208
1341
|
this.rateLimiter = new EtsyRateLimiter({
|
|
1209
1342
|
maxRequestsPerDay: config.rateLimiting?.maxRequestsPerDay || 10000,
|
|
1210
1343
|
maxRequestsPerSecond: config.rateLimiting?.maxRequestsPerSecond || 10,
|
|
1211
|
-
minRequestInterval: config.rateLimiting?.minRequestInterval ?? 100
|
|
1344
|
+
minRequestInterval: config.rateLimiting?.minRequestInterval ?? 100,
|
|
1345
|
+
maxRetries: config.rateLimiting?.maxRetries,
|
|
1346
|
+
baseDelayMs: config.rateLimiting?.baseDelayMs,
|
|
1347
|
+
maxDelayMs: config.rateLimiting?.maxDelayMs,
|
|
1348
|
+
jitter: config.rateLimiting?.jitter,
|
|
1349
|
+
qpdWarningThreshold: config.rateLimiting?.qpdWarningThreshold,
|
|
1350
|
+
onApproachingLimit: config.rateLimiting?.onApproachingLimit
|
|
1212
1351
|
});
|
|
1213
1352
|
}
|
|
1214
1353
|
else {
|
|
@@ -1245,35 +1384,53 @@ class EtsyClient {
|
|
|
1245
1384
|
'Accept': 'application/json',
|
|
1246
1385
|
...requestOptions.headers
|
|
1247
1386
|
};
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1387
|
+
return this.executeWithRetry(url, { ...requestOptions, headers }, cacheKey, useCache);
|
|
1388
|
+
}
|
|
1389
|
+
async executeWithRetry(url, options, cacheKey, useCache) {
|
|
1390
|
+
while (true) {
|
|
1391
|
+
try {
|
|
1392
|
+
const response = await this.fetch(url, options);
|
|
1393
|
+
if (response.status === 429) {
|
|
1394
|
+
const { shouldRetry, delayMs } = await this.rateLimiter.handleRateLimitResponse(response.headers);
|
|
1395
|
+
if (shouldRetry) {
|
|
1396
|
+
this.logger.warn(`Rate limited. Retrying in ${delayMs}ms...`);
|
|
1397
|
+
await this.sleep(delayMs);
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (!response.ok) {
|
|
1402
|
+
const errorText = await response.text();
|
|
1403
|
+
throw new EtsyApiError(`Etsy API error: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
1404
|
+
}
|
|
1405
|
+
this.rateLimiter.updateFromHeaders(response.headers);
|
|
1406
|
+
this.rateLimiter.resetRetryCount();
|
|
1407
|
+
if (response.status === 204) {
|
|
1408
|
+
return undefined;
|
|
1409
|
+
}
|
|
1410
|
+
const contentLength = response.headers?.get?.('content-length');
|
|
1411
|
+
if (contentLength === '0') {
|
|
1412
|
+
return undefined;
|
|
1413
|
+
}
|
|
1414
|
+
const data = await response.json();
|
|
1415
|
+
if (useCache && this.cache && options.method === 'GET') {
|
|
1416
|
+
await this.cache.set(cacheKey, JSON.stringify(data), this.cacheTtl);
|
|
1417
|
+
}
|
|
1418
|
+
return data;
|
|
1267
1419
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
if (error instanceof EtsyRateLimitError) {
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
if (error instanceof EtsyApiError || error instanceof EtsyAuthError) {
|
|
1425
|
+
throw error;
|
|
1426
|
+
}
|
|
1427
|
+
throw new EtsyApiError(`Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 0, error);
|
|
1273
1428
|
}
|
|
1274
|
-
throw new EtsyApiError(`Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 0, error);
|
|
1275
1429
|
}
|
|
1276
1430
|
}
|
|
1431
|
+
sleep(ms) {
|
|
1432
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1433
|
+
}
|
|
1277
1434
|
getApiKey() {
|
|
1278
1435
|
if (this.sharedSecret) {
|
|
1279
1436
|
return `${this.keystring}:${this.sharedSecret}`;
|
|
@@ -1642,6 +1799,34 @@ class EtsyClient {
|
|
|
1642
1799
|
const response = await this.makeRequest(`/shops/${shopId}/listings/${listingId}/properties`);
|
|
1643
1800
|
return response.results;
|
|
1644
1801
|
}
|
|
1802
|
+
async updateListingProperty(params) {
|
|
1803
|
+
const { shopId, listingId, propertyId, valueIds, values, scaleId } = params;
|
|
1804
|
+
const body = new URLSearchParams();
|
|
1805
|
+
valueIds.forEach(id => body.append('value_ids[]', id.toString()));
|
|
1806
|
+
values.forEach(val => body.append('values[]', val));
|
|
1807
|
+
if (scaleId !== undefined) {
|
|
1808
|
+
body.append('scale_id', scaleId.toString());
|
|
1809
|
+
}
|
|
1810
|
+
const url = `${this.baseUrl}/shops/${shopId}/listings/${listingId}/properties/${propertyId}`;
|
|
1811
|
+
await this.rateLimiter.waitForRateLimit();
|
|
1812
|
+
const accessToken = await this.tokenManager.getAccessToken();
|
|
1813
|
+
const response = await this.fetch(url, {
|
|
1814
|
+
method: 'PUT',
|
|
1815
|
+
headers: {
|
|
1816
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
1817
|
+
'x-api-key': this.getApiKey(),
|
|
1818
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
1819
|
+
},
|
|
1820
|
+
body: body.toString()
|
|
1821
|
+
});
|
|
1822
|
+
this.rateLimiter.updateFromHeaders(response.headers);
|
|
1823
|
+
this.rateLimiter.resetRetryCount();
|
|
1824
|
+
if (!response.ok) {
|
|
1825
|
+
const errorText = await response.text();
|
|
1826
|
+
throw new EtsyApiError(`Failed to update listing property: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
1827
|
+
}
|
|
1828
|
+
return response.json();
|
|
1829
|
+
}
|
|
1645
1830
|
async getShopProductionPartners(shopId) {
|
|
1646
1831
|
const response = await this.makeRequest(`/shops/${shopId}/production-partners`);
|
|
1647
1832
|
return response.results;
|
|
@@ -1652,6 +1837,12 @@ class EtsyClient {
|
|
|
1652
1837
|
getRateLimitStatus() {
|
|
1653
1838
|
return this.rateLimiter.getRateLimitStatus();
|
|
1654
1839
|
}
|
|
1840
|
+
onApproachingRateLimit(callback, threshold) {
|
|
1841
|
+
if (threshold !== undefined) {
|
|
1842
|
+
this.rateLimiter.setWarningThreshold(threshold);
|
|
1843
|
+
}
|
|
1844
|
+
this.rateLimiter.setApproachingLimitCallback(callback);
|
|
1845
|
+
}
|
|
1655
1846
|
async clearCache() {
|
|
1656
1847
|
if (this.cache) {
|
|
1657
1848
|
await this.cache.clear();
|