@spooled/sdk 1.0.9 → 1.0.11
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 +96 -432
- package/dist/{index-GVY6uW_o.d.cts → index-BAIulGbd.d.cts} +13 -6
- package/dist/{index-GVY6uW_o.d.ts → index-BAIulGbd.d.ts} +13 -6
- package/dist/index.cjs +13 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -6
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +1 -1
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +1 -1
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,15 @@ Official Node.js/TypeScript SDK for [Spooled Cloud](https://spooled.cloud) - a m
|
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
10
|
+
- **Full TypeScript support** with comprehensive type definitions
|
|
11
|
+
- **ESM and CommonJS** dual package support
|
|
12
|
+
- **Automatic retries** with exponential backoff and jitter
|
|
13
|
+
- **Circuit breaker** for fault tolerance
|
|
14
|
+
- **Realtime events** via WebSocket and SSE
|
|
15
|
+
- **Worker runtime** for processing jobs
|
|
16
|
+
- **Workflow DAGs** for complex job dependencies
|
|
17
|
+
- **gRPC streaming** for high-performance workers
|
|
18
|
+
- **Automatic case conversion** between camelCase (SDK) and snake_case (API)
|
|
17
19
|
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
@@ -26,9 +28,9 @@ npm install @spooled/sdk
|
|
|
26
28
|
```typescript
|
|
27
29
|
import { SpooledClient } from '@spooled/sdk';
|
|
28
30
|
|
|
29
|
-
// Create a client
|
|
30
31
|
const client = new SpooledClient({
|
|
31
|
-
apiKey: 'sk_live_your_api_key'
|
|
32
|
+
apiKey: 'sk_live_your_api_key',
|
|
33
|
+
// For self-hosted: baseUrl: 'https://your-spooled-server.com'
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
// Create a job
|
|
@@ -50,401 +52,147 @@ const job = await client.jobs.get(id);
|
|
|
50
52
|
console.log(`Status: ${job.status}`);
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
##
|
|
55
|
+
## Documentation
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
timeout: 30000, // Request timeout (ms)
|
|
64
|
-
|
|
65
|
-
// Retry settings
|
|
66
|
-
retries: 3, // Max retry attempts
|
|
67
|
-
retryDelay: 1000, // Base delay (ms)
|
|
68
|
-
retry: {
|
|
69
|
-
maxRetries: 3,
|
|
70
|
-
baseDelay: 1000,
|
|
71
|
-
maxDelay: 30000,
|
|
72
|
-
factor: 2, // Exponential factor
|
|
73
|
-
jitter: true // Add randomness
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
// Circuit breaker
|
|
77
|
-
circuitBreaker: {
|
|
78
|
-
enabled: true,
|
|
79
|
-
failureThreshold: 5, // Failures to open
|
|
80
|
-
successThreshold: 3, // Successes to close
|
|
81
|
-
timeout: 30000 // Reset timeout (ms)
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
// Advanced
|
|
85
|
-
headers: { 'X-Custom': 'header' },
|
|
86
|
-
fetch: customFetch, // Custom fetch impl
|
|
87
|
-
debug: true // Enable logging
|
|
88
|
-
});
|
|
89
|
-
```
|
|
57
|
+
| Guide | Description |
|
|
58
|
+
|-------|-------------|
|
|
59
|
+
| [Getting Started](docs/getting-started.md) | Installation, setup, and first job |
|
|
60
|
+
| [Configuration](docs/configuration.md) | All configuration options |
|
|
61
|
+
| [Workers](docs/workers.md) | Job processing with SpooledWorker |
|
|
62
|
+
| [Workflows](docs/workflows.md) | Building DAGs with job dependencies |
|
|
63
|
+
| [gRPC](docs/grpc.md) | High-performance streaming |
|
|
64
|
+
| [Resources](docs/resources.md) | Complete API reference |
|
|
90
65
|
|
|
91
|
-
##
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
See the [`examples/`](examples/) directory for runnable code:
|
|
69
|
+
|
|
70
|
+
| Example | Description |
|
|
71
|
+
|---------|-------------|
|
|
72
|
+
| [`quick-start.ts`](examples/quick-start.ts) | Basic SDK usage |
|
|
73
|
+
| [`worker.ts`](examples/worker.ts) | Processing jobs with SpooledWorker |
|
|
74
|
+
| [`workflow-dag.ts`](examples/workflow-dag.ts) | Complex workflows with dependencies |
|
|
75
|
+
| [`grpc-streaming.ts`](examples/grpc-streaming.ts) | High-performance gRPC streaming |
|
|
76
|
+
| [`realtime.ts`](examples/realtime.ts) | Real-time event streaming |
|
|
77
|
+
| [`schedules.ts`](examples/schedules.ts) | Cron schedules |
|
|
78
|
+
| [`error-handling.ts`](examples/error-handling.ts) | Error handling patterns |
|
|
79
|
+
|
|
80
|
+
## Core Concepts
|
|
92
81
|
|
|
93
82
|
### Jobs
|
|
94
83
|
|
|
84
|
+
Jobs are units of work with payloads, priorities, and retry policies:
|
|
85
|
+
|
|
95
86
|
```typescript
|
|
96
|
-
|
|
97
|
-
const { id, created } = await client.jobs.create({
|
|
87
|
+
const { id } = await client.jobs.create({
|
|
98
88
|
queueName: 'my-queue',
|
|
99
89
|
payload: { data: 'value' },
|
|
100
90
|
priority: 5, // -100 to 100
|
|
101
91
|
maxRetries: 3,
|
|
102
92
|
timeoutSeconds: 300,
|
|
103
|
-
scheduledAt: new Date(Date.now() + 60000),
|
|
104
|
-
idempotencyKey: 'unique-key',
|
|
105
|
-
tags: { env: 'production' }
|
|
93
|
+
scheduledAt: new Date(Date.now() + 60000),
|
|
94
|
+
idempotencyKey: 'unique-key',
|
|
106
95
|
});
|
|
96
|
+
```
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
const jobs = await client.jobs.list({
|
|
110
|
-
queueName: 'my-queue',
|
|
111
|
-
status: 'pending',
|
|
112
|
-
limit: 10,
|
|
113
|
-
offset: 0
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Get job details
|
|
117
|
-
const job = await client.jobs.get(id);
|
|
118
|
-
|
|
119
|
-
// Cancel a job
|
|
120
|
-
await client.jobs.cancel(id);
|
|
98
|
+
### Workers
|
|
121
99
|
|
|
122
|
-
|
|
123
|
-
await client.jobs.retry(id);
|
|
100
|
+
Process jobs with the built-in worker runtime:
|
|
124
101
|
|
|
125
|
-
|
|
126
|
-
|
|
102
|
+
```typescript
|
|
103
|
+
import { SpooledClient, SpooledWorker } from '@spooled/sdk';
|
|
127
104
|
|
|
128
|
-
|
|
129
|
-
const stats = await client.jobs.getStats();
|
|
105
|
+
const client = new SpooledClient({ apiKey: 'sk_live_...' });
|
|
130
106
|
|
|
131
|
-
|
|
132
|
-
const result = await client.jobs.bulkEnqueue({
|
|
107
|
+
const worker = new SpooledWorker(client, {
|
|
133
108
|
queueName: 'my-queue',
|
|
134
|
-
|
|
135
|
-
{ payload: { n: 1 } },
|
|
136
|
-
{ payload: { n: 2 } },
|
|
137
|
-
{ payload: { n: 3 } }
|
|
138
|
-
]
|
|
109
|
+
concurrency: 10,
|
|
139
110
|
});
|
|
140
111
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const dlqJobs = await client.jobs.dlq.list();
|
|
146
|
-
await client.jobs.dlq.retry({ jobIds: ['id1', 'id2'] });
|
|
147
|
-
await client.jobs.dlq.purge({ confirm: true });
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Queues
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
// List queues
|
|
154
|
-
const queues = await client.queues.list();
|
|
155
|
-
|
|
156
|
-
// Get queue config
|
|
157
|
-
const config = await client.queues.get('my-queue');
|
|
158
|
-
|
|
159
|
-
// Update queue config
|
|
160
|
-
await client.queues.updateConfig('my-queue', {
|
|
161
|
-
maxRetries: 5,
|
|
162
|
-
defaultTimeout: 600,
|
|
163
|
-
rateLimit: 100
|
|
112
|
+
worker.process(async (ctx) => {
|
|
113
|
+
console.log(`Processing job ${ctx.jobId}`);
|
|
114
|
+
await doSomething(ctx.payload);
|
|
115
|
+
return { success: true };
|
|
164
116
|
});
|
|
165
117
|
|
|
166
|
-
|
|
167
|
-
const stats = await client.queues.getStats('my-queue');
|
|
118
|
+
await worker.start();
|
|
168
119
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
await client.queues.resume('my-queue');
|
|
120
|
+
// Graceful shutdown
|
|
121
|
+
process.on('SIGTERM', () => worker.stop());
|
|
172
122
|
```
|
|
173
123
|
|
|
174
|
-
###
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
// Create a schedule
|
|
178
|
-
const schedule = await client.schedules.create({
|
|
179
|
-
name: 'Daily Report',
|
|
180
|
-
cronExpression: '0 0 9 * * *', // 6-field cron
|
|
181
|
-
timezone: 'America/New_York',
|
|
182
|
-
queueName: 'reports',
|
|
183
|
-
payloadTemplate: { type: 'daily' }
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// List schedules
|
|
187
|
-
const schedules = await client.schedules.list();
|
|
188
|
-
|
|
189
|
-
// Get/update/delete
|
|
190
|
-
const s = await client.schedules.get(schedule.id);
|
|
191
|
-
await client.schedules.update(schedule.id, { cronExpression: '0 0 8 * * *' });
|
|
192
|
-
await client.schedules.delete(schedule.id);
|
|
193
|
-
|
|
194
|
-
// Pause/resume
|
|
195
|
-
await client.schedules.pause(schedule.id);
|
|
196
|
-
await client.schedules.resume(schedule.id);
|
|
124
|
+
### Workflows (DAGs)
|
|
197
125
|
|
|
198
|
-
|
|
199
|
-
const { jobId } = await client.schedules.trigger(schedule.id);
|
|
200
|
-
|
|
201
|
-
// Get history
|
|
202
|
-
const runs = await client.schedules.getHistory(schedule.id, 10);
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Workflows
|
|
126
|
+
Orchestrate multiple jobs with dependencies:
|
|
206
127
|
|
|
207
128
|
```typescript
|
|
208
|
-
// Create a workflow with dependencies
|
|
209
129
|
const workflow = await client.workflows.create({
|
|
210
130
|
name: 'ETL Pipeline',
|
|
211
131
|
jobs: [
|
|
212
132
|
{ key: 'extract', queueName: 'etl', payload: { step: 'extract' } },
|
|
213
133
|
{ key: 'transform', queueName: 'etl', payload: { step: 'transform' }, dependsOn: ['extract'] },
|
|
214
|
-
{ key: 'load', queueName: 'etl', payload: { step: 'load' }, dependsOn: ['transform'] }
|
|
215
|
-
]
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Get workflow status
|
|
219
|
-
const status = await client.workflows.get(workflow.workflowId);
|
|
220
|
-
|
|
221
|
-
// Cancel workflow
|
|
222
|
-
await client.workflows.cancel(workflow.workflowId);
|
|
223
|
-
|
|
224
|
-
// Job dependencies
|
|
225
|
-
const deps = await client.workflows.jobs.getDependencies(jobId);
|
|
226
|
-
await client.workflows.jobs.addDependencies(jobId, { dependsOnJobIds: ['other-job'] });
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Webhooks
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
// Create outgoing webhook
|
|
233
|
-
const webhook = await client.webhooks.create({
|
|
234
|
-
name: 'Slack Notifications',
|
|
235
|
-
url: 'https://hooks.slack.com/...',
|
|
236
|
-
events: ['job.completed', 'job.failed'],
|
|
237
|
-
secret: 'hmac-secret'
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// List/get/update/delete
|
|
241
|
-
const webhooks = await client.webhooks.list();
|
|
242
|
-
const wh = await client.webhooks.get(webhook.id);
|
|
243
|
-
await client.webhooks.update(webhook.id, { enabled: false });
|
|
244
|
-
await client.webhooks.delete(webhook.id);
|
|
245
|
-
|
|
246
|
-
// Test webhook
|
|
247
|
-
const result = await client.webhooks.test(webhook.id);
|
|
248
|
-
|
|
249
|
-
// Get delivery history
|
|
250
|
-
const deliveries = await client.webhooks.getDeliveries(webhook.id);
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### API Keys
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
// List keys (sensitive data hidden)
|
|
257
|
-
const keys = await client.apiKeys.list();
|
|
258
|
-
|
|
259
|
-
// Create new key (key shown only once!)
|
|
260
|
-
const { id, key } = await client.apiKeys.create({
|
|
261
|
-
name: 'Production API Key',
|
|
262
|
-
queues: ['queue-1', 'queue-2'], // Optional: restrict to queues
|
|
263
|
-
rateLimit: 1000
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Update/revoke
|
|
267
|
-
await client.apiKeys.update(id, { name: 'Updated Name' });
|
|
268
|
-
await client.apiKeys.revoke(id);
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Organizations
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
// Create organization (returns initial API key)
|
|
275
|
-
const { organization, apiKey } = await client.organizations.create({
|
|
276
|
-
name: 'My Company',
|
|
277
|
-
slug: 'my-company',
|
|
278
|
-
billingEmail: 'billing@company.com'
|
|
134
|
+
{ key: 'load', queueName: 'etl', payload: { step: 'load' }, dependsOn: ['transform'] },
|
|
135
|
+
],
|
|
279
136
|
});
|
|
280
|
-
|
|
281
|
-
// Get usage and limits
|
|
282
|
-
const usage = await client.organizations.getUsage();
|
|
283
|
-
console.log(`Plan: ${usage.plan} (${usage.planDisplayName})`);
|
|
284
|
-
console.log(`Jobs today: ${usage.usage.jobsToday.current}/${usage.usage.jobsToday.limit ?? 'unlimited'}`);
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### Billing
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
// Get billing status (Stripe subscription info)
|
|
291
|
-
const status = await client.billing.getStatus();
|
|
292
|
-
console.log(status.planTier, status.hasStripeCustomer);
|
|
293
|
-
|
|
294
|
-
// Create Stripe billing portal session
|
|
295
|
-
const portal = await client.billing.createPortal({
|
|
296
|
-
returnUrl: 'https://spooled.cloud/dashboard/billing'
|
|
297
|
-
});
|
|
298
|
-
console.log('Portal URL:', portal.url);
|
|
299
137
|
```
|
|
300
138
|
|
|
301
|
-
###
|
|
302
|
-
|
|
303
|
-
```typescript
|
|
304
|
-
const dashboard = await client.dashboard.get();
|
|
305
|
-
console.log('Jobs pending:', dashboard.jobs.pending);
|
|
306
|
-
console.log('Queues:', dashboard.queues.length);
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Health & Metrics
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
const health = await client.health.get();
|
|
313
|
-
console.log(health.status, health.database, health.cache);
|
|
314
|
-
|
|
315
|
-
const ready = await client.health.readiness();
|
|
316
|
-
console.log('Ready:', ready);
|
|
317
|
-
|
|
318
|
-
const metrics = await client.metrics.get();
|
|
319
|
-
console.log(metrics.slice(0, 200));
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Admin API
|
|
323
|
-
|
|
324
|
-
```typescript
|
|
325
|
-
const adminClient = new SpooledClient({
|
|
326
|
-
apiKey: 'sk_live_...',
|
|
327
|
-
adminKey: 'admin_...'
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
const orgs = await adminClient.admin.listOrganizations({ planTier: 'pro', limit: 10 });
|
|
331
|
-
console.log(orgs.total);
|
|
332
|
-
|
|
333
|
-
const stats = await adminClient.admin.getStats();
|
|
334
|
-
console.log(stats.organizations.total);
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Webhook Ingestion
|
|
338
|
-
|
|
339
|
-
These endpoints are primarily for webhook providers (GitHub/Stripe/custom) and are **signature-based** (not API key based).
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
await client.ingest.custom('org_123', {
|
|
343
|
-
queueName: 'custom_events',
|
|
344
|
-
eventType: 'custom.event',
|
|
345
|
-
payload: { hello: 'world' },
|
|
346
|
-
});
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
## Worker Runtime
|
|
139
|
+
### Schedules
|
|
350
140
|
|
|
351
|
-
|
|
141
|
+
Run jobs on a cron schedule:
|
|
352
142
|
|
|
353
143
|
```typescript
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
concurrency: 10, // Process up to 10 jobs simultaneously
|
|
361
|
-
pollInterval: 1000, // Poll every second
|
|
362
|
-
leaseDuration: 30, // 30 second lease
|
|
363
|
-
shutdownTimeout: 30000 // Wait 30s for graceful shutdown
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Define job handler
|
|
367
|
-
worker.process(async (ctx) => {
|
|
368
|
-
console.log(`Processing job ${ctx.jobId}`);
|
|
369
|
-
console.log('Payload:', ctx.payload);
|
|
370
|
-
|
|
371
|
-
// Check for shutdown signal
|
|
372
|
-
if (ctx.signal.aborted) {
|
|
373
|
-
throw new Error('Job aborted');
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Do work...
|
|
377
|
-
await doSomething(ctx.payload);
|
|
378
|
-
|
|
379
|
-
// Return optional result
|
|
380
|
-
return { processed: true };
|
|
144
|
+
const schedule = await client.schedules.create({
|
|
145
|
+
name: 'Daily Report',
|
|
146
|
+
cronExpression: '0 0 9 * * *', // 6-field cron
|
|
147
|
+
timezone: 'America/New_York',
|
|
148
|
+
queueName: 'reports',
|
|
149
|
+
payloadTemplate: { type: 'daily' },
|
|
381
150
|
});
|
|
382
|
-
|
|
383
|
-
// Event handlers
|
|
384
|
-
worker.on('job:completed', ({ jobId }) => console.log(`Completed: ${jobId}`));
|
|
385
|
-
worker.on('job:failed', ({ jobId, error }) => console.error(`Failed: ${jobId}`, error));
|
|
386
|
-
|
|
387
|
-
// Start worker
|
|
388
|
-
await worker.start();
|
|
389
|
-
|
|
390
|
-
// Graceful shutdown
|
|
391
|
-
process.on('SIGTERM', () => worker.stop());
|
|
392
151
|
```
|
|
393
152
|
|
|
394
|
-
|
|
153
|
+
### Realtime Events
|
|
395
154
|
|
|
396
|
-
Subscribe to real-time events
|
|
155
|
+
Subscribe to real-time job events:
|
|
397
156
|
|
|
398
157
|
```typescript
|
|
399
158
|
const realtime = await client.realtime();
|
|
400
159
|
|
|
401
|
-
// Connection state
|
|
402
|
-
realtime.onStateChange((state) => {
|
|
403
|
-
console.log('State:', state); // 'connecting' | 'connected' | 'reconnecting' | 'disconnected'
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// Event handlers
|
|
407
|
-
realtime.on('job.created', (data) => {
|
|
408
|
-
console.log(`New job: ${data.jobId} in ${data.queueName}`);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
160
|
realtime.on('job.completed', (data) => {
|
|
412
|
-
console.log(`Job completed: ${data.jobId}
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
realtime.on('job.failed', (data) => {
|
|
416
|
-
console.log(`Job failed: ${data.jobId} - ${data.error}`);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
// Listen to all events
|
|
420
|
-
realtime.onEvent((event) => {
|
|
421
|
-
console.log(event.type, event);
|
|
161
|
+
console.log(`Job completed: ${data.jobId}`);
|
|
422
162
|
});
|
|
423
163
|
|
|
424
|
-
// Connect and subscribe
|
|
425
164
|
await realtime.connect();
|
|
426
165
|
await realtime.subscribe({ queueName: 'my-queue' });
|
|
427
|
-
|
|
428
|
-
// Cleanup
|
|
429
|
-
realtime.disconnect();
|
|
430
166
|
```
|
|
431
167
|
|
|
432
|
-
|
|
168
|
+
## Error Handling
|
|
433
169
|
|
|
434
|
-
|
|
435
|
-
- **SSE**: `type: 'sse'`. In the current backend implementation, SSE queue/job endpoints periodically query the database, which can be heavier at scale. Use SSE primarily when WebSocket is not possible.
|
|
170
|
+
All errors extend `SpooledError` with specific subclasses:
|
|
436
171
|
|
|
437
172
|
```typescript
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
173
|
+
import {
|
|
174
|
+
NotFoundError,
|
|
175
|
+
RateLimitError,
|
|
176
|
+
ValidationError,
|
|
177
|
+
isSpooledError,
|
|
178
|
+
} from '@spooled/sdk';
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await client.jobs.get('non-existent');
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof NotFoundError) {
|
|
184
|
+
console.log('Job not found');
|
|
185
|
+
} else if (error instanceof RateLimitError) {
|
|
186
|
+
console.log(`Retry after ${error.getRetryAfter()} seconds`);
|
|
187
|
+
} else if (isSpooledError(error)) {
|
|
188
|
+
console.log(`API error: ${error.code} - ${error.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
441
191
|
```
|
|
442
192
|
|
|
443
193
|
## gRPC API
|
|
444
194
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
For high-throughput workers or streaming scenarios, consider using the native gRPC client with `@grpc/grpc-js`:
|
|
195
|
+
For high-throughput workers, use the native gRPC API:
|
|
448
196
|
|
|
449
197
|
```typescript
|
|
450
198
|
import * as grpc from '@grpc/grpc-js';
|
|
@@ -454,27 +202,18 @@ const packageDefinition = protoLoader.loadSync('spooled.proto');
|
|
|
454
202
|
const spooled = grpc.loadPackageDefinition(packageDefinition).spooled.v1;
|
|
455
203
|
|
|
456
204
|
const client = new spooled.QueueService(
|
|
457
|
-
'
|
|
205
|
+
'grpc.spooled.cloud:443',
|
|
458
206
|
grpc.credentials.createSsl()
|
|
459
207
|
);
|
|
460
208
|
|
|
461
|
-
// Add API key metadata to calls
|
|
462
209
|
const metadata = new grpc.Metadata();
|
|
463
210
|
metadata.add('x-api-key', 'sk_live_your_key');
|
|
464
211
|
|
|
465
|
-
|
|
466
|
-
queue_name: 'emails',
|
|
467
|
-
payload: { to: 'user@example.com' },
|
|
468
|
-
max_retries: 3
|
|
469
|
-
}, metadata, (err, response) => {
|
|
470
|
-
console.log('Job ID:', response.job_id);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// Streaming example
|
|
212
|
+
// Stream jobs
|
|
474
213
|
const stream = client.StreamJobs({
|
|
475
214
|
queue_name: 'emails',
|
|
476
215
|
worker_id: 'worker-1',
|
|
477
|
-
lease_duration_secs: 300
|
|
216
|
+
lease_duration_secs: 300,
|
|
478
217
|
}, metadata);
|
|
479
218
|
|
|
480
219
|
stream.on('data', (job) => {
|
|
@@ -482,72 +221,7 @@ stream.on('data', (job) => {
|
|
|
482
221
|
});
|
|
483
222
|
```
|
|
484
223
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
| Use Case | Recommended |
|
|
488
|
-
|----------|-------------|
|
|
489
|
-
| Web/mobile apps | REST API |
|
|
490
|
-
| Dashboard/admin | REST API |
|
|
491
|
-
| High-throughput workers | gRPC |
|
|
492
|
-
| Streaming job delivery | gRPC |
|
|
493
|
-
|
|
494
|
-
See [spooled-backend/proto/spooled.proto](https://github.com/spooled-cloud/spooled-backend/blob/main/proto/spooled.proto) for the full protocol definition.
|
|
495
|
-
## Error Handling
|
|
496
|
-
|
|
497
|
-
All errors extend `SpooledError` with specific subclasses:
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
import {
|
|
501
|
-
SpooledError,
|
|
502
|
-
AuthenticationError, // 401
|
|
503
|
-
AuthorizationError, // 403
|
|
504
|
-
NotFoundError, // 404
|
|
505
|
-
ValidationError, // 400
|
|
506
|
-
ConflictError, // 409
|
|
507
|
-
RateLimitError, // 429
|
|
508
|
-
ServerError, // 5xx
|
|
509
|
-
NetworkError, // Network failure
|
|
510
|
-
TimeoutError, // Request timeout
|
|
511
|
-
CircuitBreakerOpenError,
|
|
512
|
-
isSpooledError
|
|
513
|
-
} from '@spooled/sdk';
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
await client.jobs.get('non-existent');
|
|
517
|
-
} catch (error) {
|
|
518
|
-
if (error instanceof NotFoundError) {
|
|
519
|
-
console.log('Job not found');
|
|
520
|
-
} else if (error instanceof RateLimitError) {
|
|
521
|
-
console.log(`Retry after ${error.getRetryAfter()} seconds`);
|
|
522
|
-
} else if (isSpooledError(error)) {
|
|
523
|
-
console.log(`API error: ${error.code} - ${error.message}`);
|
|
524
|
-
console.log('Retryable:', error.isRetryable());
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
## Case Conversion
|
|
530
|
-
|
|
531
|
-
The SDK automatically converts between camelCase (SDK) and snake_case (API):
|
|
532
|
-
|
|
533
|
-
```typescript
|
|
534
|
-
// You write camelCase
|
|
535
|
-
await client.jobs.create({
|
|
536
|
-
queueName: 'my-queue',
|
|
537
|
-
maxRetries: 3,
|
|
538
|
-
timeoutSeconds: 300
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
// API receives snake_case
|
|
542
|
-
// { queue_name: 'my-queue', max_retries: 3, timeout_seconds: 300 }
|
|
543
|
-
|
|
544
|
-
// Response is converted back
|
|
545
|
-
const job = await client.jobs.get(id);
|
|
546
|
-
console.log(job.queueName); // camelCase
|
|
547
|
-
console.log(job.maxRetries); // camelCase
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
**Important:** User-defined JSON blobs (`payload`, `result`, `metadata`, `tags`) are preserved as-is and not converted.
|
|
224
|
+
See [gRPC Guide](docs/grpc.md) for complete documentation.
|
|
551
225
|
|
|
552
226
|
## TypeScript Support
|
|
553
227
|
|
|
@@ -560,20 +234,10 @@ import type {
|
|
|
560
234
|
Queue, QueueStats,
|
|
561
235
|
Schedule, CreateScheduleParams,
|
|
562
236
|
Workflow, CreateWorkflowParams,
|
|
563
|
-
Worker, WorkerStatus
|
|
237
|
+
Worker, WorkerStatus,
|
|
564
238
|
} from '@spooled/sdk';
|
|
565
239
|
```
|
|
566
240
|
|
|
567
|
-
## Examples
|
|
568
|
-
|
|
569
|
-
See the [`examples/`](examples/) directory for runnable examples:
|
|
570
|
-
|
|
571
|
-
- [`quick-start.ts`](examples/quick-start.ts) - Basic usage
|
|
572
|
-
- [`worker.ts`](examples/worker.ts) - Processing jobs
|
|
573
|
-
- [`realtime.ts`](examples/realtime.ts) - Event streaming
|
|
574
|
-
- [`schedules.ts`](examples/schedules.ts) - Cron schedules
|
|
575
|
-
- [`error-handling.ts`](examples/error-handling.ts) - Error handling patterns
|
|
576
|
-
|
|
577
241
|
## License
|
|
578
242
|
|
|
579
243
|
Apache-2.0 - see [LICENSE](LICENSE) for details.
|