@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 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
- - 🚀 **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
- - 🔒 **Automatic case conversion** between camelCase (SDK) and snake_case (API)
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
- ## Configuration
55
+ ## Documentation
54
56
 
55
- ```typescript
56
- const client = new SpooledClient({
57
- // Authentication (required - one of these)
58
- apiKey: 'sk_live_...', // API key
59
- accessToken: 'jwt_token', // Or JWT token
60
-
61
- // API settings
62
- baseUrl: 'https://api.spooled.cloud', // Default
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
- ## Resources
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
- // Create a job
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), // Delay execution
104
- idempotencyKey: 'unique-key', // Prevent duplicates
105
- tags: { env: 'production' }
93
+ scheduledAt: new Date(Date.now() + 60000),
94
+ idempotencyKey: 'unique-key',
106
95
  });
96
+ ```
107
97
 
108
- // List jobs
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
- // Retry a failed job
123
- await client.jobs.retry(id);
100
+ Process jobs with the built-in worker runtime:
124
101
 
125
- // Boost priority
126
- await client.jobs.boostPriority(id, 10);
102
+ ```typescript
103
+ import { SpooledClient, SpooledWorker } from '@spooled/sdk';
127
104
 
128
- // Get statistics
129
- const stats = await client.jobs.getStats();
105
+ const client = new SpooledClient({ apiKey: 'sk_live_...' });
130
106
 
131
- // Bulk enqueue
132
- const result = await client.jobs.bulkEnqueue({
107
+ const worker = new SpooledWorker(client, {
133
108
  queueName: 'my-queue',
134
- jobs: [
135
- { payload: { n: 1 } },
136
- { payload: { n: 2 } },
137
- { payload: { n: 3 } }
138
- ]
109
+ concurrency: 10,
139
110
  });
140
111
 
141
- // Batch status check
142
- const statuses = await client.jobs.batchStatus(['id1', 'id2', 'id3']);
143
-
144
- // Dead-letter queue operations
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
- // Get queue stats
167
- const stats = await client.queues.getStats('my-queue');
118
+ await worker.start();
168
119
 
169
- // Pause/resume
170
- await client.queues.pause('my-queue', 'Maintenance');
171
- await client.queues.resume('my-queue');
120
+ // Graceful shutdown
121
+ process.on('SIGTERM', () => worker.stop());
172
122
  ```
173
123
 
174
- ### Schedules
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
- // Manual trigger
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
- ### Dashboard
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
- Process jobs with the built-in worker runtime:
141
+ Run jobs on a cron schedule:
352
142
 
353
143
  ```typescript
354
- import { SpooledClient, SpooledWorker } from '@spooled/sdk';
355
-
356
- const client = new SpooledClient({ apiKey: 'sk_live_...' });
357
-
358
- const worker = new SpooledWorker(client, {
359
- queueName: 'my-queue',
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
- ## Realtime Events
153
+ ### Realtime Events
395
154
 
396
- Subscribe to real-time events via WebSocket:
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} in ${data.durationMs}ms`);
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
- ### Choosing WebSocket vs SSE (server load)
168
+ ## Error Handling
433
169
 
434
- - **Default**: WebSocket (`type: 'websocket'`). This is usually **lowest load** on the Spooled backend because it can push events (pub/sub) without polling.
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
- const realtime = await client.realtime({ type: 'sse' });
439
- await realtime.connect();
440
- await realtime.subscribe({ queueName: 'my-queue' });
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
- Spooled provides a **real gRPC API** on port `50051` using HTTP/2 + Protobuf for high-performance worker communication.
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
- 'api.spooled.cloud:50051',
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
- client.Enqueue({
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
- **When to use gRPC vs REST:**
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.