@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/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
- constructor(message: string, _retryAfter?: number | undefined);
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
- if (this.requestCount >= this.config.maxRequestsPerDay) {
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 of ${this.config.maxRequestsPerDay} requests exceeded. Reset in ${Math.ceil(timeUntilReset / 1000 / 60)} minutes.`, timeUntilReset);
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 < this.config.minRequestInterval) {
703
- const waitTime = this.config.minRequestInterval - timeSinceLastRequest;
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: Math.max(0, this.config.maxRequestsPerDay - this.requestCount),
835
+ remainingRequests: effectiveRemaining,
717
836
  resetTime: this.dailyReset,
718
- canMakeRequest: this.requestCount < this.config.maxRequestsPerDay &&
719
- (now - this.lastRequestTime) >= this.config.minRequestInterval
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 Math.max(0, this.config.maxRequestsPerDay - this.requestCount);
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
- if (this.requestCount >= this.config.maxRequestsPerDay) {
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.config.minRequestInterval;
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
- if (timeSinceLastRequest >= this.config.minRequestInterval) {
876
+ const minInterval = this.getEffectiveMinInterval();
877
+ if (timeSinceLastRequest >= minInterval) {
745
878
  return 0;
746
879
  }
747
- return this.config.minRequestInterval - timeSinceLastRequest;
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
- try {
1249
- const response = await this.fetch(url, {
1250
- ...requestOptions,
1251
- headers
1252
- });
1253
- if (!response.ok) {
1254
- const errorText = await response.text();
1255
- throw new EtsyApiError(`Etsy API error: ${response.status} ${response.statusText}`, response.status, errorText);
1256
- }
1257
- if (response.status === 204) {
1258
- return undefined;
1259
- }
1260
- const contentLength = response.headers?.get?.('content-length');
1261
- if (contentLength === '0') {
1262
- return undefined;
1263
- }
1264
- const data = await response.json();
1265
- if (useCache && this.cache && requestOptions.method === 'GET') {
1266
- await this.cache.set(cacheKey, JSON.stringify(data), this.cacheTtl);
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
- return data;
1269
- }
1270
- catch (error) {
1271
- if (error instanceof EtsyApiError || error instanceof EtsyAuthError) {
1272
- throw error;
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();