@spooled/sdk 1.0.39 → 1.1.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
@@ -34,23 +34,23 @@ npm install @spooled/sdk
34
34
  ## Quick Start
35
35
 
36
36
  ```typescript
37
- import { SpooledClient } from '@spooled/sdk';
37
+ import { SpooledClient } from "@spooled/sdk";
38
38
 
39
39
  const client = new SpooledClient({
40
- apiKey: 'sp_live_your_api_key',
40
+ apiKey: "sp_live_your_api_key",
41
41
  // For self-hosted: baseUrl: 'https://your-spooled-server.com'
42
42
  });
43
43
 
44
44
  // Create a job
45
45
  const { id } = await client.jobs.create({
46
- queueName: 'email-notifications',
46
+ queueName: "email-notifications",
47
47
  payload: {
48
- to: 'user@example.com',
49
- subject: 'Welcome!',
50
- body: 'Thanks for signing up.'
48
+ to: "user@example.com",
49
+ subject: "Welcome!",
50
+ body: "Thanks for signing up.",
51
51
  },
52
52
  priority: 5,
53
- maxRetries: 3
53
+ maxRetries: 3,
54
54
  });
55
55
 
56
56
  console.log(`Created job: ${id}`);
@@ -62,28 +62,28 @@ console.log(`Status: ${job.status}`);
62
62
 
63
63
  ## Documentation
64
64
 
65
- | Guide | Description |
66
- |-------|-------------|
67
- | [Getting Started](docs/getting-started.md) | Installation, setup, and first job |
68
- | [Configuration](docs/configuration.md) | All configuration options |
69
- | [Workers](docs/workers.md) | Job processing with SpooledWorker |
70
- | [Workflows](docs/workflows.md) | Building DAGs with job dependencies |
71
- | [gRPC](docs/grpc.md) | High-performance streaming |
72
- | [Resources](docs/resources.md) | Complete API reference |
65
+ | Guide | Description |
66
+ | ------------------------------------------ | ----------------------------------- |
67
+ | [Getting Started](docs/getting-started.md) | Installation, setup, and first job |
68
+ | [Configuration](docs/configuration.md) | All configuration options |
69
+ | [Workers](docs/workers.md) | Job processing with SpooledWorker |
70
+ | [Workflows](docs/workflows.md) | Building DAGs with job dependencies |
71
+ | [gRPC](docs/grpc.md) | High-performance streaming |
72
+ | [Resources](docs/resources.md) | Complete API reference |
73
73
 
74
74
  ## Examples
75
75
 
76
76
  See the [`examples/`](examples/) directory for runnable code:
77
77
 
78
- | Example | Description |
79
- |---------|-------------|
80
- | [`quick-start.ts`](examples/quick-start.ts) | Basic SDK usage |
81
- | [`worker.ts`](examples/worker.ts) | Processing jobs with SpooledWorker |
82
- | [`workflow-dag.ts`](examples/workflow-dag.ts) | Complex workflows with dependencies |
83
- | [`grpc-streaming.ts`](examples/grpc-streaming.ts) | High-performance gRPC streaming |
84
- | [`realtime.ts`](examples/realtime.ts) | Real-time event streaming |
85
- | [`schedules.ts`](examples/schedules.ts) | Cron schedules |
86
- | [`error-handling.ts`](examples/error-handling.ts) | Error handling patterns |
78
+ | Example | Description |
79
+ | ------------------------------------------------- | ----------------------------------- |
80
+ | [`quick-start.ts`](examples/quick-start.ts) | Basic SDK usage |
81
+ | [`worker.ts`](examples/worker.ts) | Processing jobs with SpooledWorker |
82
+ | [`workflow-dag.ts`](examples/workflow-dag.ts) | Complex workflows with dependencies |
83
+ | [`grpc-streaming.ts`](examples/grpc-streaming.ts) | High-performance gRPC streaming |
84
+ | [`realtime.ts`](examples/realtime.ts) | Real-time event streaming |
85
+ | [`schedules.ts`](examples/schedules.ts) | Cron schedules |
86
+ | [`error-handling.ts`](examples/error-handling.ts) | Error handling patterns |
87
87
 
88
88
  ## Real-world examples (beginner friendly)
89
89
 
@@ -99,13 +99,13 @@ Jobs are units of work with payloads, priorities, and retry policies:
99
99
 
100
100
  ```typescript
