@spooled/sdk 1.0.39 → 1.0.40
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 +116 -94
- package/dist/{index-Lgo8JxCV.d.cts → index-DA_znF4h.d.cts} +80 -66
- package/dist/{index-Lgo8JxCV.d.ts → index-DA_znF4h.d.ts} +80 -66
- package/dist/index.cjs +363 -90
- 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 +363 -90
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +14 -5
- 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 +14 -5
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -1
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
|
|
37
|
+
import { SpooledClient } from "@spooled/sdk";
|
|
38
38
|
|
|
39
39
|
const client = new SpooledClient({
|
|
40
|
-
apiKey:
|
|
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:
|
|
46
|
+
queueName: "email-notifications",
|
|
47
47
|
payload: {
|
|
48
|
-
to:
|
|
49
|
-
subject:
|
|
50
|
-
body:
|
|
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
|
|
66
|
-
|
|
67
|
-
| [Getting Started](docs/getting-started.md) | Installation, setup, and first job
|
|
68
|
-
| [Configuration](docs/configuration.md)
|
|
69
|
-
| [Workers](docs/workers.md)
|
|
70
|
-
| [Workflows](docs/workflows.md)
|
|
71
|
-
| [gRPC](docs/grpc.md)
|
|
72
|
-
| [Resources](docs/resources.md)
|
|
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
|
|
79
|
-
|
|
80
|
-
| [`quick-start.ts`](examples/quick-start.ts)
|
|
81
|
-
| [`worker.ts`](examples/worker.ts)
|
|
82
|
-
| [`workflow-dag.ts`](examples/workflow-dag.ts)
|
|
83
|
-
| [`grpc-streaming.ts`](examples/grpc-streaming.ts) | High-performance gRPC streaming
|
|
84
|
-
| [`realtime.ts`](examples/realtime.ts)
|
|
85
|
-
| [`schedules.ts`](examples/schedules.ts)
|
|
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:
|
|
103
|
-
payload: { data:
|
|
104
|
-
priority: 5,
|
|
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:
|
|
108
|
+
idempotencyKey: "unique-key",
|
|
109
109
|
});
|
|
110
110
|
```
|
|
111
111
|
|
|
@@ -114,12 +114,12 @@ 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
|
|
117
|
+
import { SpooledClient, SpooledWorker } from "@spooled/sdk";
|
|
118
118
|
|
|
119
|
-
const client = new SpooledClient({ apiKey:
|
|
119
|
+
const client = new SpooledClient({ apiKey: "sp_live_..." });
|
|
120
120
|
|
|
121
121
|
const worker = new SpooledWorker(client, {
|
|
122
|
-
queueName:
|
|
122
|
+
queueName: "my-queue",
|
|
123
123
|
concurrency: 10,
|
|
124
124
|
});
|
|
125
125
|
|
|
@@ -132,7 +132,7 @@ worker.process(async (ctx) => {
|
|
|
132
132
|
await worker.start();
|
|
133
133
|
|
|
134
134
|
// Graceful shutdown
|
|
135
|
-
process.on(
|
|
135
|
+
process.on("SIGTERM", () => worker.stop());
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
### Workflows (DAGs)
|
|
@@ -141,11 +141,21 @@ Orchestrate multiple jobs with dependencies:
|
|
|
141
141
|
|
|
142
142
|
```typescript
|
|
143
143
|
const workflow = await client.workflows.create({
|
|
144
|
-
name:
|
|
144
|
+
name: "ETL Pipeline",
|
|
145
145
|
jobs: [
|
|
146
|
-
{ key:
|
|
147
|
-
{
|
|
148
|
-
|
|
146
|
+
{ key: "extract", queueName: "etl", payload: { step: "extract" } },
|
|
147
|
+
{
|
|
148
|
+
key: "transform",
|
|
149
|
+
queueName: "etl",
|
|
150
|
+
payload: { step: "transform" },
|
|
151
|
+
dependsOn: ["extract"],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: "load",
|
|
155
|
+
queueName: "etl",
|
|
156
|
+
payload: { step: "load" },
|
|
157
|
+
dependsOn: ["transform"],
|
|
158
|
+
},
|
|
149
159
|
],
|
|
150
160
|
});
|
|
151
161
|
```
|
|
@@ -156,11 +166,11 @@ Run jobs on a cron schedule:
|
|
|
156
166
|
|
|
157
167
|
```typescript
|
|
158
168
|
const schedule = await client.schedules.create({
|
|
159
|
-
name:
|
|
160
|
-
cronExpression:
|
|
161
|
-
timezone:
|
|
162
|
-
queueName:
|
|
163
|
-
payloadTemplate: { type:
|
|
169
|
+
name: "Daily Report",
|
|
170
|
+
cronExpression: "0 9 * * *", // 9 AM daily; 5-field (min hour dom mon dow) or 6-field (leading seconds) both work
|
|
171
|
+
timezone: "America/New_York",
|
|
172
|
+
queueName: "reports",
|
|
173
|
+
payloadTemplate: { type: "daily" },
|
|
164
174
|
});
|
|
165
175
|
```
|
|
166
176
|
|
|
@@ -171,12 +181,12 @@ Subscribe to real-time job events:
|
|
|
171
181
|
```typescript
|
|
172
182
|
const realtime = await client.realtime();
|
|
173
183
|
|
|
174
|
-
realtime.on(
|
|
184
|
+
realtime.on("job.completed", (data) => {
|
|
175
185
|
console.log(`Job completed: ${data.jobId}`);
|
|
176
186
|
});
|
|
177
187
|
|
|
178
188
|
await realtime.connect();
|
|
179
|
-
await realtime.subscribe({ queueName:
|
|
189
|
+
await realtime.subscribe({ queueName: "my-queue" });
|
|
180
190
|
```
|
|
181
191
|
|
|
182
192
|
### Organization Management
|
|
@@ -186,14 +196,16 @@ Manage your organization and track usage:
|
|
|
186
196
|
```typescript
|
|
187
197
|
// Get current usage and limits
|
|
188
198
|
const usage = await client.organizations.getUsage();
|
|
189
|
-
console.log(
|
|
199
|
+
console.log(
|
|
200
|
+
`Active jobs: ${usage.active_jobs.current}/${usage.active_jobs.limit}`,
|
|
201
|
+
);
|
|
190
202
|
console.log(`Plan: ${usage.plan.tier}`);
|
|
191
203
|
|
|
192
204
|
// Generate a unique slug for a new organization
|
|
193
|
-
const { slug } = await client.organizations.generateSlug(
|
|
205
|
+
const { slug } = await client.organizations.generateSlug("My Company");
|
|
194
206
|
|
|
195
207
|
// Check if a slug is available
|
|
196
|
-
const { available } = await client.organizations.checkSlug(
|
|
208
|
+
const { available } = await client.organizations.checkSlug("my-company");
|
|
197
209
|
```
|
|
198
210
|
|
|
199
211
|
### Webhooks
|
|
@@ -203,10 +215,10 @@ Configure outgoing webhooks for job events:
|
|
|
203
215
|
```typescript
|
|
204
216
|
// Create webhook
|
|
205
217
|
const webhook = await client.webhooks.create({
|
|
206
|
-
url:
|
|
207
|
-
events: [
|
|
208
|
-
queueName:
|
|
209
|
-
secret:
|
|
218
|
+
url: "https://your-app.com/webhooks/spooled",
|
|
219
|
+
events: ["job.completed", "job.failed"],
|
|
220
|
+
queueName: "my-queue",
|
|
221
|
+
secret: "webhook_secret_key",
|
|
210
222
|
});
|
|
211
223
|
|
|
212
224
|
// Retry a failed delivery
|
|
@@ -226,19 +238,19 @@ const dlqJobs = await client.jobs.dlq.list({ limit: 100 });
|
|
|
226
238
|
|
|
227
239
|
// Retry specific jobs from DLQ
|
|
228
240
|
await client.jobs.dlq.retry({
|
|
229
|
-
jobIds: [
|
|
241
|
+
jobIds: ["job-1", "job-2"],
|
|
230
242
|
});
|
|
231
243
|
|
|
232
244
|
// Retry jobs by queue
|
|
233
245
|
await client.jobs.dlq.retry({
|
|
234
|
-
queueName:
|
|
235
|
-
limit: 50
|
|
246
|
+
queueName: "my-queue",
|
|
247
|
+
limit: 50,
|
|
236
248
|
});
|
|
237
249
|
|
|
238
250
|
// Purge DLQ (requires confirmation)
|
|
239
251
|
await client.jobs.dlq.purge({
|
|
240
|
-
queueName:
|
|
241
|
-
confirm: true
|
|
252
|
+
queueName: "my-queue",
|
|
253
|
+
confirm: true,
|
|
242
254
|
});
|
|
243
255
|
```
|
|
244
256
|
|
|
@@ -249,9 +261,9 @@ Manage API keys programmatically:
|
|
|
249
261
|
```typescript
|
|
250
262
|
// Create a new API key
|
|
251
263
|
const apiKey = await client.apiKeys.create({
|
|
252
|
-
name:
|
|
253
|
-
queues: [
|
|
254
|
-
rateLimit: 1000
|
|
264
|
+
name: "Production Worker",
|
|
265
|
+
queues: ["emails", "notifications"],
|
|
266
|
+
rateLimit: 1000,
|
|
255
267
|
});
|
|
256
268
|
|
|
257
269
|
console.log(`Save this key: ${apiKey.key}`); // Only shown once!
|
|
@@ -273,7 +285,7 @@ const status = await client.billing.getStatus();
|
|
|
273
285
|
|
|
274
286
|
// Create customer portal session
|
|
275
287
|
const { url } = await client.billing.createPortal({
|
|
276
|
-
returnUrl:
|
|
288
|
+
returnUrl: "https://your-app.com/settings",
|
|
277
289
|
});
|
|
278
290
|
|
|
279
291
|
// Redirect user to: url
|
|
@@ -285,15 +297,15 @@ Email-based passwordless authentication:
|
|
|
285
297
|
|
|
286
298
|
```typescript
|
|
287
299
|
// Start email login flow
|
|
288
|
-
await client.auth.startEmailLogin(
|
|
300
|
+
await client.auth.startEmailLogin("user@example.com");
|
|
289
301
|
// User receives email with login link
|
|
290
302
|
|
|
291
303
|
// Check if email exists
|
|
292
|
-
const { exists } = await client.auth.checkEmail(
|
|
304
|
+
const { exists } = await client.auth.checkEmail("user@example.com");
|
|
293
305
|
|
|
294
306
|
// Exchange API key for JWT
|
|
295
307
|
const { accessToken, refreshToken } = await client.auth.login({
|
|
296
|
-
apiKey:
|
|
308
|
+
apiKey: "sp_live_...",
|
|
297
309
|
});
|
|
298
310
|
|
|
299
311
|
// Use JWT for subsequent requests
|
|
@@ -304,13 +316,14 @@ const jwtClient = new SpooledClient({ accessToken });
|
|
|
304
316
|
|
|
305
317
|
All operations automatically enforce tier-based limits:
|
|
306
318
|
|
|
307
|
-
| Tier
|
|
308
|
-
|
|
309
|
-
| **Free**
|
|
310
|
-
| **Starter**
|
|
311
|
-
| **Enterprise** | Unlimited
|
|
319
|
+
| Tier | Active Jobs | Daily Jobs | Queues | Workers | Webhooks |
|
|
320
|
+
| -------------- | ----------- | ---------- | --------- | --------- | --------- |
|
|
321
|
+
| **Free** | 10 | 1,000 | 5 | 3 | 2 |
|
|
322
|
+
| **Starter** | 100 | 100,000 | 25 | 25 | 10 |
|
|
323
|
+
| **Enterprise** | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
|
|
312
324
|
|
|
313
325
|
**Limits are enforced on:**
|
|
326
|
+
|
|
314
327
|
- ✅ HTTP job creation (`POST /jobs`, `POST /jobs/bulk`)
|
|
315
328
|
- ✅ gRPC job enqueue
|
|
316
329
|
- ✅ Workflow creation (counts all jobs in workflow)
|
|
@@ -327,16 +340,18 @@ metadata under a JSON `details` field; quota fields sent at the top level of the
|
|
|
327
340
|
(such as `resource`, `current`, `limit`, and `plan`) are not currently exposed by the SDK.
|
|
328
341
|
|
|
329
342
|
```typescript
|
|
330
|
-
import { RateLimitError } from
|
|
343
|
+
import { RateLimitError } from "@spooled/sdk";
|
|
331
344
|
|
|
332
345
|
try {
|
|
333
|
-
await client.jobs.create({
|
|
346
|
+
await client.jobs.create({
|
|
347
|
+
/* ... */
|
|
348
|
+
});
|
|
334
349
|
} catch (error) {
|
|
335
|
-
if (error instanceof RateLimitError && error.code ===
|
|
350
|
+
if (error instanceof RateLimitError && error.code === "QUOTA_EXCEEDED") {
|
|
336
351
|
console.log(`Plan limit: ${error.message}`);
|
|
337
|
-
console.log(`Request ID: ${error.requestId ??
|
|
352
|
+
console.log(`Request ID: ${error.requestId ?? "not provided"}`);
|
|
338
353
|
// Present only if this server response included a nested `details` object.
|
|
339
|
-
if (error.details) console.log(
|
|
354
|
+
if (error.details) console.log("Details:", error.details);
|
|
340
355
|
}
|
|
341
356
|
}
|
|
342
357
|
```
|
|
@@ -354,13 +369,13 @@ import {
|
|
|
354
369
|
ValidationError,
|
|
355
370
|
AuthorizationError,
|
|
356
371
|
isSpooledError,
|
|
357
|
-
} from
|
|
372
|
+
} from "@spooled/sdk";
|
|
358
373
|
|
|
359
374
|
try {
|
|
360
|
-
await client.jobs.get(
|
|
375
|
+
await client.jobs.get("non-existent");
|
|
361
376
|
} catch (error) {
|
|
362
377
|
if (error instanceof NotFoundError) {
|
|
363
|
-
console.log(
|
|
378
|
+
console.log("Job not found");
|
|
364
379
|
} else if (error instanceof AuthorizationError) {
|
|
365
380
|
console.log(`Forbidden: ${error.message}`);
|
|
366
381
|
} else if (error instanceof RateLimitError) {
|
|
@@ -377,37 +392,37 @@ try {
|
|
|
377
392
|
For high-throughput workers, use the native gRPC API with the included gRPC client:
|
|
378
393
|
|
|
379
394
|
```typescript
|
|
380
|
-
import { SpooledGrpcClient } from
|
|
395
|
+
import { SpooledGrpcClient } from "@spooled/sdk";
|
|
381
396
|
|
|
382
397
|
// Connect to gRPC server (Optimized for Cloudflare Tunnel)
|
|
383
398
|
// useTls: true if connecting to grpc.spooled.cloud:443 (Cloudflare terminates TLS)
|
|
384
399
|
// useTls: false if connecting to localhost or direct backend
|
|
385
400
|
const grpcClient = new SpooledGrpcClient({
|
|
386
|
-
address:
|
|
387
|
-
apiKey:
|
|
388
|
-
useTls: true
|
|
401
|
+
address: "grpc.spooled.cloud:443",
|
|
402
|
+
apiKey: "sp_live_your_key",
|
|
403
|
+
useTls: true,
|
|
389
404
|
});
|
|
390
405
|
|
|
391
406
|
// Register worker
|
|
392
407
|
const { workerId } = await grpcClient.workers.register({
|
|
393
|
-
queueName:
|
|
394
|
-
hostname:
|
|
395
|
-
maxConcurrency: 10
|
|
408
|
+
queueName: "emails",
|
|
409
|
+
hostname: "worker-1",
|
|
410
|
+
maxConcurrency: 10,
|
|
396
411
|
});
|
|
397
412
|
|
|
398
413
|
// Enqueue job (enforces plan limits automatically)
|
|
399
414
|
const { jobId } = await grpcClient.queue.enqueue({
|
|
400
|
-
queueName:
|
|
401
|
-
payload: { to:
|
|
402
|
-
priority: 5
|
|
415
|
+
queueName: "emails",
|
|
416
|
+
payload: { to: "user@example.com" },
|
|
417
|
+
priority: 5,
|
|
403
418
|
});
|
|
404
419
|
|
|
405
420
|
// Dequeue jobs in batch
|
|
406
421
|
const { jobs } = await grpcClient.queue.dequeue({
|
|
407
|
-
queueName:
|
|
422
|
+
queueName: "emails",
|
|
408
423
|
workerId,
|
|
409
424
|
batchSize: 10,
|
|
410
|
-
leaseDurationSecs: 300
|
|
425
|
+
leaseDurationSecs: 300,
|
|
411
426
|
});
|
|
412
427
|
|
|
413
428
|
// 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 +432,13 @@ for (const job of jobs) {
|
|
|
417
432
|
jobId: job.id,
|
|
418
433
|
workerId,
|
|
419
434
|
leaseId: job.leaseId ?? undefined,
|
|
420
|
-
result: { success: true }
|
|
435
|
+
result: { success: true },
|
|
421
436
|
});
|
|
422
437
|
}
|
|
423
438
|
```
|
|
424
439
|
|
|
425
440
|
**gRPC Features:**
|
|
441
|
+
|
|
426
442
|
- **~28x faster** than HTTP (with Redis cache: ~50ms vs 1400ms)
|
|
427
443
|
- **Automatic plan limit enforcement** on enqueue operations
|
|
428
444
|
- **Batch operations** for higher throughput
|
|
@@ -438,12 +454,18 @@ Full TypeScript support with comprehensive type definitions:
|
|
|
438
454
|
```typescript
|
|
439
455
|
import type {
|
|
440
456
|
SpooledClientConfig,
|
|
441
|
-
Job,
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
457
|
+
Job,
|
|
458
|
+
JobStatus,
|
|
459
|
+
CreateJobParams,
|
|
460
|
+
Queue,
|
|
461
|
+
QueueStats,
|
|
462
|
+
Schedule,
|
|
463
|
+
CreateScheduleParams,
|
|
464
|
+
Workflow,
|
|
465
|
+
CreateWorkflowParams,
|
|
466
|
+
Worker,
|
|
467
|
+
WorkerStatus,
|
|
468
|
+
} from "@spooled/sdk";
|
|
447
469
|
```
|
|
448
470
|
|
|
449
471
|
## Development and releases
|