@spooled/sdk 1.3.0 → 1.4.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 CHANGED
@@ -21,6 +21,7 @@ Official Node.js/TypeScript SDK for [Spooled Cloud](https://spooled.cloud) - a m
21
21
  - **Organization management** with usage tracking
22
22
  - **Webhook management** with automatic retries
23
23
  - **Dead Letter Queue (DLQ)** operations
24
+ - **Server-side job search** (status lists, text, job type, creation time), paging with `hasMore`, and **job history**
24
25
  - **API Key management** with Redis caching (~28x faster authentication)
25
26
  - **Billing integration** via Stripe
26
27
  - **Automatic case conversion** between camelCase (SDK) and snake_case (API)
@@ -109,6 +110,24 @@ const { id } = await client.jobs.create({
109
110
  });
110
111
  ```
111
112
 
113
+ Find jobs on the server and page through them (API 0.1.122+):
114
+
115
+ ```typescript
116
+ const page = await client.jobs.listPage({
117
+ status: ["failed", "deadletter"], // one status or several
118
+ q: "invoice", // id prefix, or text in the queue name or payload.job_type
119
+ jobType: "send-invoice", // exact payload.job_type
120
+ createdAfter: new Date(Date.now() - 86_400_000), // Date or RFC 3339 string
121
+ limit: 100,
122
+ });
123
+ console.log(page.data.length, page.hasMore); // hasMore comes from X-Has-More
124
+
125
+ // What happened to a job: claims, completions, retries, dead-lettering
126
+ const history = await client.jobs.history(id, { order: "desc" });
127
+ ```
128
+
129
+ `jobs.list()` takes the same filters and still returns a plain array.
130
+
112
131
  ### Workers
113
132
 
114
133
  Process jobs with the built-in worker runtime:
@@ -153,7 +172,13 @@ the server makes them idempotent. If an ack still fails after the retries the wo
153
172
  Give any worker that restarts a stable `workerId` (1-128 chars, `[A-Za-z0-9._-]`). Registration then
154
173
  upserts a single row instead of minting a new UUID each start, which otherwise leaves the previous
155
174
  registration counting against your plan's worker limit until the stale-worker reaper clears it
156
- (~2 minutes).
175
+ (~2 minutes). `workers.get()` and `workers.list()` report `jobsProcessed` and `jobsFailed` per worker
176
+ (API 0.1.122+).
177
+
178
+ If you process jobs without `SpooledWorker` (`jobs.claim()` and then `jobs.complete()`,
179
+ `jobs.fail()` or `jobs.heartbeat()`), pass the claimed job's `leaseId` on every one of those calls.
180
+ The API requires it: a call without it fails with a `ValidationError` (HTTP 422), and a stale lease
181
+ with HTTP 409. `SpooledWorker` does this for you.
157
182
 
158
183
  ### Workflows (DAGs)
159
184
 
@@ -241,6 +266,14 @@ const webhook = await client.webhooks.create({
241
266
  secret: "webhook_secret_key",
242
267
  });
243
268
 
269
+ // Post dead-lettered jobs and workflow outcomes straight to a Slack channel
270
+ await client.webhooks.create({
271
+ name: "On-call",
272
+ url: "https://hooks.slack.com/services/...",
273
+ events: ["job.deadlettered", "workflow.failed"],
274
+ format: "slack", // "json" (default) | "slack" | "discord" | "teams"
275
+ });
276
+
244
277
  // Retry a failed delivery
245
278
  await client.webhooks.retryDelivery(webhookId, deliveryId);
246
279
 
@@ -249,6 +282,13 @@ await client.webhooks.retryDelivery(webhookId, deliveryId);
249
282
  const deliveries = await client.webhooks.getDeliveries(webhookId);
250
283
  ```
251
284
 
285
+ API 0.1.122 added the events `job.deadlettered`, `job.retried`, `workflow.completed` and
286
+ `workflow.failed`, and `format`, which sends each delivery as a chat message that a Slack, Discord
287
+ or Teams (legacy Office 365 connector) incoming-webhook URL accepts as it is.
288
+
289
+ The inbound side has a log too: `client.ingest.listDeliveries({ status, since, queueName })` lists
290
+ the accepted requests to your custom webhook URL (`GET /webhooks/deliveries`, no payloads).
291
+
252
292
  After 20 consecutive failed deliveries a webhook is disabled automatically (`enabled: false`,
253
293
  `lastStatus: "auto_disabled"`) and stops receiving events until you turn it back on:
254
294
 
@@ -296,7 +336,7 @@ Manage API keys programmatically:
296
336
  const apiKey = await client.apiKeys.create({
297
337
  name: "Production Worker",
298
338
  queues: ["emails", "notifications"],
299
- rateLimit: 1000,
339
+ rateLimit: 1000, // optional, requests per MINUTE for this key (can only lower the plan limit)
300
340
  });
301
341
 
302
342
  console.log(`Save this key: ${apiKey.key}`); // Only shown once!
@@ -351,10 +391,13 @@ All operations automatically enforce tier-based limits:
351
391
 
352
392
  | Tier | Active Jobs | Daily Jobs | Queues | Workers | Webhooks |
353
393
  | -------------- | ----------- | ---------- | --------- | --------- | --------- |
354
- | **Free** | 10 | 1,000 | 5 | 3 | 2 |
355
- | **Starter** | 100 | 100,000 | 25 | 25 | 10 |
394
+ | **Free** | 10 | 1,000 | 2 | 1 | 1 |
395
+ | **Starter** | 500 | 10,000 | 10 | 5 | 5 |
396
+ | **Pro** | 5,000 | 100,000 | 50 | 25 | 20 |
356
397
  | **Enterprise** | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
357
398
 
399
+ (Source: the backend's `config/plans.rs`; Free has no workflows, Starter 5, Pro 25.)
400
+
358
401
  **Limits are enforced on:**
359
402
 
360
403
  - ✅ HTTP job creation (`POST /jobs`, `POST /jobs/bulk`)
@@ -389,10 +432,23 @@ try {
389
432
  }
390
433
  ```
391
434
 
392
- (Per-second rate limiting also returns `429`, as a `RateLimitError` with the default `RATE_LIMIT_EXCEEDED` code.)
435
+ (Request-rate limiting also returns `429`, as a `RateLimitError` with the default `RATE_LIMIT_EXCEEDED` code. The plan limit is per organization in requests per second; an API key's own `rateLimit` is requests per minute.)
436
+
437
+ Since API 0.1.122, responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
438
+ (a Unix time in seconds) and `X-RateLimit-Scope` (`organization` or `api_key`), and a 429 adds
439
+ `Retry-After`. `client.getRateLimitInfo()` returns the values from the most recent response that had
440
+ them, and `RateLimitError.rateLimitInfo` those of a 429. The headers are optional: public routes and
441
+ servers without Redis do not send them.
442
+
443
+ ```typescript
444
+ await client.jobs.getStats();
445
+ const rate = client.getRateLimitInfo();
446
+ // { limit: 200, remaining: 199, reset: Date, scope: "organization" }
447
+ ```
393
448
 
394
- A plan quota does not clear for hours, so `error.isQuota` is `true` for it and the SDK never retries it and
395
- never counts it toward the circuit breaker (rate-limit 429s are retried after `Retry-After`).
449
+ A plan quota does not clear for hours, so `error.isQuota` is `true` for it and the SDK never retries it.
450
+ Rate-limit 429s are retried after `Retry-After`. No 429 counts toward the circuit breaker, so a burst
451
+ above the plan limit slows the client down instead of opening the breaker for every call.
396
452
 
397
453
  ## Error Handling
398
454
 
@@ -440,11 +496,12 @@ const grpcClient = new SpooledGrpcClient({
440
496
  useTls: true,
441
497
  });
442
498
 
443
- // Register worker
499
+ // Register worker (a stable workerId is reused across restarts)
444
500
  const { workerId } = await grpcClient.workers.register({
445
501
  queueName: "emails",
446
502
  hostname: "worker-1",
447
503
  maxConcurrency: 10,
504
+ workerId: "emails-worker-1",
448
505
  });
449
506
 
450
507
  // Enqueue job (enforces plan limits automatically)
@@ -486,6 +543,20 @@ for (const job of jobs) {
486
543
  }
487
544
  ```
488
545
 
546
+ `fail` sends `retry: true` unless you pass `retry: false`; `false` is sent on the wire (proto field 4
547
+ has presence) and dead-letters the job at once, whatever its `maxRetries`.
548
+
549
+ Over gRPC `payload` and `result` are `google.protobuf.Struct`, so two things differ from REST: only a
550
+ JSON object travels (a job whose payload is an array, string, number or `null` arrives as an empty
551
+ object), and integers at or above 2^53 lose precision (send them as strings). Use REST for such jobs.
552
+
553
+ `dequeue` hands out up to `batchSize` jobs, each with a lease of `leaseDurationSecs` (5-3600 s; when
554
+ omitted the server default applies, 300 s on Spooled Cloud). For a job that can run longer than its
555
+ lease, pass a longer lease or renew it before it expires (`renewLease` with `jobId`, `workerId`,
556
+ `leaseId`, `extensionSecs`). A gRPC `StreamJobs` stream delivers **one job at a time**: the next
557
+ job is sent only after the previous one on that stream is completed, failed or released, so open
558
+ one stream per job slot you want to run in parallel.
559
+
489
560
  **gRPC Features:**
490
561
 
491
562
  - **~28x faster** than HTTP (with Redis cache: ~50ms vs 1400ms)
@@ -519,12 +590,13 @@ import type {
519
590
 
520
591
  ## Development and releases
521
592
 
522
- Before opening a pull request or tagging a release, run the same checks used by the package and publish workflow:
593
+ CI (`.github/workflows/ci.yml`) runs these checks on Node 22 and 24 for every push to `main` and every pull request; the publish workflow runs them again on Node 22 before publishing. Run them locally before opening a pull request or tagging a release:
523
594
 
524
595
  ```bash
525
596
  npm ci
526
597
  npm run lint
527
598
  npm run typecheck
599
+ npm run typecheck:examples
528
600
  npm run build
529
601
  npm test
530
602
  ```