101
101
  const { id } = await client.jobs.create({
102
- queueName: 'my-queue',
103
- payload: { data: 'value' },
104
- priority: 5, // -100 to 100
102
+ queueName: "my-queue",
103
+ payload: { data: "value" },
104
+ priority: 5, // -100 to 100
105
105
  maxRetries: 3,
106
106
  timeoutSeconds: 300,
107
107
  scheduledAt: new Date(Date.now() + 60000),
108
- idempotencyKey: 'unique-key',
108
+ idempotencyKey: "unique-key",
109
109
  });
110
110
  ```
111
111
 
@@ -114,12 +114,13 @@ const { id } = await client.jobs.create({
114
114
  Process jobs with the built-in worker runtime:
115
115
 
116
116
  ```typescript
117
- import { SpooledClient, SpooledWorker } from '@spooled/sdk';
117
+ import { SpooledClient, SpooledWorker } from "@spooled/sdk";
118
118
 
119
- const client = new SpooledClient({ apiKey: 'sp_live_...' });
119
+ const client = new SpooledClient({ apiKey: "sp_live_..." });
120
120
 
121
121
  const worker = new SpooledWorker(client, {
122
- queueName: 'my-queue',
122
+ queueName: "my-queue",
123
+ workerId: "my-queue-worker-01", // stable id — see below
123
124
  concurrency: 10,
124
125
  });
125
126
 
