@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 +138 -97
- package/dist/{index-Lgo8JxCV.d.cts → index-D_MpLQYY.d.cts} +161 -69
- package/dist/{index-Lgo8JxCV.d.ts → index-D_MpLQYY.d.ts} +161 -69
- package/dist/index.cjs +369 -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 +369 -90
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +20 -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 +20 -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,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
|
|
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
|
+
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(
|
|
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:
|
|
150
|
+
name: "ETL Pipeline",
|
|
145
151
|
jobs: [
|
|
146
|
-
{ key:
|
|
147
|
-
{
|
|
148
|
-
|
|
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:
|
|
160
|
-
cronExpression:
|
|
161
|
-
timezone:
|
|
162
|
-
queueName:
|
|
163
|
-
payloadTemplate: { type:
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
secret:
|
|
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: [
|
|
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:
|
|
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:
|
|
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:
|
|
253
|
-
queues: [
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
308
|
-
|
|
309
|
-
| **Free**
|
|
310
|
-
| **Starter**
|
|
311
|
-
| **Enterprise** | 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
|
|
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 ===
|
|
369
|
+
if (error instanceof RateLimitError && error.code === "QUOTA_EXCEEDED") {
|
|
336
370
|
console.log(`Plan limit: ${error.message}`);
|
|
337
|
-
console.log(`Request ID: ${error.requestId ??
|
|
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(
|
|
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
|
|
391
|
+
} from "@spooled/sdk";
|
|
358
392
|
|
|
359
393
|
try {
|
|
360
|
-
await client.jobs.get(
|
|
394
|
+
await client.jobs.get("non-existent");
|
|
361
395
|
} catch (error) {
|
|
362
396
|
if (error instanceof NotFoundError) {
|
|
363
|
-
console.log(
|
|
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
|
|
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:
|
|
387
|
-
apiKey:
|
|
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:
|
|
394
|
-
hostname:
|
|
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:
|
|
401
|
-
payload: { to:
|
|
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:
|
|
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,
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|