@short.io/client-node 3.1.0 → 3.2.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 CHANGED
@@ -21,6 +21,7 @@ Official Node.js SDK for the [Short.io](https://short.io) URL shortening and lin
21
21
  - [Advanced Configuration](#advanced-configuration)
22
22
  - [TypeScript Support](#typescript-support)
23
23
  - [Rate Limits](#rate-limits)
24
+ - [Rate Limit Handling](#rate-limit-handling)
24
25
  - [Error Handling](#error-handling)
25
26
  - [Support](#support)
26
27
  - [License](#license)
@@ -38,6 +39,7 @@ Official Node.js SDK for the [Short.io](https://short.io) URL shortening and lin
38
39
  - **Permissions Management** - Control user access to links
39
40
  - **Full TypeScript Support** - Comprehensive type definitions included
40
41
  - **Modern ESM** - Built with ES modules
42
+ - **Automatic Rate Limit Handling** - Built-in retry logic for 429 responses with exponential backoff
41
43
 
42
44
  ## Requirements
43
45
 
@@ -701,6 +703,100 @@ interface Response<T> {
701
703
  | QR Bulk Generation | 1 request/minute |
702
704
  | Public API | 50 requests/second |
703
705
 
706
+ ## Rate Limit Handling
707
+
708
+ The SDK provides optional automatic retry logic for HTTP 429 (Too Many Requests) responses with exponential backoff.
709
+
710
+ **Default behavior:** No automatic retries. When a 429 response is received, it is returned immediately (or thrown if `throwOnError` is enabled). You must explicitly enable rate limit handling to get automatic retries.
711
+
712
+ ### Enable Global Rate Limiting
713
+
714
+ ```javascript
715
+ import { setApiKey, enableRateLimiting } from "@short.io/client-node";
716
+
717
+ setApiKey("YOUR_API_KEY");
718
+
719
+ // Enable with default settings
720
+ enableRateLimiting();
721
+ // Defaults: maxRetries=3, baseDelayMs=1000, maxDelayMs=60000
722
+
723
+ // Or customize the behavior
724
+ enableRateLimiting({
725
+ maxRetries: 5, // Maximum retry attempts (default: 3)
726
+ baseDelayMs: 1000, // Initial delay in ms (default: 1000)
727
+ maxDelayMs: 60000, // Maximum delay cap in ms (default: 60000)
728
+ onRateLimited: (info) => {
729
+ console.log(`Rate limited. Retry ${info.attempt} in ${info.delayMs}ms`);
730
+ console.log(`Limit: ${info.rateLimitLimit}, Remaining: ${info.rateLimitRemaining}`);
731
+ }
732
+ });
733
+ ```
734
+
735
+ ### Disable Rate Limiting
736
+
737
+ ```javascript
738
+ import { disableRateLimiting } from "@short.io/client-node";
739
+
740
+ disableRateLimiting();
741
+ ```
742
+
743
+ ### Per-Request Rate Limiting
744
+
745
+ Create a rate-limited client for specific requests:
746
+
747
+ ```javascript
748
+ import { createRateLimitedClient, createLink } from "@short.io/client-node";
749
+
750
+ const client = createRateLimitedClient({
751
+ maxRetries: 5,
752
+ onRateLimited: (info) => console.log(`Retry ${info.attempt}...`)
753
+ });
754
+
755
+ const result = await createLink({
756
+ client,
757
+ body: {
758
+ originalURL: "https://example.com",
759
+ domain: "your-domain.com"
760
+ }
761
+ });
762
+ ```
763
+
764
+ ### Rate Limit Info
765
+
766
+ The `onRateLimited` callback receives detailed information:
767
+
768
+ ```typescript
769
+ interface RateLimitInfo {
770
+ status: number; // HTTP status (429)
771
+ attempt: number; // Current retry attempt (1, 2, 3...)
772
+ delayMs: number; // Delay before next retry in ms
773
+ retryAfter?: number; // Seconds from Retry-After header
774
+ rateLimitLimit?: number; // Request limit per window (X-RateLimit-Limit)
775
+ rateLimitRemaining?: number; // Remaining requests (X-RateLimit-Remaining)
776
+ rateLimitReset?: number; // Unix timestamp when limit resets (X-RateLimit-Reset)
777
+ request: Request; // The request that was rate limited
778
+ }
779
+ ```
780
+
781
+ ### Configuration Options
782
+
783
+ | Option | Default | Description |
784
+ |--------|---------|-------------|
785
+ | `maxRetries` | `3` | Maximum number of retry attempts before returning the 429 response |
786
+ | `baseDelayMs` | `1000` | Initial delay in milliseconds for exponential backoff |
787
+ | `maxDelayMs` | `60000` | Maximum delay cap in milliseconds (1 minute) |
788
+ | `onRateLimited` | `undefined` | Optional callback invoked before each retry |
789
+
790
+ ### Backoff Strategy
791
+
792
+ The SDK uses exponential backoff with jitter:
793
+
794
+ 1. If `Retry-After` header is present, uses that value (capped at `maxDelayMs`)
795
+ 2. Otherwise, calculates delay as: `baseDelayMs * 2^(attempt-1) + random jitter`
796
+ 3. All delays are capped at `maxDelayMs`
797
+
798
+ After exhausting all retries, the 429 response is returned normally (or thrown if `throwOnError` is enabled).
799
+
704
800
  ## Error Handling
705
801
 
706
802
  ```javascript
package/dist/index.d.ts CHANGED
@@ -1,4 +1,29 @@
1
+ import type { Client } from "./generated/client/index.js";
1
2
  export * from './generated/types.gen.js';
2
3
  export * from './generated/sdk.gen.js';
3
4
  export * from './generated/zod.gen.js';
5
+ export interface RateLimitConfig {
6
+ enabled?: boolean;
7
+ maxRetries?: number;
8
+ baseDelayMs?: number;
9
+ maxDelayMs?: number;
10
+ onRateLimited?: (info: RateLimitInfo) => void;
11
+ }
12
+ export interface RateLimitInfo {
13
+ status: number;
14
+ attempt: number;
15
+ delayMs: number;
16
+ retryAfter?: number;
17
+ rateLimitLimit?: number;
18
+ rateLimitRemaining?: number;
19
+ rateLimitReset?: number;
20
+ request: Request;
21
+ }
22
+ export type SleepFunction = (ms: number) => Promise<void>;
23
+ export declare const defaultSleep: SleepFunction;
24
+ export declare function createRateLimitFetch(originalFetch: typeof fetch, config: RateLimitConfig, sleepFn?: SleepFunction): typeof fetch;
25
+ export declare function enableRateLimiting(config?: Partial<RateLimitConfig>): void;
26
+ export declare function disableRateLimiting(): void;
27
+ export declare function getRateLimitConfig(): Readonly<RateLimitConfig> | null;
28
+ export declare function createRateLimitedClient(config?: Partial<RateLimitConfig>): Client;
4
29
  export declare const setApiKey: (apiKey: string) => void;
package/dist/index.js CHANGED
@@ -1,7 +1,104 @@
1
1
  import { client } from "./generated/client.gen.js";
2
+ import { createClient, createConfig } from "./generated/client/index.js";
2
3
  export * from './generated/types.gen.js';
3
4
  export * from './generated/sdk.gen.js';
4
5
  export * from './generated/zod.gen.js';
6
+ const DEFAULT_RATE_LIMIT_CONFIG = {
7
+ enabled: true,
8
+ maxRetries: 3,
9
+ baseDelayMs: 1000,
10
+ maxDelayMs: 60000,
11
+ onRateLimited: undefined,
12
+ };
13
+ let globalRateLimitConfig = null;
14
+ function parseRateLimitHeaders(response) {
15
+ const retryAfterHeader = response.headers.get('Retry-After');
16
+ const rateLimitLimit = response.headers.get('X-RateLimit-Limit');
17
+ const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
18
+ const rateLimitReset = response.headers.get('X-RateLimit-Reset');
19
+ let retryAfter;
20
+ if (retryAfterHeader) {
21
+ const parsed = parseInt(retryAfterHeader, 10);
22
+ if (!isNaN(parsed)) {
23
+ retryAfter = parsed;
24
+ }
25
+ else {
26
+ const date = Date.parse(retryAfterHeader);
27
+ if (!isNaN(date)) {
28
+ retryAfter = Math.max(0, Math.ceil((date - Date.now()) / 1000));
29
+ }
30
+ }
31
+ }
32
+ return {
33
+ retryAfter,
34
+ rateLimitLimit: rateLimitLimit ? parseInt(rateLimitLimit, 10) : undefined,
35
+ rateLimitRemaining: rateLimitRemaining ? parseInt(rateLimitRemaining, 10) : undefined,
36
+ rateLimitReset: rateLimitReset ? parseInt(rateLimitReset, 10) : undefined,
37
+ };
38
+ }
39
+ function calculateBackoffDelay(attempt, baseDelay, maxDelay, retryAfter) {
40
+ if (retryAfter !== undefined) {
41
+ return Math.min(retryAfter * 1000, maxDelay);
42
+ }
43
+ const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
44
+ const jitter = Math.random() * 0.1 * exponentialDelay;
45
+ return Math.min(exponentialDelay + jitter, maxDelay);
46
+ }
47
+ export const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
48
+ export function createRateLimitFetch(originalFetch, config, sleepFn = defaultSleep) {
49
+ const mergedConfig = { ...DEFAULT_RATE_LIMIT_CONFIG, ...config };
50
+ return async function rateLimitFetch(input, init) {
51
+ let lastResponse;
52
+ const templateRequest = input instanceof Request ? input.clone() : new Request(input, init);
53
+ for (let attempt = 1; attempt <= mergedConfig.maxRetries + 1; attempt++) {
54
+ const requestForAttempt = templateRequest.clone();
55
+ const response = await originalFetch(requestForAttempt);
56
+ if (response.status !== 429) {
57
+ return response;
58
+ }
59
+ lastResponse = response;
60
+ if (attempt > mergedConfig.maxRetries) {
61
+ break;
62
+ }
63
+ const headerInfo = parseRateLimitHeaders(response);
64
+ const delayMs = calculateBackoffDelay(attempt, mergedConfig.baseDelayMs, mergedConfig.maxDelayMs, headerInfo.retryAfter);
65
+ if (mergedConfig.onRateLimited) {
66
+ const info = {
67
+ status: 429,
68
+ attempt,
69
+ delayMs,
70
+ ...headerInfo,
71
+ request: templateRequest.clone(),
72
+ };
73
+ mergedConfig.onRateLimited(info);
74
+ }
75
+ await sleepFn(delayMs);
76
+ }
77
+ return lastResponse;
78
+ };
79
+ }
80
+ export function enableRateLimiting(config = {}) {
81
+ globalRateLimitConfig = { ...DEFAULT_RATE_LIMIT_CONFIG, ...config, enabled: true };
82
+ client.setConfig({
83
+ fetch: createRateLimitFetch(globalThis.fetch, globalRateLimitConfig),
84
+ });
85
+ }
86
+ export function disableRateLimiting() {
87
+ globalRateLimitConfig = null;
88
+ client.setConfig({
89
+ fetch: globalThis.fetch,
90
+ });
91
+ }
92
+ export function getRateLimitConfig() {
93
+ return globalRateLimitConfig ? { ...globalRateLimitConfig } : null;
94
+ }
95
+ export function createRateLimitedClient(config = {}) {
96
+ const mergedConfig = { ...DEFAULT_RATE_LIMIT_CONFIG, ...config, enabled: true };
97
+ return createClient(createConfig({
98
+ baseUrl: 'https://api.short.io',
99
+ fetch: createRateLimitFetch(globalThis.fetch, mergedConfig),
100
+ }));
101
+ }
5
102
  client.setConfig({
6
103
  baseUrl: "https://api.short.io"
7
104
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAC/C,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AAEpC,MAAM,CAAC,SAAS,CAAC;IACb,OAAO,EAAE,sBAAsB;CAClC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAc,EAAE,EAAE;IACxC,MAAM,CAAC,SAAS,CAAC;QACb,OAAO,EAAE;YACL,aAAa,EAAE,MAAM;SACxB;KACJ,CAAC,CAAA;AACN,CAAC,CAAA","sourcesContent":["import { client } from \"./generated/client.gen\"\nexport * from './generated/types.gen';\nexport * from './generated/sdk.gen';\nexport * from './generated/zod.gen';\n\nclient.setConfig({\n baseUrl: \"https://api.short.io\"\n})\n\nexport const setApiKey = (apiKey: string) => {\n client.setConfig({\n headers: {\n authorization: apiKey\n }\n })\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { client } from \"./generated/client.gen\"\nimport { createClient, createConfig } from \"./generated/client\"\nimport type { Client } from \"./generated/client\"\nexport * from './generated/types.gen';\nexport * from './generated/sdk.gen';\nexport * from './generated/zod.gen';\n\n// ─── Rate Limit Types ────────────────────────────────────────────────────────\n\nexport interface RateLimitConfig {\n enabled?: boolean;\n maxRetries?: number;\n baseDelayMs?: number;\n maxDelayMs?: number;\n onRateLimited?: (info: RateLimitInfo) => void;\n}\n\nexport interface RateLimitInfo {\n status: number;\n attempt: number;\n delayMs: number;\n retryAfter?: number;\n rateLimitLimit?: number;\n rateLimitRemaining?: number;\n rateLimitReset?: number;\n request: Request;\n}\n\n// ─── Rate Limit Internal State ───────────────────────────────────────────────\n\nconst DEFAULT_RATE_LIMIT_CONFIG: Required<Omit<RateLimitConfig, 'onRateLimited'>> & Pick<RateLimitConfig, 'onRateLimited'> = {\n enabled: true,\n maxRetries: 3,\n baseDelayMs: 1000,\n maxDelayMs: 60000,\n onRateLimited: undefined,\n};\n\nlet globalRateLimitConfig: RateLimitConfig | null = null;\n\n// ─── Rate Limit Helper Functions ─────────────────────────────────────────────\n\nfunction parseRateLimitHeaders(response: Response): Pick<RateLimitInfo, 'retryAfter' | 'rateLimitLimit' | 'rateLimitRemaining' | 'rateLimitReset'> {\n const retryAfterHeader = response.headers.get('Retry-After');\n const rateLimitLimit = response.headers.get('X-RateLimit-Limit');\n const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');\n const rateLimitReset = response.headers.get('X-RateLimit-Reset');\n\n let retryAfter: number | undefined;\n if (retryAfterHeader) {\n const parsed = parseInt(retryAfterHeader, 10);\n if (!isNaN(parsed)) {\n retryAfter = parsed;\n } else {\n // Try parsing as HTTP-date\n const date = Date.parse(retryAfterHeader);\n if (!isNaN(date)) {\n retryAfter = Math.max(0, Math.ceil((date - Date.now()) / 1000));\n }\n }\n }\n\n return {\n retryAfter,\n rateLimitLimit: rateLimitLimit ? parseInt(rateLimitLimit, 10) : undefined,\n rateLimitRemaining: rateLimitRemaining ? parseInt(rateLimitRemaining, 10) : undefined,\n rateLimitReset: rateLimitReset ? parseInt(rateLimitReset, 10) : undefined,\n };\n}\n\nfunction calculateBackoffDelay(\n attempt: number,\n baseDelay: number,\n maxDelay: number,\n retryAfter?: number\n): number {\n if (retryAfter !== undefined) {\n return Math.min(retryAfter * 1000, maxDelay);\n }\n // Exponential backoff with jitter\n const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);\n const jitter = Math.random() * 0.1 * exponentialDelay;\n return Math.min(exponentialDelay + jitter, maxDelay);\n}\n\nexport type SleepFunction = (ms: number) => Promise<void>;\n\nexport const defaultSleep: SleepFunction = (ms: number) =>\n new Promise(resolve => setTimeout(resolve, ms));\n\n// ─── Rate Limit Fetch Wrapper ────────────────────────────────────────────────\n\nexport function createRateLimitFetch(\n originalFetch: typeof fetch,\n config: RateLimitConfig,\n sleepFn: SleepFunction = defaultSleep\n): typeof fetch {\n const mergedConfig = { ...DEFAULT_RATE_LIMIT_CONFIG, ...config };\n\n return async function rateLimitFetch(\n input: RequestInfo | URL,\n init?: RequestInit\n ): Promise<Response> {\n let lastResponse: Response | undefined;\n\n // If input is already a Request, clone it to preserve for retries\n // Clone once at the start to create a \"template\" we can clone for each attempt\n const templateRequest = input instanceof Request ? input.clone() : new Request(input, init);\n\n for (let attempt = 1; attempt <= mergedConfig.maxRetries + 1; attempt++) {\n // Clone the template for each attempt\n const requestForAttempt = templateRequest.clone();\n const response = await originalFetch(requestForAttempt);\n\n if (response.status !== 429) {\n return response;\n }\n\n lastResponse = response;\n\n if (attempt > mergedConfig.maxRetries) {\n break;\n }\n\n const headerInfo = parseRateLimitHeaders(response);\n const delayMs = calculateBackoffDelay(\n attempt,\n mergedConfig.baseDelayMs,\n mergedConfig.maxDelayMs,\n headerInfo.retryAfter\n );\n\n if (mergedConfig.onRateLimited) {\n const info: RateLimitInfo = {\n status: 429,\n attempt,\n delayMs,\n ...headerInfo,\n request: templateRequest.clone(),\n };\n mergedConfig.onRateLimited(info);\n }\n\n await sleepFn(delayMs);\n }\n\n return lastResponse!;\n };\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────────\n\nexport function enableRateLimiting(config: Partial<RateLimitConfig> = {}): void {\n globalRateLimitConfig = { ...DEFAULT_RATE_LIMIT_CONFIG, ...config, enabled: true };\n client.setConfig({\n fetch: createRateLimitFetch(globalThis.fetch, globalRateLimitConfig),\n });\n}\n\nexport function disableRateLimiting(): void {\n globalRateLimitConfig = null;\n client.setConfig({\n fetch: globalThis.fetch,\n });\n}\n\nexport function getRateLimitConfig(): Readonly<RateLimitConfig> | null {\n return globalRateLimitConfig ? { ...globalRateLimitConfig } : null;\n}\n\nexport function createRateLimitedClient(config: Partial<RateLimitConfig> = {}): Client {\n const mergedConfig = { ...DEFAULT_RATE_LIMIT_CONFIG, ...config, enabled: true };\n return createClient(createConfig({\n baseUrl: 'https://api.short.io',\n fetch: createRateLimitFetch(globalThis.fetch, mergedConfig),\n }));\n}\n\n// ─── Client Configuration ────────────────────────────────────────────────────\n\nclient.setConfig({\n baseUrl: \"https://api.short.io\"\n})\n\nexport const setApiKey = (apiKey: string) => {\n client.setConfig({\n headers: {\n authorization: apiKey\n }\n })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@short.io/client-node",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Official Short.io Node.js SDK for URL shortening, link management, QR codes, analytics, and geographic targeting",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",