ezthrottle 1.0.0 → 1.1.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/README.md +262 -10
- package/dist/client.d.ts +58 -0
- package/dist/client.js +177 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +25 -0
- package/dist/idempotentStrategy.d.ts +9 -0
- package/dist/idempotentStrategy.js +13 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +33 -0
- package/dist/step.d.ts +125 -0
- package/dist/step.js +411 -0
- package/dist/stepType.d.ts +9 -0
- package/dist/stepType.js +13 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.js +2 -0
- package/package.json +20 -5
- package/examples/basic.js +0 -43
- package/src/client.js +0 -136
- package/src/errors.js +0 -27
- package/src/index.js +0 -11
package/README.md
CHANGED
|
@@ -1,28 +1,280 @@
|
|
|
1
1
|
# EZThrottle Node.js SDK
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The API Dam for rate-limited services. Queue and execute HTTP requests with smart retry logic, multi-region racing, and webhook delivery.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install
|
|
8
|
+
npm install ezthrottle
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
const { EZThrottle } = require('
|
|
14
|
+
const { EZThrottle, Step, StepType } = require('ezthrottle');
|
|
15
15
|
|
|
16
|
-
const client = new EZThrottle({
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const client = new EZThrottle({ apiKey: 'your_api_key' });
|
|
17
|
+
|
|
18
|
+
// Simple job submission
|
|
19
|
+
const result = await new Step(client)
|
|
20
|
+
.url('https://api.example.com/endpoint')
|
|
21
|
+
.method('POST')
|
|
22
|
+
.type(StepType.PERFORMANCE)
|
|
23
|
+
.webhooks([{ url: 'https://your-app.com/webhook' }])
|
|
24
|
+
.execute();
|
|
25
|
+
|
|
26
|
+
console.log(`Job ID: ${result.job_id}`);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Step Types
|
|
30
|
+
|
|
31
|
+
### StepType.PERFORMANCE (Server-side execution)
|
|
32
|
+
|
|
33
|
+
Submit jobs to EZThrottle for distributed execution with multi-region racing and webhook delivery.
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
await new Step(client)
|
|
37
|
+
.url('https://api.stripe.com/charges')
|
|
38
|
+
.type(StepType.PERFORMANCE)
|
|
39
|
+
.webhooks([{ url: 'https://app.com/webhook' }])
|
|
40
|
+
.regions(['iad', 'lax', 'ord']) // Multi-region racing
|
|
41
|
+
.executionMode('race') // First completion wins
|
|
42
|
+
.execute();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### StepType.FRUGAL (Client-side first)
|
|
46
|
+
|
|
47
|
+
Execute locally first, only forward to EZThrottle on specific error codes. Saves money!
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
await new Step(client)
|
|
51
|
+
.url('https://api.example.com')
|
|
52
|
+
.type(StepType.FRUGAL)
|
|
53
|
+
.fallbackOnError([429, 500, 503]) // Forward to EZThrottle on these codes
|
|
54
|
+
.execute();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Idempotent Key Strategies
|
|
58
|
+
|
|
59
|
+
**Critical concept:** Idempotent keys prevent duplicate job execution. Choose the right strategy for your use case.
|
|
60
|
+
|
|
61
|
+
### IdempotentStrategy.HASH (Default)
|
|
62
|
+
|
|
63
|
+
Backend generates deterministic hash of (url, method, body, customer_id). **Prevents duplicates.**
|
|
64
|
+
|
|
65
|
+
**Use when:**
|
|
66
|
+
- Payment processing (don't charge twice!)
|
|
67
|
+
- Critical operations (create user, send notification)
|
|
68
|
+
- You want automatic deduplication
|
|
69
|
+
|
|
70
|
+
**Example:**
|
|
71
|
+
```javascript
|
|
72
|
+
const { IdempotentStrategy } = require('ezthrottle');
|
|
73
|
+
|
|
74
|
+
// Prevents duplicate charges - same request = rejected as duplicate
|
|
75
|
+
await new Step(client)
|
|
76
|
+
.url('https://api.stripe.com/charges')
|
|
77
|
+
.body(JSON.stringify({ amount: 1000, currency: 'usd' }))
|
|
78
|
+
.idempotentStrategy(IdempotentStrategy.HASH) // Default
|
|
79
|
+
.execute();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### IdempotentStrategy.UNIQUE
|
|
83
|
+
|
|
84
|
+
SDK generates unique UUID per request. **Allows duplicates.**
|
|
85
|
+
|
|
86
|
+
**Use when:**
|
|
87
|
+
- Polling endpoints (same URL, different data each time)
|
|
88
|
+
- Webhooks (want to send every time)
|
|
89
|
+
- Scheduled jobs (run every minute/hour)
|
|
90
|
+
- GET requests that return changing data
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```javascript
|
|
94
|
+
// Poll API every minute - each request gets unique UUID
|
|
95
|
+
setInterval(async () => {
|
|
96
|
+
await new Step(client)
|
|
97
|
+
.url('https://api.example.com/status')
|
|
98
|
+
.idempotentStrategy(IdempotentStrategy.UNIQUE) // New UUID each time
|
|
99
|
+
.execute();
|
|
100
|
+
}, 60000);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Workflow Chaining
|
|
104
|
+
|
|
105
|
+
Chain steps together with `.onSuccess()`, `.onFailure()`, and `.fallback()`:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// Analytics step (cheap)
|
|
109
|
+
const analytics = new Step(client)
|
|
110
|
+
.url('https://analytics.com/track')
|
|
111
|
+
.type(StepType.FRUGAL);
|
|
112
|
+
|
|
113
|
+
// Notification (fast, distributed)
|
|
114
|
+
const notification = new Step(client)
|
|
115
|
+
.url('https://notify.com')
|
|
116
|
+
.type(StepType.PERFORMANCE)
|
|
117
|
+
.webhooks([{ url: 'https://app.com/webhook' }])
|
|
118
|
+
.regions(['iad', 'lax'])
|
|
119
|
+
.onSuccess(analytics);
|
|
120
|
+
|
|
121
|
+
// Primary API call (cheap local execution)
|
|
122
|
+
await new Step(client)
|
|
123
|
+
.url('https://api.example.com')
|
|
124
|
+
.type(StepType.FRUGAL)
|
|
125
|
+
.fallbackOnError([429, 500])
|
|
126
|
+
.onSuccess(notification)
|
|
127
|
+
.execute();
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Fallback Chains
|
|
131
|
+
|
|
132
|
+
Handle failures with automatic fallback execution:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
const backupApi = new Step().url('https://backup-api.com');
|
|
136
|
+
|
|
137
|
+
await new Step(client)
|
|
138
|
+
.url('https://primary-api.com')
|
|
139
|
+
.fallback(backupApi, { triggerOnError: [500, 502, 503] })
|
|
140
|
+
.execute();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Multi-Region Racing
|
|
19
144
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
145
|
+
Submit jobs to multiple regions, fastest wins:
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
await new Step(client)
|
|
149
|
+
.url('https://api.example.com')
|
|
150
|
+
.regions(['iad', 'lax', 'ord']) // Try all 3 regions
|
|
151
|
+
.regionPolicy('fallback') // Auto-route if region down
|
|
152
|
+
.executionMode('race') // First completion wins
|
|
153
|
+
.webhooks([{ url: 'https://app.com/webhook' }])
|
|
154
|
+
.execute();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Webhook Fanout (Multiple Webhooks)
|
|
158
|
+
|
|
159
|
+
Deliver job results to multiple services simultaneously:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
await new Step(client)
|
|
163
|
+
.url('https://api.stripe.com/charges')
|
|
164
|
+
.method('POST')
|
|
165
|
+
.webhooks([
|
|
166
|
+
// Primary webhook (must succeed)
|
|
167
|
+
{ url: 'https://app.com/payment-complete', has_quorum_vote: true },
|
|
168
|
+
|
|
169
|
+
// Analytics webhook (optional)
|
|
170
|
+
{ url: 'https://analytics.com/track', has_quorum_vote: false },
|
|
171
|
+
|
|
172
|
+
// Notification service (must succeed)
|
|
173
|
+
{ url: 'https://notify.com/alert', has_quorum_vote: true },
|
|
174
|
+
|
|
175
|
+
// Multi-region webhook racing
|
|
176
|
+
{ url: 'https://backup.com/webhook', regions: ['iad', 'lax'], has_quorum_vote: true }
|
|
177
|
+
])
|
|
178
|
+
.webhookQuorum(2) // At least 2 webhooks with has_quorum_vote=true must succeed
|
|
179
|
+
.execute();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Retry Policies
|
|
183
|
+
|
|
184
|
+
Customize retry behavior:
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
await new Step(client)
|
|
188
|
+
.url('https://api.example.com')
|
|
189
|
+
.retryPolicy({
|
|
190
|
+
max_retries: 5,
|
|
191
|
+
max_reroutes: 3,
|
|
192
|
+
retry_codes: [429, 503], // Retry in same region
|
|
193
|
+
reroute_codes: [500, 502, 504] // Try different region
|
|
194
|
+
})
|
|
195
|
+
.execute();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Production Ready ✅
|
|
199
|
+
|
|
200
|
+
This SDK is production-ready with **working examples validated in CI on every push**.
|
|
201
|
+
|
|
202
|
+
### Reference Implementation: test-app/
|
|
203
|
+
|
|
204
|
+
The `test-app/` directory contains **real, working code** you can learn from. Not toy examples - this is production code we run in automated tests against live EZThrottle backend.
|
|
205
|
+
|
|
206
|
+
**Multi-Region Racing** ([test-app/app.js:104-122](test-app/app.js#L104-L122))
|
|
207
|
+
```javascript
|
|
208
|
+
await new Step(client)
|
|
209
|
+
.url('https://httpbin.org/delay/1')
|
|
210
|
+
.type(StepType.PERFORMANCE)
|
|
211
|
+
.webhooks([{ url: `${APP_URL}/webhook` }])
|
|
212
|
+
.regions(['iad', 'lax', 'ord']) // Race across 3 regions
|
|
213
|
+
.executionMode('race') // First completion wins
|
|
214
|
+
.execute();
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Idempotent HASH (Deduplication)** ([test-app/app.js:181-203](test-app/app.js#L181-L203))
|
|
218
|
+
```javascript
|
|
219
|
+
// Same request twice = same job_id (deduplicated)
|
|
220
|
+
await new Step(client)
|
|
221
|
+
.url(`https://httpbin.org/get?run=${runId}`)
|
|
222
|
+
.idempotentStrategy(IdempotentStrategy.HASH)
|
|
223
|
+
.execute();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Fallback Chain** ([test-app/app.js:125-154](test-app/app.js#L125-L154))
|
|
227
|
+
```javascript
|
|
228
|
+
await new Step(client)
|
|
229
|
+
.url('https://httpbin.org/status/500')
|
|
230
|
+
.fallback(
|
|
231
|
+
new Step().url('https://httpbin.org/status/200'),
|
|
232
|
+
{ triggerOnError: [500, 502, 503] }
|
|
233
|
+
)
|
|
234
|
+
.execute();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**On-Success Workflow** ([test-app/app.js:157-178](test-app/app.js#L157-L178))
|
|
238
|
+
```javascript
|
|
239
|
+
await new Step(client)
|
|
240
|
+
.url('https://httpbin.org/status/200')
|
|
241
|
+
.onSuccess(
|
|
242
|
+
new Step().url('https://httpbin.org/delay/1')
|
|
243
|
+
)
|
|
244
|
+
.execute();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**FRUGAL Local Execution** ([test-app/app.js:247-260](test-app/app.js#L247-L260))
|
|
248
|
+
```javascript
|
|
249
|
+
await new Step(client)
|
|
250
|
+
.url('https://httpbin.org/status/200')
|
|
251
|
+
.type(StepType.FRUGAL)
|
|
252
|
+
.execute();
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Validated in CI:**
|
|
256
|
+
- ✅ GitHub Actions runs these examples against live backend on every push
|
|
257
|
+
- ✅ 7 integration tests covering all SDK features
|
|
258
|
+
- ✅ Proves the code actually works, not just documentation
|
|
259
|
+
|
|
260
|
+
## Legacy API (Deprecated)
|
|
261
|
+
|
|
262
|
+
For backward compatibility, the old `queueRequest()` method is still available:
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
await client.queueRequest({
|
|
266
|
+
url: 'https://api.example.com',
|
|
267
|
+
webhookUrl: 'https://your-app.com/webhook', // Note: singular
|
|
268
|
+
method: 'POST'
|
|
23
269
|
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Prefer the new `Step` builder API for all new code!**
|
|
24
273
|
|
|
25
|
-
|
|
274
|
+
## Environment Variables
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
EZTHROTTLE_API_KEY=your_api_key_here
|
|
26
278
|
```
|
|
27
279
|
|
|
28
280
|
## License
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EZThrottleConfig, SubmitJobParams } from './types';
|
|
2
|
+
interface QueueRequestParams {
|
|
3
|
+
url: string;
|
|
4
|
+
webhookUrl?: string;
|
|
5
|
+
method?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
body?: string;
|
|
8
|
+
metadata?: Record<string, any>;
|
|
9
|
+
retryAt?: number;
|
|
10
|
+
}
|
|
11
|
+
interface QueueAndWaitParams extends QueueRequestParams {
|
|
12
|
+
timeout?: number;
|
|
13
|
+
pollInterval?: number;
|
|
14
|
+
}
|
|
15
|
+
interface RequestParams {
|
|
16
|
+
url: string;
|
|
17
|
+
method?: string;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
body?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class EZThrottle {
|
|
22
|
+
private apiKey;
|
|
23
|
+
private tracktagsUrl;
|
|
24
|
+
private ezthrottleUrl;
|
|
25
|
+
constructor({ apiKey, tracktagsUrl, ezthrottleUrl }: EZThrottleConfig);
|
|
26
|
+
/**
|
|
27
|
+
* Submit a job through TracktTags proxy → EZThrottle (NEW API with full features)
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} options - Job configuration
|
|
30
|
+
* @param {string} options.url - Target URL to request
|
|
31
|
+
* @param {string} [options.method='GET'] - HTTP method
|
|
32
|
+
* @param {Object} [options.headers] - Request headers
|
|
33
|
+
* @param {string} [options.body] - Request body
|
|
34
|
+
* @param {Object} [options.metadata] - Custom metadata
|
|
35
|
+
* @param {Array} [options.webhooks] - Array of webhook configs: [{url, regions, hasQuorumVote}]
|
|
36
|
+
* @param {number} [options.webhookQuorum=1] - Minimum webhooks that must succeed
|
|
37
|
+
* @param {Array<string>} [options.regions] - Regions to execute in (e.g., ['iad', 'lax', 'ord'])
|
|
38
|
+
* @param {string} [options.regionPolicy='fallback'] - 'fallback' or 'strict'
|
|
39
|
+
* @param {string} [options.executionMode='race'] - 'race' or 'fanout'
|
|
40
|
+
* @param {Object} [options.retryPolicy] - Retry configuration
|
|
41
|
+
* @param {Object} [options.fallbackJob] - Recursive fallback configuration
|
|
42
|
+
* @param {Object} [options.onSuccess] - Job to spawn on success
|
|
43
|
+
* @param {Object} [options.onFailure] - Job to spawn on failure
|
|
44
|
+
* @param {number} [options.onFailureTimeoutMs] - Timeout before triggering onFailure
|
|
45
|
+
* @param {string} [options.idempotentKey] - Deduplication key
|
|
46
|
+
* @param {number} [options.retryAt] - Timestamp (ms) when job can be retried
|
|
47
|
+
* @returns {Promise<Object>} - {job_id, status, ...}
|
|
48
|
+
*/
|
|
49
|
+
submitJob({ url, method, headers, body, metadata, webhooks, webhookQuorum, regions, regionPolicy, executionMode, retryPolicy, fallbackJob, onSuccess, onFailure, onFailureTimeoutMs, idempotentKey, retryAt, }: SubmitJobParams): Promise<any>;
|
|
50
|
+
/**
|
|
51
|
+
* DEPRECATED: Use submitJob() instead
|
|
52
|
+
* Legacy method for backward compatibility
|
|
53
|
+
*/
|
|
54
|
+
queueRequest({ url, webhookUrl, method, headers, body, metadata, retryAt }: QueueRequestParams): Promise<any>;
|
|
55
|
+
request({ url, method, headers, body }: RequestParams): Promise<any>;
|
|
56
|
+
queueAndWait({ url, webhookUrl, method, headers, body, metadata, retryAt, timeout, pollInterval, }: QueueAndWaitParams): Promise<any>;
|
|
57
|
+
}
|
|
58
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EZThrottle = void 0;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
class EZThrottle {
|
|
10
|
+
constructor({ apiKey, tracktagsUrl, ezthrottleUrl }) {
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
throw new Error('apiKey is required');
|
|
13
|
+
}
|
|
14
|
+
this.apiKey = apiKey;
|
|
15
|
+
this.tracktagsUrl = tracktagsUrl || 'https://tracktags.fly.dev';
|
|
16
|
+
this.ezthrottleUrl = ezthrottleUrl || 'https://ezthrottle.fly.dev';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Submit a job through TracktTags proxy → EZThrottle (NEW API with full features)
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} options - Job configuration
|
|
22
|
+
* @param {string} options.url - Target URL to request
|
|
23
|
+
* @param {string} [options.method='GET'] - HTTP method
|
|
24
|
+
* @param {Object} [options.headers] - Request headers
|
|
25
|
+
* @param {string} [options.body] - Request body
|
|
26
|
+
* @param {Object} [options.metadata] - Custom metadata
|
|
27
|
+
* @param {Array} [options.webhooks] - Array of webhook configs: [{url, regions, hasQuorumVote}]
|
|
28
|
+
* @param {number} [options.webhookQuorum=1] - Minimum webhooks that must succeed
|
|
29
|
+
* @param {Array<string>} [options.regions] - Regions to execute in (e.g., ['iad', 'lax', 'ord'])
|
|
30
|
+
* @param {string} [options.regionPolicy='fallback'] - 'fallback' or 'strict'
|
|
31
|
+
* @param {string} [options.executionMode='race'] - 'race' or 'fanout'
|
|
32
|
+
* @param {Object} [options.retryPolicy] - Retry configuration
|
|
33
|
+
* @param {Object} [options.fallbackJob] - Recursive fallback configuration
|
|
34
|
+
* @param {Object} [options.onSuccess] - Job to spawn on success
|
|
35
|
+
* @param {Object} [options.onFailure] - Job to spawn on failure
|
|
36
|
+
* @param {number} [options.onFailureTimeoutMs] - Timeout before triggering onFailure
|
|
37
|
+
* @param {string} [options.idempotentKey] - Deduplication key
|
|
38
|
+
* @param {number} [options.retryAt] - Timestamp (ms) when job can be retried
|
|
39
|
+
* @returns {Promise<Object>} - {job_id, status, ...}
|
|
40
|
+
*/
|
|
41
|
+
async submitJob({ url, method = 'GET', headers, body, metadata, webhooks, webhookQuorum = 1, regions, regionPolicy = 'fallback', executionMode = 'race', retryPolicy, fallbackJob, onSuccess, onFailure, onFailureTimeoutMs, idempotentKey, retryAt, }) {
|
|
42
|
+
// Build EZThrottle job payload
|
|
43
|
+
const jobPayload = {
|
|
44
|
+
url,
|
|
45
|
+
method: method.toUpperCase(),
|
|
46
|
+
};
|
|
47
|
+
// Add optional parameters
|
|
48
|
+
if (headers)
|
|
49
|
+
jobPayload.headers = headers;
|
|
50
|
+
if (body)
|
|
51
|
+
jobPayload.body = body;
|
|
52
|
+
if (metadata)
|
|
53
|
+
jobPayload.metadata = metadata;
|
|
54
|
+
if (webhooks)
|
|
55
|
+
jobPayload.webhooks = webhooks;
|
|
56
|
+
if (webhookQuorum !== 1)
|
|
57
|
+
jobPayload.webhook_quorum = webhookQuorum;
|
|
58
|
+
if (regions)
|
|
59
|
+
jobPayload.regions = regions;
|
|
60
|
+
if (regionPolicy !== 'fallback')
|
|
61
|
+
jobPayload.region_policy = regionPolicy;
|
|
62
|
+
if (executionMode !== 'race')
|
|
63
|
+
jobPayload.execution_mode = executionMode;
|
|
64
|
+
if (retryPolicy)
|
|
65
|
+
jobPayload.retry_policy = retryPolicy;
|
|
66
|
+
if (fallbackJob)
|
|
67
|
+
jobPayload.fallback_job = fallbackJob;
|
|
68
|
+
if (onSuccess)
|
|
69
|
+
jobPayload.on_success = onSuccess;
|
|
70
|
+
if (onFailure)
|
|
71
|
+
jobPayload.on_failure = onFailure;
|
|
72
|
+
if (onFailureTimeoutMs !== undefined)
|
|
73
|
+
jobPayload.on_failure_timeout_ms = onFailureTimeoutMs;
|
|
74
|
+
if (idempotentKey)
|
|
75
|
+
jobPayload.idempotent_key = idempotentKey;
|
|
76
|
+
if (retryAt !== undefined)
|
|
77
|
+
jobPayload.retry_at = retryAt;
|
|
78
|
+
console.log('[SDK] Sending jobPayload with idempotent_key:', jobPayload.idempotent_key);
|
|
79
|
+
// Build proxy request
|
|
80
|
+
const proxyPayload = {
|
|
81
|
+
scope: 'customer',
|
|
82
|
+
metric_name: '',
|
|
83
|
+
target_url: `${this.ezthrottleUrl}/api/v1/jobs`,
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json',
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(jobPayload),
|
|
89
|
+
};
|
|
90
|
+
const response = await (0, node_fetch_1.default)(`${this.tracktagsUrl}/api/v1/proxy`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify(proxyPayload),
|
|
97
|
+
});
|
|
98
|
+
// Handle proxy responses
|
|
99
|
+
if (response.status === 429) {
|
|
100
|
+
const errorData = await response.json();
|
|
101
|
+
const retryAfterSeconds = response.headers.get('retry-after');
|
|
102
|
+
let calculatedRetryAt = errorData.retry_at;
|
|
103
|
+
if (retryAfterSeconds && !calculatedRetryAt) {
|
|
104
|
+
calculatedRetryAt = Date.now() + (parseInt(retryAfterSeconds) * 1000);
|
|
105
|
+
}
|
|
106
|
+
throw new errors_1.RateLimitError(`Rate limited: ${errorData.error || 'Unknown error'}`, calculatedRetryAt);
|
|
107
|
+
}
|
|
108
|
+
if (response.status !== 200) {
|
|
109
|
+
const text = await response.text();
|
|
110
|
+
throw new errors_1.EZThrottleError(`Proxy request failed: ${text}`);
|
|
111
|
+
}
|
|
112
|
+
const proxyResponse = await response.json();
|
|
113
|
+
if (proxyResponse.status !== 'allowed') {
|
|
114
|
+
throw new errors_1.EZThrottleError(`Request denied: ${proxyResponse.error || 'Unknown error'}`);
|
|
115
|
+
}
|
|
116
|
+
const forwarded = proxyResponse.forwarded_response || {};
|
|
117
|
+
const statusCode = forwarded.status_code || 0;
|
|
118
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
119
|
+
throw new errors_1.EZThrottleError(`EZThrottle job creation failed: ${forwarded.body || 'Unknown error'}`);
|
|
120
|
+
}
|
|
121
|
+
const ezthrottleResponse = JSON.parse(forwarded.body || '{}');
|
|
122
|
+
return ezthrottleResponse;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* DEPRECATED: Use submitJob() instead
|
|
126
|
+
* Legacy method for backward compatibility
|
|
127
|
+
*/
|
|
128
|
+
async queueRequest({ url, webhookUrl, method = 'GET', headers, body, metadata, retryAt }) {
|
|
129
|
+
// Convert singular webhookUrl to webhooks array
|
|
130
|
+
const webhooks = webhookUrl
|
|
131
|
+
? [{ url: webhookUrl, has_quorum_vote: true }]
|
|
132
|
+
: undefined;
|
|
133
|
+
return this.submitJob({
|
|
134
|
+
url,
|
|
135
|
+
method,
|
|
136
|
+
headers,
|
|
137
|
+
body,
|
|
138
|
+
metadata,
|
|
139
|
+
webhooks,
|
|
140
|
+
retryAt,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async request({ url, method = 'GET', headers, body }) {
|
|
144
|
+
return (0, node_fetch_1.default)(url, {
|
|
145
|
+
method,
|
|
146
|
+
headers,
|
|
147
|
+
body,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async queueAndWait({ url, webhookUrl, method = 'GET', headers, body, metadata, retryAt, timeout = 300000, pollInterval = 2000, }) {
|
|
151
|
+
const result = await this.queueRequest({
|
|
152
|
+
url,
|
|
153
|
+
webhookUrl,
|
|
154
|
+
method,
|
|
155
|
+
headers,
|
|
156
|
+
body,
|
|
157
|
+
metadata,
|
|
158
|
+
retryAt,
|
|
159
|
+
});
|
|
160
|
+
const jobId = result.job_id;
|
|
161
|
+
if (!jobId) {
|
|
162
|
+
throw new errors_1.EZThrottleError('No job_id in response');
|
|
163
|
+
}
|
|
164
|
+
const startTime = Date.now();
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const checkResult = async () => {
|
|
167
|
+
if (Date.now() - startTime > timeout) {
|
|
168
|
+
reject(new errors_1.TimeoutError(`Timeout waiting for job ${jobId}`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
setTimeout(checkResult, pollInterval);
|
|
172
|
+
};
|
|
173
|
+
checkResult();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
exports.EZThrottle = EZThrottle;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class EZThrottleError extends Error {
|
|
2
|
+
retryAt: number | null;
|
|
3
|
+
constructor(message: string, retryAt?: number | null);
|
|
4
|
+
}
|
|
5
|
+
export declare class TimeoutError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class RateLimitError extends EZThrottleError {
|
|
9
|
+
constructor(message: string, retryAt: number);
|
|
10
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RateLimitError = exports.TimeoutError = exports.EZThrottleError = void 0;
|
|
4
|
+
class EZThrottleError extends Error {
|
|
5
|
+
constructor(message, retryAt = null) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'EZThrottleError';
|
|
8
|
+
this.retryAt = retryAt;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.EZThrottleError = EZThrottleError;
|
|
12
|
+
class TimeoutError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'TimeoutError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.TimeoutError = TimeoutError;
|
|
19
|
+
class RateLimitError extends EZThrottleError {
|
|
20
|
+
constructor(message, retryAt) {
|
|
21
|
+
super(message, retryAt);
|
|
22
|
+
this.name = 'RateLimitError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.RateLimitError = RateLimitError;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent key generation strategy
|
|
3
|
+
*/
|
|
4
|
+
export declare enum IdempotentStrategy {
|
|
5
|
+
/** Backend generates deterministic hash - prevents duplicates (DEFAULT) */
|
|
6
|
+
HASH = "hash",
|
|
7
|
+
/** SDK generates UUID - allows duplicates (polling, webhooks, scheduled jobs) */
|
|
8
|
+
UNIQUE = "unique"
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdempotentStrategy = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Idempotent key generation strategy
|
|
6
|
+
*/
|
|
7
|
+
var IdempotentStrategy;
|
|
8
|
+
(function (IdempotentStrategy) {
|
|
9
|
+
/** Backend generates deterministic hash - prevents duplicates (DEFAULT) */
|
|
10
|
+
IdempotentStrategy["HASH"] = "hash";
|
|
11
|
+
/** SDK generates UUID - allows duplicates (polling, webhooks, scheduled jobs) */
|
|
12
|
+
IdempotentStrategy["UNIQUE"] = "unique";
|
|
13
|
+
})(IdempotentStrategy || (exports.IdempotentStrategy = IdempotentStrategy = {}));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { EZThrottle } from './client';
|
|
2
|
+
export { EZThrottleError, TimeoutError, RateLimitError } from './errors';
|
|
3
|
+
export { Step } from './step';
|
|
4
|
+
export { StepType } from './stepType';
|
|
5
|
+
export { IdempotentStrategy } from './idempotentStrategy';
|
|
6
|
+
export * from './types';
|
|
7
|
+
import { EZThrottle } from './client';
|
|
8
|
+
export default EZThrottle;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.IdempotentStrategy = exports.StepType = exports.Step = exports.RateLimitError = exports.TimeoutError = exports.EZThrottleError = exports.EZThrottle = void 0;
|
|
18
|
+
var client_1 = require("./client");
|
|
19
|
+
Object.defineProperty(exports, "EZThrottle", { enumerable: true, get: function () { return client_1.EZThrottle; } });
|
|
20
|
+
var errors_1 = require("./errors");
|
|
21
|
+
Object.defineProperty(exports, "EZThrottleError", { enumerable: true, get: function () { return errors_1.EZThrottleError; } });
|
|
22
|
+
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_1.TimeoutError; } });
|
|
23
|
+
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_1.RateLimitError; } });
|
|
24
|
+
var step_1 = require("./step");
|
|
25
|
+
Object.defineProperty(exports, "Step", { enumerable: true, get: function () { return step_1.Step; } });
|
|
26
|
+
var stepType_1 = require("./stepType");
|
|
27
|
+
Object.defineProperty(exports, "StepType", { enumerable: true, get: function () { return stepType_1.StepType; } });
|
|
28
|
+
var idempotentStrategy_1 = require("./idempotentStrategy");
|
|
29
|
+
Object.defineProperty(exports, "IdempotentStrategy", { enumerable: true, get: function () { return idempotentStrategy_1.IdempotentStrategy; } });
|
|
30
|
+
__exportStar(require("./types"), exports);
|
|
31
|
+
// Default export for CommonJS compatibility
|
|
32
|
+
const client_2 = require("./client");
|
|
33
|
+
exports.default = client_2.EZThrottle;
|