shipmail 0.5.5 → 0.5.7

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 CHANGED
@@ -93,15 +93,15 @@ const shipmail = new ShipmailClient({
93
93
  });
94
94
  ```
95
95
 
96
- | Option | Type | Default | Description |
97
- | ---------------- | ------------------------ | ---------------------------- | --------------------------------------------------------------- |
98
- | `apiKey` | `string` | required | Shipmail API key (`sm_live_...`). |
99
- | `baseUrl` | `string` | `https://shipmail.to/api/v1` | API base URL. |
100
- | `maxRetries` | `number` | `2` | Retry count on 5xx and 429. Total attempts is `maxRetries + 1`. |
101
- | `timeout` | `number` | `30_000` | Per-request timeout in ms. |
102
- | `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation. |
103
- | `defaultHeaders` | `Record<string, string>` | `{}` | Headers added to every request. |
104
- | `organizationId` | `string` | none | Delegated child organization for approved infrastructure calls. |
96
+ | Option | Type | Default | Description |
97
+ | ---------------- | ------------------------ | ---------------------------- | ------------------------------------------------------------------- |
98
+ | `apiKey` | `string` | required | Shipmail API key (`sm_live_...`). |
99
+ | `baseUrl` | `string` | `https://shipmail.to/api/v1` | API base URL. |
100
+ | `maxRetries` | `number` | `2` | Retry count for eligible requests. Up to `maxRetries + 1` attempts. |
101
+ | `timeout` | `number` | `30_000` | Per-request timeout in ms. |
102
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation. |
103
+ | `defaultHeaders` | `Record<string, string>` | `{}` | Headers added to every request. |
104
+ | `organizationId` | `string` | none | Delegated child organization for approved infrastructure calls. |
105
105
 
106
106
  ## Domains
107
107
 
@@ -730,21 +730,25 @@ try {
730
730
  }
731
731
  ```
732
732
 
733
- | Error | When |
734
- | --------------------- | -------------------------------------------------------------- |
735
- | `AuthenticationError` | 401. Bad or missing API key. |
736
- | `AuthorizationError` | 403. Key lacks permission for the resource. |
737
- | `ValidationError` | 400 or 422. See `details` for per-field errors. |
738
- | `NotFoundError` | 404. |
739
- | `ConflictError` | 409. Resource already exists or state conflict. |
740
- | `RateLimitError` | 429. Read `retryAfter` (seconds). |
741
- | `QuotaExceededError` | 402. Plan or sending quota exceeded. |
742
- | `InternalServerError` | 5xx. Retried automatically up to `maxRetries`. |
743
- | `ConnectionError` | Network error, timeout, or DNS failure. Retried automatically. |
733
+ | Error | When |
734
+ | --------------------- | ---------------------------------------------------------------- |
735
+ | `AuthenticationError` | 401. Bad or missing API key. |
736
+ | `AuthorizationError` | 403. Key lacks permission for the resource. |
737
+ | `ValidationError` | 400 or 422. See `details` for per-field errors. |
738
+ | `NotFoundError` | 404. |
739
+ | `ConflictError` | 409. Resource already exists or state conflict. |
740
+ | `RateLimitError` | 429. Read `retryAfter` (seconds). |
741
+ | `QuotaExceededError` | 402. Plan or sending quota exceeded. |
742
+ | `InternalServerError` | 5xx. Eligible requests retry up to `maxRetries`. |
743
+ | `ConnectionError` | Network error, timeout, or DNS failure. Eligible requests retry. |
744
744
 
745
745
  ## Retries
746
746
 
747
- The SDK retries on `5xx`, `429`, and connection errors with exponential backoff and jitter. `Retry-After` is honored when present. Default is 2 retries (3 total attempts).
747
+ The SDK retries eligible requests after connection errors, `429` responses, and retryable `5xx` errors. Automatic retries apply to `GET`, `HEAD`, and `OPTIONS`. Only POST, PATCH, and PUT mutations can retry, and they require an explicit `idempotencyKey` method option or an `Idempotency-Key` header. Reuse the same key for the same operation. DELETE requests and errors marked non-retryable are never retried.
748
+
749
+ `maxRetries` defaults to 2 (up to 3 total attempts) and must be a non-negative integer. Set it to 0 to disable retries.
750
+
751
+ A valid `Retry-After` header, in seconds or HTTP-date format, takes precedence over the rate-limit error body's `retry_after`. Otherwise, retries use exponential backoff with jitter. If the requested delay exceeds 60 seconds, the SDK returns the error without retrying automatically; schedule any later retry after that delay.
748
752
 
749
753
  ```ts
750
754
  new ShipmailClient({ apiKey, maxRetries: 0 }); // disable retries