@@ -132,20 +133,35 @@ worker.process(async (ctx) => {
132
133
  await worker.start();
133
134
 
134
135
  // Graceful shutdown
135
- process.on('SIGTERM', () => worker.stop());
136
+ process.on("SIGTERM", () => worker.stop());
136
137
  ```
137
138
 
139
+ Give any worker that restarts a stable `workerId` (1-128 chars, `[A-Za-z0-9._-]`). Registration then
140
+ upserts a single row instead of minting a new UUID each start, which otherwise leaves the previous
141
+ registration counting against your plan's worker limit until the stale-worker reaper clears it
142
+ (~2 minutes).
143
+
138
144
  ### Workflows (DAGs)
139
145
 
140
146
  Orchestrate multiple jobs with dependencies:
141
147
 
142
148
  ```typescript
143
149
  const workflow = await client.workflows.create({
144
- name: 'ETL Pipeline',
150
+ name: "ETL Pipeline",
145
151
  jobs: [
146
- { key: 'extract', queueName: 'etl', payload: { step: 'extract' } },
147
- { key: 'transform', queueName: 'etl', payload: { step: 'transform' }, dependsOn: ['extract'] },
148
- { key: 'load', queueName: 'etl', payload: { step: 'load' }, dependsOn: ['transform'] },
152
+ { key: "extract", queueName: "etl", payload: { step: "extract" } },
153
+ {
154
+ key: "transform",
155
+ queueName: "etl",
156
+ payload: { step: "transform" },
157
+ dependsOn: ["extract"],
158
+ },
159
+ {
160
+ key: "load",
161
+ queueName: "etl",
162
+ payload: { step: "load" },
163
+ dependsOn: ["transform"],
164
+ },
149
165
  ],
150
166
  });
151
167
  ```
@@ -156,11 +172,11 @@ Run jobs on a cron schedule:
156
172
 
157
173
  ```typescript
158
174
  const schedule = await client.schedules.create({
159
- name: 'Daily Report',
160
- cronExpression: '0 9 * * *', // 9 AM daily; 5-field (min hour dom mon dow) or 6-field (leading seconds) both work
161
- timezone: 'America/New_York',
162
- queueName: 'reports',
163
- payloadTemplate: { type: 'daily' },
175
+ name: "Daily Report",
176
+ cronExpression: "0 9 * * *", // 9 AM daily; 5-field (min hour dom mon dow) or 6-field (leading seconds) both work
177
+ timezone: "America/New_York",
178
+ queueName: "reports",
179
+ payloadTemplate: { type: "daily" },
164
180
  });
165
181
  ```
166
182
 
@@ -171,12 +187,12 @@ Subscribe to real-time job events:
171
187
  ```typescript
172
188
  const realtime = await client.realtime();
173
189
 
174
- realtime.on('job.completed', (data) => {
190
+ realtime.on("job.completed", (data) => {
175
191
  console.log(`Job completed: ${data.jobId}`);
176
192
  });
177
193
 
178
194
  await realtime.connect();
179
- await realtime.subscribe({ queueName: 'my-queue' });
195
+ await realtime.subscribe({ queueName: "my-queue" });
180
196
  ```
181
197
 
182
198
  ### Organization Management
@@ -186,14 +202,16 @@ Manage your organization and track usage:
186
202
  ```typescript
187
203
  // Get current usage and limits
188
204
  const usage = await client.organizations.getUsage();
189
- console.log(`Active jobs: ${usage.active_jobs.current}/${usage.active_jobs.limit}`);
205
+ console.log(
206
+ `Active jobs: ${usage.active_jobs.current}/${usage.active_jobs.limit}`,
207
+ );
190
208
  console.log(`Plan: ${usage.plan.tier}`);
191
209
 
192
210
  // Generate a unique slug for a new organization
193
- const { slug } = await client.organizations.generateSlug('My Company');
211
+ const { slug } = await client.organizations.generateSlug("My Company");
194
212
 
195
213
  // Check if a slug is available
196
- const { available } = await client.organizations.checkSlug('my-company');
214
+ const { available } = await client.organizations.checkSlug("my-company");
197
215
  ```
198
216
 
199
217
  ### Webhooks
@@ -203,19 +221,32 @@ Configure outgoing webhooks for job events:
203
221
  ```typescript
204
222
  // Create webhook
205
223
  const webhook = await client.webhooks.create({
206
- url: 'https://your-app.com/webhooks/spooled',
207
- events: ['job.completed', 'job.failed'],
208
- queueName: 'my-queue',
209
- secret: 'webhook_secret_key'
224
+ name: "App notifications",
225
+ url: "https://your-app.com/webhooks/spooled",
226
+ events: ["job.completed", "job.failed"],
227
+ secret: "webhook_secret_key",
210
228
  });
211
229
 
212
230
  // Retry a failed delivery
213
231
  await client.webhooks.retryDelivery(webhookId, deliveryId);
214
232
 
215
- // Get delivery history
233
+ // Get delivery history (retention-bounded: free 1 day, starter 7, pro 30,
234
+ // enterprise 90 — and only the newest 100 deliveries per webhook)
216
235
  const deliveries = await client.webhooks.getDeliveries(webhookId);
217
236
  ```
218
237
 
238
+ After 20 consecutive failed deliveries a webhook is disabled automatically (`enabled: false`,
239
+ `lastStatus: "auto_disabled"`) and stops receiving events until you turn it back on:
240
+
241
+ ```typescript
242
+ await client.webhooks.update(webhookId, { enabled: true });
243
+ ```
244
+
245
+ Updating changes only the fields you send. `secret` is three-state — omit it to keep the current
246
+ secret, pass a string to replace it, or pass `null` to clear it. Clearing is destructive: deliveries
247
+ then go out unsigned, with no `X-Spooled-Signature` header. Do not serialise unchanged fields as
248
+ `null`.
249
+
219
250
  ### Dead Letter Queue (DLQ)
220
251
 
221
252
  Manage jobs that have exhausted all retries:
@@ -226,19 +257,19 @@ const dlqJobs = await client.jobs.dlq.list({ limit: 100 });
226
257
 
227
258
  // Retry specific jobs from DLQ
228
259
  await client.jobs.dlq.retry({
229
- jobIds: ['job-1', 'job-2'],
260
+ jobIds: ["job-1", "job-2"],
230
261
  });
231
262
 
232
263
  // Retry jobs by queue
233
264
  await client.jobs.dlq.retry({
234
- queueName: 'my-queue',
235
- limit: 50
265
+ queueName: "my-queue",
266
+ limit: 50,
236
267
  });
237
268
 
238
269
  // Purge DLQ (requires confirmation)
239
270
  await client.jobs.dlq.purge({
240
- queueName: 'my-queue',
241
- confirm: true
271
+ queueName: "my-queue",
272
+ confirm: true,
242
273
  });
243
274
  ```
244
275
 
@@ -249,9 +280,9 @@ Manage API keys programmatically:
249
280
  ```typescript
250
281
  // Create a new API key
251
282
  const apiKey = await client.apiKeys.create({
252
- name: 'Production Worker',
253
- queues: ['emails', 'notifications'],
254
- rateLimit: 1000
283
+ name: "Production Worker",
284
+ queues: ["emails", "notifications"],
285
+ rateLimit: 1000,
255
286
  });
256
287
 
257
288
  console.log(`Save this key: ${apiKey.key}`); // Only shown once!
@@ -273,7 +304,7 @@ const status = await client.billing.getStatus();
273
304
 
274
305
  // Create customer portal session
275
306
  const { url } = await client.billing.createPortal({
276
- returnUrl: 'https://your-app.com/settings'
307
+ returnUrl: "https://your-app.com/settings",
277
308
  });
278
309
 
279
310
  // Redirect user to: url
@@ -285,15 +316,15 @@ Email-based passwordless authentication:
285
316
 
286
317
  ```typescript
287
318
  // Start email login flow
288
- await client.auth.startEmailLogin('user@example.com');
319
+ await client.auth.startEmailLogin("user@example.com");
289
320
  // User receives email with login link
290
321
 
291
322
  // Check if email exists
292
- const { exists } = await client.auth.checkEmail('user@example.com');
323
+ const { exists } = await client.auth.checkEmail("user@example.com");
293
324
 
294
325
  // Exchange API key for JWT
295
326
  const { accessToken, refreshToken } = await client.auth.login({
296
- apiKey: 'sp_live_...'
327
+ apiKey: "sp_live_...",
297
328
  });
298
329
 
299
330
  // Use JWT for subsequent requests
@@ -304,21 +335,22 @@ const jwtClient = new SpooledClient({ accessToken });
304
335
 
305
336
  All operations automatically enforce tier-based limits:
306
337
 
307
- | Tier | Active Jobs | Daily Jobs | Queues | Workers | Webhooks |
308
- |------|-------------|------------|--------|---------|----------|
309
- | **Free** | 10 | 1,000 | 5 | 3 | 2 |
310
- | **Starter** | 100 | 100,000 | 25 | 25 | 10 |
311
- | **Enterprise** | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
338
+ | Tier | Active Jobs | Daily Jobs | Queues | Workers | Webhooks |
339
+ | -------------- | ----------- | ---------- | --------- | --------- | --------- |
340
+ | **Free** | 10 | 1,000 | 5 | 3 | 2 |
341
+ | **Starter** | 100 | 100,000 | 25 | 25 | 10 |
342
+ | **Enterprise** | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
312
343
 
313
344
  **Limits are enforced on:**
345
+
314
346
  - ✅ HTTP job creation (`POST /jobs`, `POST /jobs/bulk`)
315
347
  - ✅ gRPC job enqueue
316
348
  - ✅ Workflow creation (counts all jobs in workflow)
317
349
  - ✅ Schedule triggers
318
350
  - ✅ DLQ retry operations
319
- - ✅ Worker registration
351
+ - ✅ Worker registration (re-registering your own stable `workerId` is not charged again)
320
352
  - ✅ Queue creation
321
- - ✅ Webhook creation
353
+ - ✅ Webhook creation, and re-enabling a disabled webhook
322
354
 
323
355
  When a plan quota or limit is exceeded, you'll receive an `HTTP 429` response with
324
356
  `code: "QUOTA_EXCEEDED"`. The SDK reliably preserves the HTTP status, error code, message,
@@ -327,16 +359,18 @@ metadata under a JSON `details` field; quota fields sent at the top level of the
327
359
  (such as `resource`, `current`, `limit`, and `plan`) are not currently exposed by the SDK.
328
360
 
329
361
  ```typescript
330
- import { RateLimitError } from '@spooled/sdk';
362
+ import { RateLimitError } from "@spooled/sdk";
331
363
 
332
364
  try {
333
- await client.jobs.create({ /* ... */ });
365
+ await client.jobs.create({
366
+ /* ... */
367
+ });
334
368
  } catch (error) {
335
- if (error instanceof RateLimitError && error.code === 'QUOTA_EXCEEDED') {
369
+ if (error instanceof RateLimitError && error.code === "QUOTA_EXCEEDED") {
336
370
  console.log(`Plan limit: ${error.message}`);
337
- console.log(`Request ID: ${error.requestId ?? 'not provided'}`);
371
+ console.log(`Request ID: ${error.requestId ?? "not provided"}`);
338
372
  // Present only if this server response included a nested `details` object.
339
- if (error.details) console.log('Details:', error.details);
373
+ if (error.details) console.log("Details:", error.details);
340
374
  }
341
375
  }
342
376
  ```
@@ -354,13 +388,13 @@ import {
354
388
  ValidationError,
355
389
  AuthorizationError,
356
390
  isSpooledError,
357
- } from '@spooled/sdk';
391
+ } from "@spooled/sdk";
358
392
 
359
393
  try {
360
- await client.jobs.get('non-existent');
394
+ await client.jobs.get("non-existent");
361
395
  } catch (error) {
362
396
  if (error instanceof NotFoundError) {
363
- console.log('Job not found');
397
+ console.log("Job not found");
364
398
  } else if (error instanceof AuthorizationError) {
365
399
  console.log(`Forbidden: ${error.message}`);
366
400
  } else if (error instanceof RateLimitError) {
@@ -377,37 +411,37 @@ try {
377
411
  For high-throughput workers, use the native gRPC API with the included gRPC client:
378
412
 
379
413
  ```typescript
380
- import { SpooledGrpcClient } from '@spooled/sdk';
414
+ import { SpooledGrpcClient } from "@spooled/sdk";
381
415
 
382
416
  // Connect to gRPC server (Optimized for Cloudflare Tunnel)
383
417
  // useTls: true if connecting to grpc.spooled.cloud:443 (Cloudflare terminates TLS)
384
418
  // useTls: false if connecting to localhost or direct backend
385
419
  const grpcClient = new SpooledGrpcClient({
386
- address: 'grpc.spooled.cloud:443',
387
- apiKey: 'sp_live_your_key',
388
- useTls: true
420
+ address: "grpc.spooled.cloud:443",
421
+ apiKey: "sp_live_your_key",
422
+ useTls: true,
389
423
  });
390
424
 
391
425
  // Register worker
392
426
  const { workerId } = await grpcClient.workers.register({
393
- queueName: 'emails',
394
- hostname: 'worker-1',
395
- maxConcurrency: 10
427
+ queueName: "emails",
428
+ hostname: "worker-1",
429
+ maxConcurrency: 10,
396
430
  });
397
431
 
398
432
  // Enqueue job (enforces plan limits automatically)
399
433
  const { jobId } = await grpcClient.queue.enqueue({
400
- queueName: 'emails',
401
- payload: { to: 'user@example.com' },
402
- priority: 5
434
+ queueName: "emails",
435
+ payload: { to: "user@example.com" },
436
+ priority: 5,
403
437
  });
404
438
 
405
439
  // Dequeue jobs in batch
406
440
  const { jobs } = await grpcClient.queue.dequeue({
407
- queueName: 'emails',
441
+ queueName: "emails",
408
442
  workerId,
409
443
  batchSize: 10,
410
- leaseDurationSecs: 300
444
+ leaseDurationSecs: 300,
411
445
  });
412
446
 
413
447
  // Process and complete. Echo leaseId when present so current servers can fence stale executions; legacy servers may omit it and then use worker-ID-only fencing.
@@ -417,12 +451,13 @@ for (const job of jobs) {
417
451
  jobId: job.id,
418
452
  workerId,
419
453
  leaseId: job.leaseId ?? undefined,
420
- result: { success: true }
454
+ result: { success: true },
421
455
  });
422
456
  }
423
457
  ```
424
458
 
425
459
  **gRPC Features:**
460
+
426
461
  - **~28x faster** than HTTP (with Redis cache: ~50ms vs 1400ms)
427
462
  - **Automatic plan limit enforcement** on enqueue operations
428
463
  - **Batch operations** for higher throughput
@@ -438,12 +473,18 @@ Full TypeScript support with comprehensive type definitions:
438
473
  ```typescript
439
474
  import type {
440
475
  SpooledClientConfig,
441
- Job, JobStatus, CreateJobParams,
442
- Queue, QueueStats,
443
- Schedule, CreateScheduleParams,
444
- Workflow, CreateWorkflowParams,
445
- Worker, WorkerStatus,
446
- } from '@spooled/sdk';
476
+ Job,
477
+ JobStatus,
478
+ CreateJobParams,
479
+ Queue,
480
+ QueueStats,
481
+ Schedule,
482
+ CreateScheduleParams,
483
+ Workflow,
484
+ CreateWorkflowParams,
485
+ Worker,
486
+ WorkerStatus,
487
+ } from "@spooled/sdk";
447
488
  ```
448
489
 
449
490
  ## Development and releases