package/dist/index.cjs CHANGED
@@ -1699,17 +1699,26 @@ var Webhooks = class extends ApiResource {
1699
1699
  };
1700
1700
 
1701
1701
  // src/version.ts
1702
- var VERSION = "0.5.5";
1702
+ var VERSION = "0.5.7";
1703
1703
 
1704
1704
  // src/client.ts
1705
1705
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
1706
1706
  var DEFAULT_MAX_RETRIES = 2;
1707
1707
  var DEFAULT_TIMEOUT = 3e4;
1708
+ var MAX_RETRY_AFTER_MS = 6e4;
1709
+ function parseRetryAfter(value) {
1710
+ if (!value?.trim()) return void 0;
1711
+ const seconds = Number(value);
1712
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
1713
+ if (/^[+-]?[\d.]+$/.test(value.trim())) return void 0;
1714
+ const date = Date.parse(value);
1715
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
1716
+ }
1708
1717
  function isRetryableStatus(status) {
1709
1718
  return status === 429 || status >= 500;
1710
1719
  }
1711
1720
  function calculateBackoff(attempt, retryAfterMs) {
1712
- if (retryAfterMs !== void 0 && retryAfterMs > 0) {
1721
+ if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
1713
1722
  return retryAfterMs;
1714
1723
  }
1715
1724
  const base2 = Math.min(2 ** attempt * 500, 8e3);
@@ -1752,6 +1761,9 @@ var ShipmailClient = class {
1752
1761
  this.defaultHeaders = config.defaultHeaders ? { ...config.defaultHeaders } : {};
1753
1762
  this.organizationId = config.organizationId;
1754
1763
  }
1764
+ if (!Number.isSafeInteger(this.maxRetries) || this.maxRetries < 0) {
1765
+ throw new RangeError("maxRetries must be a non-negative safe integer");
1766
+ }
1755
1767
  this.audiences = new Audiences(this);
1756
1768
  this.automations = new Automations(this);
1757
1769
  this.bookingPages = new BookingPages(this);
@@ -1787,9 +1799,13 @@ var ShipmailClient = class {
1787
1799
  const url = this.buildUrl(options.path, options.query);
1788
1800
  let lastError;
1789
1801
  const methodOpts = options.methodOptions;
1802
+ const retryHeaders = new Headers({ ...this.defaultHeaders, ...methodOpts?.headers });
1803
+ const method = options.method.toUpperCase();
1804
+ const idempotencyKey = methodOpts?.idempotencyKey || retryHeaders.get("Idempotency-Key");
1805
+ const canRetry = ["GET", "HEAD", "OPTIONS"].includes(method) || ["POST", "PATCH", "PUT"].includes(method) && Boolean(idempotencyKey?.trim());
1806
+ let retryAfterMs;
1790
1807
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
1791
1808
  if (attempt > 0) {
1792
- const retryAfterMs = lastError instanceof RateLimitError && lastError.retryAfter !== void 0 ? lastError.retryAfter * 1e3 : void 0;
1793
1809
  const delay = calculateBackoff(attempt - 1, retryAfterMs);
1794
1810
  await new Promise((resolve) => setTimeout(resolve, delay));
1795
1811
  }
@@ -1826,7 +1842,10 @@ var ShipmailClient = class {
1826
1842
  });
1827
1843
  } catch (err) {
1828
1844
  lastError = new ConnectionError(err instanceof Error ? err.message : "Request failed");
1829
- if (attempt < this.maxRetries) continue;
1845
+ if (canRetry && !methodOpts?.signal?.aborted && attempt < this.maxRetries) {
1846
+ retryAfterMs = void 0;
1847
+ continue;
1848
+ }
1830
1849
  throw lastError;
1831
1850
  }
1832
1851
  if (response.ok) {
@@ -1846,7 +1865,14 @@ var ShipmailClient = class {
1846
1865
  retryable: isRetryableStatus(response.status)
1847
1866
  });
1848
1867
  }
1849
- if (!lastError.retryable || attempt >= this.maxRetries) {
1868
+ retryAfterMs = parseRetryAfter(response.headers.get("Retry-After"));
1869
+ if (retryAfterMs === void 0 && lastError instanceof RateLimitError) {
1870
+ const seconds = lastError.retryAfter;
1871
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0) {
1872
+ retryAfterMs = seconds * 1e3;
1873
+ }
1874
+ }
1875
+ if (!canRetry || !lastError.retryable || attempt >= this.maxRetries || retryAfterMs !== void 0 && retryAfterMs > MAX_RETRY_AFTER_MS) {
1850
1876
  throw lastError;
1851
1877
  }
1852
1878
  }