@igniter-js/jobs 0.1.1 → 0.1.12
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/AGENTS.md +1118 -96
- package/CHANGELOG.md +8 -0
- package/README.md +2146 -93
- package/dist/{adapter-PiDCQWQd.d.mts → adapter-CXZxomI9.d.mts} +2 -2
- package/dist/{adapter-PiDCQWQd.d.ts → adapter-CXZxomI9.d.ts} +2 -2
- package/dist/adapters/bullmq.adapter.d.mts +2 -2
- package/dist/adapters/bullmq.adapter.d.ts +2 -2
- package/dist/adapters/bullmq.adapter.js +2 -2
- package/dist/adapters/bullmq.adapter.js.map +1 -1
- package/dist/adapters/bullmq.adapter.mjs +1 -1
- package/dist/adapters/bullmq.adapter.mjs.map +1 -1
- package/dist/adapters/index.d.mts +140 -2
- package/dist/adapters/index.d.ts +140 -2
- package/dist/adapters/index.js +864 -31
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs +863 -31
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/adapters/memory.adapter.d.mts +2 -2
- package/dist/adapters/memory.adapter.d.ts +2 -2
- package/dist/adapters/memory.adapter.js +122 -30
- package/dist/adapters/memory.adapter.js.map +1 -1
- package/dist/adapters/memory.adapter.mjs +121 -29
- package/dist/adapters/memory.adapter.mjs.map +1 -1
- package/dist/index.d.mts +452 -342
- package/dist/index.d.ts +452 -342
- package/dist/index.js +1923 -1002
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1921 -1001
- package/dist/index.mjs.map +1 -1
- package/dist/shim.d.mts +36 -0
- package/dist/shim.d.ts +36 -0
- package/dist/shim.js +75 -0
- package/dist/shim.js.map +1 -0
- package/dist/shim.mjs +67 -0
- package/dist/shim.mjs.map +1 -0
- package/dist/telemetry/index.d.mts +281 -0
- package/dist/telemetry/index.d.ts +281 -0
- package/dist/telemetry/index.js +97 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/index.mjs +95 -0
- package/dist/telemetry/index.mjs.map +1 -0
- package/package.json +44 -11
package/README.md
CHANGED
|
@@ -1,57 +1,78 @@
|
|
|
1
1
|
# @igniter-js/jobs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@igniter-js/jobs)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://nodejs.org)
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
**Type-safe jobs, queues, and workers for Igniter.js**
|
|
11
|
+
Define queues and jobs with full TypeScript inference, validate inputs at runtime, and run workers with built-in observability.
|
|
8
12
|
|
|
9
|
-
-
|
|
10
|
-
- Queue/job registry with full TypeScript inference via `IgniterQueue`
|
|
11
|
-
- Cron tasks via `queue.addCron()` (BullMQ auto-schedules repeatable jobs)
|
|
12
|
-
- Optional runtime input validation (Zod-like schemas or StandardSchemaV1)
|
|
13
|
-
- Single-scope support for multi-tenancy (`jobs.scope(type, id)` or per-dispatch scope)
|
|
14
|
-
- Typed events via `subscribe()` at runtime/queue/job levels
|
|
15
|
-
- Distributed management APIs (queue/job search, pause/resume, retry, logs, progress)
|
|
13
|
+
[Quick Start](#-quick-start) • [Why](#-why-igniter-jsjobs) • [Examples](#-real-world-examples) • [API Reference](#-api-reference) • [Telemetry](#-telemetry)
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ✨ Why @igniter-js/jobs?
|
|
20
|
+
|
|
21
|
+
Background processing is notoriously hard to keep reliable and observable. `@igniter-js/jobs` is built to solve the most common pain points:
|
|
22
|
+
|
|
23
|
+
- ✅ **Type-safe job inputs** — Your job handlers and dispatch calls stay in sync.
|
|
24
|
+
- ✅ **Zero-boilerplate queues** — Define jobs with a fluent queue builder.
|
|
25
|
+
- ✅ **Runtime validation** — Zod or Standard Schema V1 input validation.
|
|
26
|
+
- ✅ **Scoped jobs** — First-class multi-tenant support via `scope()`.
|
|
27
|
+
- ✅ **Observability built-in** — Telemetry events and pub/sub job lifecycle events.
|
|
28
|
+
- ✅ **Adapter-based backends** — In-memory for tests, SQLite for local, BullMQ for production.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🚀 Quick Start
|
|
33
|
+
|
|
34
|
+
### Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# npm
|
|
38
|
+
npm install @igniter-js/jobs zod
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# pnpm
|
|
43
|
+
pnpm add @igniter-js/jobs zod
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# yarn
|
|
48
|
+
yarn add @igniter-js/jobs zod
|
|
49
|
+
```
|
|
18
50
|
|
|
19
51
|
```bash
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
npm install bullmq ioredis zod @igniter-js/adapter-bullmq
|
|
52
|
+
# bun
|
|
53
|
+
bun add @igniter-js/jobs zod
|
|
23
54
|
```
|
|
24
55
|
|
|
25
|
-
|
|
56
|
+
### Your First Queue (60 seconds)
|
|
26
57
|
|
|
27
|
-
```
|
|
58
|
+
```typescript
|
|
28
59
|
import { IgniterJobs, IgniterQueue } from "@igniter-js/jobs";
|
|
29
|
-
import {
|
|
30
|
-
import Redis from "ioredis";
|
|
60
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
31
61
|
import { z } from "zod";
|
|
32
62
|
|
|
33
|
-
type AppContext = { mailer:
|
|
34
|
-
|
|
35
|
-
const redis = new Redis();
|
|
63
|
+
type AppContext = { mailer: { sendWelcome: (email: string) => Promise<void> } };
|
|
36
64
|
|
|
37
65
|
const emailQueue = IgniterQueue.create("email")
|
|
38
|
-
.withContext<AppContext>()
|
|
39
66
|
.addJob("sendWelcome", {
|
|
40
67
|
input: z.object({ email: z.string().email() }),
|
|
41
68
|
handler: async ({ input, context }) => {
|
|
42
|
-
await context.mailer.
|
|
43
|
-
},
|
|
44
|
-
})
|
|
45
|
-
.addCron("cleanupExpired", {
|
|
46
|
-
cron: "0 2 * * *",
|
|
47
|
-
handler: async ({ context }) => {
|
|
48
|
-
await context.mailer.cleanup();
|
|
69
|
+
await context.mailer.sendWelcome(input.email);
|
|
49
70
|
},
|
|
50
71
|
})
|
|
51
72
|
.build();
|
|
52
73
|
|
|
53
|
-
const jobs = IgniterJobs.create
|
|
54
|
-
.withAdapter(
|
|
74
|
+
const jobs = IgniterJobs.create()
|
|
75
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
55
76
|
.withService("my-api")
|
|
56
77
|
.withEnvironment("development")
|
|
57
78
|
.withContext(async () => ({ mailer }))
|
|
@@ -61,112 +82,2144 @@ const jobs = IgniterJobs.create<AppContext>()
|
|
|
61
82
|
await jobs.email.sendWelcome.dispatch({ input: { email: "user@example.com" } });
|
|
62
83
|
```
|
|
63
84
|
|
|
64
|
-
|
|
85
|
+
**✅ Success!** You just created a typed job, registered it with a queue, and dispatched it.
|
|
65
86
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🎯 Core Concepts
|
|
90
|
+
|
|
91
|
+
### Architecture Overview
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
95
|
+
│ Your App │
|
|
96
|
+
├─────────────────────────────────────────────────────────────┤
|
|
97
|
+
│ jobs.email.sendWelcome.dispatch({ input }) │
|
|
98
|
+
└────────────┬────────────────────────────────────────────────┘
|
|
99
|
+
│ Typed runtime (Proxy accessors)
|
|
100
|
+
▼
|
|
101
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
102
|
+
│ IgniterJobsManager (core) │
|
|
103
|
+
│ • Dispatch & schedule │
|
|
104
|
+
│ • Worker builder │
|
|
105
|
+
│ • Queue & job management │
|
|
106
|
+
│ • Scopes + telemetry + events │
|
|
107
|
+
└────────────┬────────────────────────────────────────────────┘
|
|
108
|
+
│ Adapter contract (IgniterJobsAdapter)
|
|
109
|
+
▼
|
|
110
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
111
|
+
│ Adapter Layer │
|
|
112
|
+
│ Memory Adapter • SQLite Adapter • BullMQ Adapter │
|
|
113
|
+
└────────────┬────────────────────────────────────────────────┘
|
|
114
|
+
│
|
|
115
|
+
▼
|
|
116
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
117
|
+
│ Queue Backend │
|
|
118
|
+
│ In-memory • SQLite • Redis (BullMQ) │
|
|
119
|
+
└─────────────────────────────────────────────────────────────┘
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Key Abstractions
|
|
123
|
+
|
|
124
|
+
- **IgniterJobs Builder** — Configures adapter, context, scopes, and queues.
|
|
125
|
+
- **IgniterQueue Builder** — Defines jobs and cron tasks.
|
|
126
|
+
- **Runtime Accessors** — Dynamic queue/job accessors with typed input.
|
|
127
|
+
- **Adapters** — Backend implementations (memory, sqlite, bullmq).
|
|
128
|
+
- **Telemetry** — Structured, typed events for observability.
|
|
129
|
+
- **Scopes** — Optional tenant isolation for multi-tenant systems.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 📖 Usage Examples
|
|
134
|
+
|
|
135
|
+
### Example Index
|
|
136
|
+
|
|
137
|
+
1. Basic queue + job
|
|
138
|
+
2. Typed input with Zod
|
|
139
|
+
3. Register jobs runtime
|
|
140
|
+
4. Dispatch a job
|
|
141
|
+
5. Schedule a job (delay)
|
|
142
|
+
6. Schedule a job (absolute time)
|
|
143
|
+
7. Add cron tasks
|
|
144
|
+
8. Scope jobs (multi-tenant)
|
|
145
|
+
9. Per-dispatch scope override
|
|
146
|
+
10. Subscribe to all events
|
|
147
|
+
11. Subscribe to queue events
|
|
148
|
+
12. Subscribe to job events
|
|
149
|
+
13. Queue management APIs
|
|
150
|
+
14. Queue cleaning
|
|
151
|
+
15. Queue obliterate
|
|
152
|
+
16. Queue retry all failed
|
|
153
|
+
17. Job inspection
|
|
154
|
+
18. Job retry/remove/promote
|
|
155
|
+
19. Move job to failed
|
|
156
|
+
20. Retry many jobs
|
|
157
|
+
21. Remove many jobs
|
|
158
|
+
22. Pause/resume job type
|
|
159
|
+
23. Create a worker
|
|
160
|
+
24. Worker hooks
|
|
161
|
+
25. Worker control
|
|
162
|
+
26. Worker metrics
|
|
163
|
+
27. Search jobs
|
|
164
|
+
28. Search queues
|
|
165
|
+
29. Search workers
|
|
166
|
+
30. Shutdown
|
|
167
|
+
31. Queue defaults
|
|
168
|
+
32. Worker defaults
|
|
169
|
+
33. Auto-start config (stored)
|
|
170
|
+
34. Job priority + delay combo
|
|
171
|
+
35. Remove-on-complete policies
|
|
172
|
+
36. Remove-on-fail policies
|
|
173
|
+
37. Custom metadata pattern
|
|
174
|
+
38. Scoped metadata merge
|
|
175
|
+
39. Standard schema guard
|
|
176
|
+
40. Result mapping pattern
|
|
177
|
+
41. Idempotency guard
|
|
178
|
+
42. Dead-letter alerting
|
|
179
|
+
43. Progress updates
|
|
180
|
+
44. Job logs inspection
|
|
181
|
+
45. Queue list with paging
|
|
182
|
+
46. Worker limiter pattern
|
|
183
|
+
47. Worker sharding by queue
|
|
184
|
+
48. Graceful shutdown in process
|
|
185
|
+
49. Global events to analytics
|
|
186
|
+
50. Event filtering pattern
|
|
187
|
+
51. SQLite adapter usage
|
|
188
|
+
52. SQLite adapter persistence
|
|
189
|
+
53. Memory adapter testing
|
|
190
|
+
54. BullMQ adapter usage
|
|
191
|
+
55. Custom adapter skeleton
|
|
192
|
+
56. Telemetry integration
|
|
193
|
+
57. Next.js integration
|
|
194
|
+
58. Express integration
|
|
195
|
+
59. Fastify integration
|
|
196
|
+
60. Server-only safety
|
|
197
|
+
|
|
198
|
+
### 1) Basic Queue + Job
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { IgniterQueue } from "@igniter-js/jobs";
|
|
202
|
+
|
|
203
|
+
const emailQueue = IgniterQueue.create("email")
|
|
204
|
+
.addJob("sendReceipt", {
|
|
205
|
+
handler: async ({ input }) => {
|
|
206
|
+
// input is unknown unless you define a schema
|
|
207
|
+
await sendReceipt(input as { orderId: string });
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
.build();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 2) Typed Input with Zod
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { z } from "zod";
|
|
217
|
+
|
|
218
|
+
const uploadQueue = IgniterQueue.create("uploads")
|
|
219
|
+
.addJob("processImage", {
|
|
220
|
+
input: z.object({ url: z.string().url(), width: z.number().min(1) }),
|
|
221
|
+
handler: async ({ input }) => {
|
|
222
|
+
await resizeImage(input.url, input.width);
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
.build();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 3) Register Jobs Runtime
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { IgniterJobs } from "@igniter-js/jobs";
|
|
232
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
233
|
+
|
|
234
|
+
const jobs = IgniterJobs.create()
|
|
235
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
236
|
+
.withService("worker")
|
|
237
|
+
.withEnvironment("local")
|
|
238
|
+
.withContext(async () => ({ db }))
|
|
239
|
+
.addQueue(uploadQueue)
|
|
240
|
+
.build();
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### 4) Dispatch a Job
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
await jobs.uploads.processImage.dispatch({
|
|
247
|
+
input: { url: "https://cdn.example.com/a.png", width: 640 },
|
|
248
|
+
priority: 10,
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 5) Schedule a Job (Delay)
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
await jobs.uploads.processImage.schedule({
|
|
256
|
+
input: { url: "https://cdn.example.com/a.png", width: 640 },
|
|
257
|
+
delay: 60_000,
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 6) Schedule a Job (Absolute Time)
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
await jobs.uploads.processImage.schedule({
|
|
265
|
+
input: { url: "https://cdn.example.com/a.png", width: 640 },
|
|
266
|
+
at: new Date(Date.now() + 5 * 60 * 1000),
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 7) Add Cron Tasks
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
const reportsQueue = IgniterQueue.create("reports")
|
|
274
|
+
.addCron("dailySummary", {
|
|
275
|
+
cron: "0 2 * * *",
|
|
276
|
+
handler: async ({ context }) => {
|
|
277
|
+
await context.reports.runDailySummary();
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
.build();
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 8) Scope Jobs (Multi-Tenant)
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const jobs = IgniterJobs.create()
|
|
287
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
288
|
+
.withService("api")
|
|
289
|
+
.withEnvironment("production")
|
|
290
|
+
.withContext(async () => ({ db }))
|
|
72
291
|
.addScope("organization", { required: true })
|
|
73
292
|
.addQueue(emailQueue)
|
|
74
293
|
.build();
|
|
75
294
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
295
|
+
const orgJobs = jobs.scope("organization", "org_123");
|
|
296
|
+
await orgJobs.email.sendWelcome.dispatch({ input: { email: "a@b.com" } });
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 9) Per-Dispatch Scope Override
|
|
81
300
|
|
|
82
|
-
|
|
301
|
+
```typescript
|
|
83
302
|
await jobs.email.sendWelcome.dispatch({
|
|
84
|
-
input: { email: "
|
|
85
|
-
scope: { type: "organization", id: "org_123"
|
|
303
|
+
input: { email: "a@b.com" },
|
|
304
|
+
scope: { type: "organization", id: "org_123" },
|
|
86
305
|
});
|
|
87
306
|
```
|
|
88
307
|
|
|
89
|
-
|
|
308
|
+
### 10) Subscribe to All Events
|
|
90
309
|
|
|
91
|
-
```
|
|
92
|
-
// Global listener (unscoped)
|
|
310
|
+
```typescript
|
|
93
311
|
const unsubscribe = await jobs.subscribe((event) => {
|
|
94
|
-
console.log(event.type, event.data, event.timestamp
|
|
312
|
+
console.log(event.type, event.data, event.timestamp);
|
|
95
313
|
});
|
|
96
314
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
315
|
+
await unsubscribe();
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### 11) Subscribe to Queue Events
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const unsubscribe = await jobs.email.subscribe((event) => {
|
|
100
322
|
console.log(event.type, event.data);
|
|
101
323
|
});
|
|
102
324
|
|
|
103
325
|
await unsubscribe();
|
|
104
326
|
```
|
|
105
327
|
|
|
106
|
-
|
|
328
|
+
### 12) Subscribe to Job Events
|
|
107
329
|
|
|
108
|
-
|
|
330
|
+
```typescript
|
|
331
|
+
const unsubscribe = await jobs.email.sendWelcome.subscribe((event) => {
|
|
332
|
+
console.log(event.type, event.data);
|
|
333
|
+
});
|
|
109
334
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
import { IgniterJobsTelemetryEvents } from "@igniter-js/jobs";
|
|
335
|
+
await unsubscribe();
|
|
336
|
+
```
|
|
113
337
|
|
|
114
|
-
|
|
115
|
-
const telemetry = IgniterTelemetry.create()
|
|
116
|
-
.withService("my-api")
|
|
117
|
-
.addEvents(IgniterJobsTelemetryEvents)
|
|
118
|
-
.build();
|
|
338
|
+
### 13) Queue Management APIs
|
|
119
339
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
340
|
+
```typescript
|
|
341
|
+
const info = await jobs.email.get().retrieve();
|
|
342
|
+
await jobs.email.get().pause();
|
|
343
|
+
await jobs.email.get().resume();
|
|
344
|
+
await jobs.email.get().drain();
|
|
125
345
|
```
|
|
126
346
|
|
|
127
|
-
|
|
347
|
+
### 14) Queue Cleaning
|
|
128
348
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
349
|
+
```typescript
|
|
350
|
+
await jobs.email.get().clean({
|
|
351
|
+
status: ["completed", "failed"],
|
|
352
|
+
olderThan: 7 * 24 * 60 * 60 * 1000,
|
|
353
|
+
limit: 1000,
|
|
354
|
+
});
|
|
355
|
+
```
|
|
134
356
|
|
|
135
|
-
|
|
357
|
+
### 15) Queue Obliterate
|
|
136
358
|
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
359
|
+
```typescript
|
|
360
|
+
await jobs.email.get().obliterate({ force: true });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 16) Queue Retry All Failed
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
const retried = await jobs.email.get().retryAll();
|
|
367
|
+
console.log(`Retried ${retried} jobs`);
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 17) Job Inspection
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const job = await jobs.email.sendWelcome.get("job-id").retrieve();
|
|
374
|
+
const state = await jobs.email.sendWelcome.get("job-id").state();
|
|
375
|
+
const logs = await jobs.email.sendWelcome.get("job-id").logs();
|
|
376
|
+
const progress = await jobs.email.sendWelcome.get("job-id").progress();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 18) Job Retry / Remove / Promote
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
await jobs.email.sendWelcome.get("job-id").retry();
|
|
383
|
+
await jobs.email.sendWelcome.get("job-id").remove();
|
|
384
|
+
await jobs.email.sendWelcome.get("job-id").promote();
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### 19) Move Job to Failed
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
await jobs.email.sendWelcome.get("job-id").move("failed", "Manual fail");
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 20) Retry Many Jobs
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
await jobs.email.sendWelcome.many(["job-1", "job-2"]).retry();
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### 21) Remove Many Jobs
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
await jobs.email.sendWelcome.many(["job-1", "job-2"]).remove();
|
|
403
|
+
```
|
|
142
404
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
405
|
+
### 22) Pause/Resume Job Type
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
await jobs.email.sendWelcome.pause();
|
|
409
|
+
await jobs.email.sendWelcome.resume();
|
|
147
410
|
```
|
|
148
411
|
|
|
149
|
-
|
|
412
|
+
> Note: The BullMQ adapter throws `JOBS_QUEUE_OPERATION_FAILED` for pause/resume job type.
|
|
413
|
+
|
|
414
|
+
### 23) Create a Worker
|
|
150
415
|
|
|
151
|
-
```
|
|
416
|
+
```typescript
|
|
152
417
|
const worker = await jobs.worker
|
|
153
418
|
.create()
|
|
154
419
|
.addQueue("email")
|
|
155
420
|
.withConcurrency(10)
|
|
156
|
-
.
|
|
421
|
+
.start();
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### 24) Worker Hooks
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
const worker = await jobs.worker
|
|
428
|
+
.create()
|
|
429
|
+
.addQueue("email")
|
|
430
|
+
.onActive(({ job }) => console.log("Active", job.id))
|
|
431
|
+
.onSuccess(({ job }) => console.log("Success", job.id))
|
|
432
|
+
.onFailure(({ job, error }) => console.error("Fail", job.id, error))
|
|
433
|
+
.onIdle(() => console.log("Idle"))
|
|
434
|
+
.start();
|
|
435
|
+
```
|
|
157
436
|
|
|
158
|
-
|
|
437
|
+
### 25) Worker Control
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
159
440
|
await worker.pause();
|
|
160
441
|
await worker.resume();
|
|
161
442
|
await worker.close();
|
|
162
443
|
```
|
|
163
444
|
|
|
164
|
-
|
|
445
|
+
### 26) Worker Metrics
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const metrics = await worker.getMetrics();
|
|
449
|
+
console.log(metrics.processed, metrics.failed, metrics.avgDuration);
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 27) Search Jobs
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
const failedJobs = await jobs.search("jobs", {
|
|
456
|
+
status: ["failed"],
|
|
457
|
+
queue: "email",
|
|
458
|
+
limit: 50,
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### 28) Search Queues
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
const queues = await jobs.search("queues", { isPaused: false });
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 29) Search Workers
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
const workers = await jobs.search("workers", { queue: "email" });
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 30) Shutdown
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
await jobs.shutdown();
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### 31) Queue Defaults
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
const jobs = IgniterJobs.create()
|
|
484
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
485
|
+
.withService("api")
|
|
486
|
+
.withEnvironment("development")
|
|
487
|
+
.withContext(async () => ({ db }))
|
|
488
|
+
.withQueueDefaults({ attempts: 3, removeOnComplete: 100 })
|
|
489
|
+
.addQueue(emailQueue)
|
|
490
|
+
.build();
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### 32) Worker Defaults
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
const jobs = IgniterJobs.create()
|
|
497
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
498
|
+
.withService("api")
|
|
499
|
+
.withEnvironment("development")
|
|
500
|
+
.withContext(async () => ({ db }))
|
|
501
|
+
.withWorkerDefaults({ concurrency: 5 })
|
|
502
|
+
.addQueue(emailQueue)
|
|
503
|
+
.build();
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 33) Auto-Start Config (Stored)
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
const jobs = IgniterJobs.create()
|
|
510
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
511
|
+
.withService("api")
|
|
512
|
+
.withEnvironment("development")
|
|
513
|
+
.withContext(async () => ({ db }))
|
|
514
|
+
.withAutoStartWorker({ queues: ["email"], concurrency: 2 })
|
|
515
|
+
.addQueue(emailQueue)
|
|
516
|
+
.build();
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
> Note: Auto-start configuration is stored in runtime config; the core runtime does not start workers automatically.
|
|
520
|
+
|
|
521
|
+
### 34) Job Priority + Delay Combo
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
await jobs.notifications.digest.dispatch({
|
|
525
|
+
input: { userId: "u_1" },
|
|
526
|
+
priority: 25,
|
|
527
|
+
delay: 30_000,
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### 35) Remove-on-Complete Policies
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
const queue = IgniterQueue.create("cleanup")
|
|
535
|
+
.addJob("purge", {
|
|
536
|
+
removeOnComplete: 1000,
|
|
537
|
+
handler: async () => purgeOldItems(),
|
|
538
|
+
})
|
|
539
|
+
.build();
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### 36) Remove-on-Fail Policies
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
const queue = IgniterQueue.create("sync")
|
|
546
|
+
.addJob("pull", {
|
|
547
|
+
removeOnFail: false,
|
|
548
|
+
handler: async () => pullUpdates(),
|
|
549
|
+
})
|
|
550
|
+
.build();
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### 37) Custom Metadata Pattern
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
await jobs.email.sendWelcome.dispatch({
|
|
557
|
+
input: { email: "user@example.com" },
|
|
558
|
+
metadata: {
|
|
559
|
+
"ctx.request.id": "req_123",
|
|
560
|
+
"ctx.user.id": "user_456",
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### 38) Scoped Metadata Merge
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
const scoped = jobs.scope("organization", "org_123", { plan: "pro" });
|
|
569
|
+
await scoped.email.sendWelcome.dispatch({
|
|
570
|
+
input: { email: "user@example.com" },
|
|
571
|
+
metadata: { "ctx.session.id": "sess_9" },
|
|
572
|
+
});
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### 39) Standard Schema Guard
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
const queue = IgniterQueue.create("webhooks")
|
|
579
|
+
.addJob("ingest", {
|
|
580
|
+
input: schema,
|
|
581
|
+
handler: async ({ input }) => processWebhook(input),
|
|
582
|
+
})
|
|
583
|
+
.build();
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### 40) Result Mapping Pattern
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
const queue = IgniterQueue.create("billing")
|
|
590
|
+
.addJob("charge", {
|
|
591
|
+
input: z.object({ invoiceId: z.string() }),
|
|
592
|
+
handler: async ({ input }) => {
|
|
593
|
+
const chargeId = await chargeInvoice(input.invoiceId);
|
|
594
|
+
return { chargeId };
|
|
595
|
+
},
|
|
596
|
+
})
|
|
597
|
+
.build();
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### 41) Idempotency Guard
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
const queue = IgniterQueue.create("orders")
|
|
604
|
+
.addJob("confirm", {
|
|
605
|
+
input: z.object({ orderId: z.string() }),
|
|
606
|
+
handler: async ({ input, context }) => {
|
|
607
|
+
if (await context.orders.isConfirmed(input.orderId)) return;
|
|
608
|
+
await context.orders.confirm(input.orderId);
|
|
609
|
+
},
|
|
610
|
+
})
|
|
611
|
+
.build();
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### 42) Dead-Letter Alerting
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
const queue = IgniterQueue.create("payments")
|
|
618
|
+
.addJob("capture", {
|
|
619
|
+
attempts: 3,
|
|
620
|
+
input: z.object({ paymentId: z.string() }),
|
|
621
|
+
handler: async ({ input }) => capturePayment(input.paymentId),
|
|
622
|
+
onFailure: async ({ error, isFinalAttempt }) => {
|
|
623
|
+
if (isFinalAttempt) {
|
|
624
|
+
await sendAlert(`Final failure: ${error.message}`);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
})
|
|
628
|
+
.build();
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### 43) Progress Updates
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
const queue = IgniterQueue.create("import")
|
|
635
|
+
.addJob("csv", {
|
|
636
|
+
input: z.object({ fileId: z.string() }),
|
|
637
|
+
handler: async ({ input }) => {
|
|
638
|
+
await importCsv(input.fileId);
|
|
639
|
+
return { ok: true };
|
|
640
|
+
},
|
|
641
|
+
onProgress: async ({ progress, message }) => {
|
|
642
|
+
console.log(progress, message);
|
|
643
|
+
},
|
|
644
|
+
})
|
|
645
|
+
.build();
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### 44) Job Logs Inspection
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
const logs = await jobs.email.sendWelcome.get("job-id").logs();
|
|
652
|
+
for (const entry of logs) {
|
|
653
|
+
console.log(entry.level, entry.message, entry.timestamp);
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### 45) Queue List with Paging
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
const jobsInQueue = await jobs.email.list({
|
|
661
|
+
status: ["waiting", "active"],
|
|
662
|
+
limit: 20,
|
|
663
|
+
offset: 0,
|
|
664
|
+
});
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### 46) Worker Limiter Pattern
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
const worker = await jobs.worker
|
|
671
|
+
.create()
|
|
672
|
+
.addQueue("emails")
|
|
673
|
+
.withLimiter({ max: 50, duration: 60_000 })
|
|
674
|
+
.start();
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### 47) Worker Sharding by Queue
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
const workerA = await jobs.worker.create().addQueue("email").start();
|
|
681
|
+
const workerB = await jobs.worker.create().addQueue("analytics").start();
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### 48) Graceful Shutdown in Process
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
process.on("SIGINT", async () => {
|
|
688
|
+
await jobs.shutdown();
|
|
689
|
+
process.exit(0);
|
|
690
|
+
});
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### 49) Global Events to Analytics
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
const unsubscribe = await jobs.subscribe((event) => {
|
|
697
|
+
analytics.track("job_event", {
|
|
698
|
+
type: event.type,
|
|
699
|
+
timestamp: event.timestamp,
|
|
700
|
+
scope: event.scope,
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### 50) Event Filtering Pattern
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
const unsubscribe = await jobs.subscribe((event) => {
|
|
709
|
+
if (event.type.endsWith(":failed")) {
|
|
710
|
+
console.error("Job failed", event.data);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### 51) SQLite Adapter Usage
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
const adapter = IgniterJobsSQLiteAdapter.create({
|
|
719
|
+
path: "./jobs.sqlite",
|
|
720
|
+
pollingInterval: 500,
|
|
721
|
+
enableWAL: true,
|
|
722
|
+
});
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### 52) SQLite Adapter Persistence
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
const adapter = IgniterJobsSQLiteAdapter.create({ path: "./jobs.sqlite" });
|
|
729
|
+
await adapter.dispatch({ queue: "emails", jobName: "send", input: { id: "1" } });
|
|
730
|
+
await adapter.shutdown();
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### 53) Memory Adapter Testing
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
const adapter = IgniterJobsMemoryAdapter.create({ maxJobHistory: 500 });
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### 54) BullMQ Adapter Usage
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
const adapter = IgniterJobsBullMQAdapter.create({ redis });
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### 55) Custom Adapter Skeleton
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
class CustomAdapter implements IgniterJobsAdapter {
|
|
749
|
+
readonly client = {};
|
|
750
|
+
readonly queues = null as any;
|
|
751
|
+
async dispatch(params: IgniterJobsAdapterDispatchParams) { return "job-id"; }
|
|
752
|
+
async schedule(params: IgniterJobsAdapterScheduleParams) { return "job-id"; }
|
|
753
|
+
async getJob(jobId: string) { return null; }
|
|
754
|
+
async getJobState(jobId: string) { return null; }
|
|
755
|
+
async getJobLogs(jobId: string) { return []; }
|
|
756
|
+
async getJobProgress(jobId: string) { return 0; }
|
|
757
|
+
async retryJob(jobId: string) { /* ... */ }
|
|
758
|
+
async removeJob(jobId: string) { /* ... */ }
|
|
759
|
+
async promoteJob(jobId: string) { /* ... */ }
|
|
760
|
+
async moveJobToFailed(jobId: string, reason: string) { /* ... */ }
|
|
761
|
+
async retryManyJobs(jobIds: string[]) { /* ... */ }
|
|
762
|
+
async removeManyJobs(jobIds: string[]) { /* ... */ }
|
|
763
|
+
async getQueueInfo(queue: string) { return null; }
|
|
764
|
+
async getQueueJobCounts(queue: string) { return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, paused: 0 }; }
|
|
765
|
+
async listQueues() { return []; }
|
|
766
|
+
async pauseQueue(queue: string) { /* ... */ }
|
|
767
|
+
async resumeQueue(queue: string) { /* ... */ }
|
|
768
|
+
async drainQueue(queue: string) { return 0; }
|
|
769
|
+
async cleanQueue(queue: string, options: IgniterJobsQueueCleanOptions) { return 0; }
|
|
770
|
+
async obliterateQueue(queue: string, options?: { force?: boolean }) { /* ... */ }
|
|
771
|
+
async retryAllInQueue(queue: string) { return 0; }
|
|
772
|
+
async pauseJobType(queue: string, jobName: string) { /* ... */ }
|
|
773
|
+
async resumeJobType(queue: string, jobName: string) { /* ... */ }
|
|
774
|
+
async searchJobs(filter: Record<string, unknown>) { return []; }
|
|
775
|
+
async searchQueues(filter: Record<string, unknown>) { return []; }
|
|
776
|
+
async searchWorkers(filter: Record<string, unknown>) { return []; }
|
|
777
|
+
async createWorker(config: IgniterJobsWorkerBuilderConfig) { return {} as IgniterJobsWorkerHandle; }
|
|
778
|
+
getWorkers() { return new Map(); }
|
|
779
|
+
async publishEvent(channel: string, payload: unknown) { /* ... */ }
|
|
780
|
+
async subscribeEvent(channel: string, handler: IgniterJobsEventHandler) { return async () => {}; }
|
|
781
|
+
registerJob(queueName: string, jobName: string, definition: IgniterJobDefinition<any, any, any>) { /* ... */ }
|
|
782
|
+
registerCron(queueName: string, cronName: string, definition: IgniterCronDefinition<any, any>) { /* ... */ }
|
|
783
|
+
async shutdown() { /* ... */ }
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### 56) Telemetry Integration
|
|
788
|
+
|
|
789
|
+
```typescript
|
|
790
|
+
const telemetry = IgniterTelemetry.create()
|
|
791
|
+
.withService("my-api")
|
|
792
|
+
.withEnvironment("production")
|
|
793
|
+
.addEvents(IgniterJobsTelemetryEvents)
|
|
794
|
+
.build();
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### 57) Next.js Integration
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
export async function POST() {
|
|
801
|
+
const id = await jobs.email.sendWelcome.dispatch({ input: { email: "user@example.com" } });
|
|
802
|
+
return NextResponse.json({ jobId: id });
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### 58) Express Integration
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
app.post("/send", async (_req, res) => {
|
|
810
|
+
const jobId = await jobs.email.sendWelcome.dispatch({ input: { email: "user@example.com" } });
|
|
811
|
+
res.json({ jobId });
|
|
812
|
+
});
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### 59) Fastify Integration
|
|
816
|
+
|
|
817
|
+
```typescript
|
|
818
|
+
app.post("/send", async (_req, res) => {
|
|
819
|
+
const jobId = await jobs.email.sendWelcome.dispatch({ input: { email: "user@example.com" } });
|
|
820
|
+
return res.send({ jobId });
|
|
821
|
+
});
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### 60) Server-Only Safety
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
// Import @igniter-js/jobs only in server environments.
|
|
828
|
+
// Client bundles will throw a server-only error from the shim.
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## 🧠 Input Validation
|
|
834
|
+
|
|
835
|
+
`@igniter-js/jobs` supports **Standard Schema V1** and **Zod-like** schemas.
|
|
836
|
+
|
|
837
|
+
### Standard Schema V1
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
import { type StandardSchemaV1 } from "@igniter-js/common";
|
|
841
|
+
|
|
842
|
+
const schema: StandardSchemaV1<{ email: string }, { email: string }> = {
|
|
843
|
+
"~standard": {
|
|
844
|
+
validate: async (value) => {
|
|
845
|
+
if (!value || typeof (value as any).email !== "string") {
|
|
846
|
+
return { issues: [{ message: "email is required" }] } as any;
|
|
847
|
+
}
|
|
848
|
+
return { value } as any;
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const queue = IgniterQueue.create("email")
|
|
854
|
+
.addJob("send", {
|
|
855
|
+
input: schema,
|
|
856
|
+
handler: async ({ input }) => {
|
|
857
|
+
// input is validated and typed
|
|
858
|
+
await sendEmail(input.email);
|
|
859
|
+
},
|
|
860
|
+
})
|
|
861
|
+
.build();
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
### Zod Schema
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
import { z } from "zod";
|
|
868
|
+
|
|
869
|
+
const queue = IgniterQueue.create("email")
|
|
870
|
+
.addJob("send", {
|
|
871
|
+
input: z.object({ email: z.string().email() }),
|
|
872
|
+
handler: async ({ input }) => {
|
|
873
|
+
await sendEmail(input.email);
|
|
874
|
+
},
|
|
875
|
+
})
|
|
876
|
+
.build();
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
## 🧩 Hooks & Lifecycle
|
|
882
|
+
|
|
883
|
+
Jobs support lifecycle hooks defined in the job definition:
|
|
884
|
+
|
|
885
|
+
- `onStart`
|
|
886
|
+
- `onProgress`
|
|
887
|
+
- `onSuccess`
|
|
888
|
+
- `onFailure`
|
|
889
|
+
|
|
890
|
+
### Hook Example
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
const queue = IgniterQueue.create("billing")
|
|
894
|
+
.addJob("charge", {
|
|
895
|
+
input: z.object({ invoiceId: z.string() }),
|
|
896
|
+
handler: async ({ input }) => {
|
|
897
|
+
return chargeInvoice(input.invoiceId);
|
|
898
|
+
},
|
|
899
|
+
onStart: async ({ job }) => {
|
|
900
|
+
await audit.log("billing.started", { id: job.id });
|
|
901
|
+
},
|
|
902
|
+
onProgress: async ({ progress }) => {
|
|
903
|
+
await audit.log("billing.progress", { progress });
|
|
904
|
+
},
|
|
905
|
+
onSuccess: async ({ result }) => {
|
|
906
|
+
await audit.log("billing.success", { result });
|
|
907
|
+
},
|
|
908
|
+
onFailure: async ({ error, isFinalAttempt }) => {
|
|
909
|
+
await audit.log("billing.failed", {
|
|
910
|
+
message: error.message,
|
|
911
|
+
isFinalAttempt,
|
|
912
|
+
});
|
|
913
|
+
},
|
|
914
|
+
})
|
|
915
|
+
.build();
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
## ⏱ Scheduling Options
|
|
921
|
+
|
|
922
|
+
Scheduling options come from `IgniterJobsScheduleOptions` and can be used in `.schedule()`.
|
|
923
|
+
|
|
924
|
+
### Cron Scheduling
|
|
925
|
+
|
|
926
|
+
```typescript
|
|
927
|
+
await jobs.reports.dailySummary.schedule({
|
|
928
|
+
input: { timezone: "UTC" },
|
|
929
|
+
cron: "0 2 * * *",
|
|
930
|
+
tz: "UTC",
|
|
931
|
+
});
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### Fixed Interval
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
await jobs.analytics.rollup.schedule({
|
|
938
|
+
input: { hours: 24 },
|
|
939
|
+
every: 60 * 60 * 1000,
|
|
940
|
+
maxExecutions: 24,
|
|
941
|
+
});
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### Skip Weekends
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
await jobs.notifications.weekdayDigest.schedule({
|
|
948
|
+
input: { userId: "u_1" },
|
|
949
|
+
cron: "0 8 * * *",
|
|
950
|
+
skipWeekends: true,
|
|
951
|
+
});
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### Business Hours Only
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
await jobs.support.assign.schedule({
|
|
958
|
+
input: { ticketId: "t_1" },
|
|
959
|
+
cron: "*/15 * * * *",
|
|
960
|
+
onlyBusinessHours: true,
|
|
961
|
+
businessHours: { start: 9, end: 17, timezone: "America/New_York" },
|
|
962
|
+
});
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
## 🔭 Telemetry
|
|
968
|
+
|
|
969
|
+
Telemetry events are exported via the **telemetry subpath**:
|
|
970
|
+
|
|
971
|
+
```typescript
|
|
972
|
+
import { IgniterTelemetry } from "@igniter-js/telemetry";
|
|
973
|
+
import { IgniterJobsTelemetryEvents } from "@igniter-js/jobs/telemetry";
|
|
974
|
+
|
|
975
|
+
const telemetry = IgniterTelemetry.create()
|
|
976
|
+
.withService("my-api")
|
|
977
|
+
.withEnvironment("production")
|
|
978
|
+
.addEvents(IgniterJobsTelemetryEvents)
|
|
979
|
+
.build();
|
|
980
|
+
|
|
981
|
+
const jobs = IgniterJobs.create()
|
|
982
|
+
.withTelemetry(telemetry)
|
|
983
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
984
|
+
.withService("my-api")
|
|
985
|
+
.withEnvironment("production")
|
|
986
|
+
.withContext(async () => ({ db }))
|
|
987
|
+
.addQueue(emailQueue)
|
|
988
|
+
.build();
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
### Job Telemetry Events
|
|
992
|
+
|
|
993
|
+
- `igniter.jobs.job.enqueued`
|
|
994
|
+
- `igniter.jobs.job.started`
|
|
995
|
+
- `igniter.jobs.job.completed`
|
|
996
|
+
- `igniter.jobs.job.failed`
|
|
997
|
+
- `igniter.jobs.job.progress`
|
|
998
|
+
- `igniter.jobs.job.scheduled`
|
|
999
|
+
|
|
1000
|
+
> Note: `igniter.jobs.job.retrying` exists in the telemetry schema for future parity, but the runtime does not emit it today.
|
|
1001
|
+
|
|
1002
|
+
### Worker Telemetry Events
|
|
1003
|
+
|
|
1004
|
+
- `igniter.jobs.worker.started`
|
|
1005
|
+
- `igniter.jobs.worker.stopped`
|
|
1006
|
+
- `igniter.jobs.worker.idle`
|
|
1007
|
+
- `igniter.jobs.worker.paused`
|
|
1008
|
+
- `igniter.jobs.worker.resumed`
|
|
1009
|
+
|
|
1010
|
+
### Queue Telemetry Events
|
|
1011
|
+
|
|
1012
|
+
- `igniter.jobs.queue.paused`
|
|
1013
|
+
- `igniter.jobs.queue.resumed`
|
|
1014
|
+
- `igniter.jobs.queue.drained`
|
|
1015
|
+
- `igniter.jobs.queue.cleaned`
|
|
1016
|
+
- `igniter.jobs.queue.obliterated`
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## 🔌 Adapters
|
|
1021
|
+
|
|
1022
|
+
Adapters implement the `IgniterJobsAdapter` interface and are exported via:
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
import { IgniterJobsMemoryAdapter, IgniterJobsSQLiteAdapter, IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters";
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Adapter Comparison
|
|
1029
|
+
|
|
1030
|
+
| Adapter | Persistence | Multi-process | Use Case |
|
|
1031
|
+
|--------|-------------|---------------|---------|
|
|
1032
|
+
| Memory | ❌ | ❌ | Unit tests, local dev |
|
|
1033
|
+
| SQLite | ✅ | ⚠️ (single process) | Desktop, CLI, local |
|
|
1034
|
+
| BullMQ | ✅ | ✅ | Production scale |
|
|
1035
|
+
|
|
1036
|
+
### Memory Adapter
|
|
1037
|
+
|
|
1038
|
+
```typescript
|
|
1039
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
1040
|
+
|
|
1041
|
+
const adapter = IgniterJobsMemoryAdapter.create();
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### SQLite Adapter
|
|
1045
|
+
|
|
1046
|
+
```typescript
|
|
1047
|
+
import { IgniterJobsSQLiteAdapter } from "@igniter-js/jobs/adapters";
|
|
1048
|
+
|
|
1049
|
+
const adapter = IgniterJobsSQLiteAdapter.create({
|
|
1050
|
+
path: "./data/jobs.sqlite",
|
|
1051
|
+
pollingInterval: 500,
|
|
1052
|
+
enableWAL: true,
|
|
1053
|
+
});
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
### BullMQ Adapter
|
|
1057
|
+
|
|
1058
|
+
```typescript
|
|
1059
|
+
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters";
|
|
1060
|
+
import Redis from "ioredis";
|
|
1061
|
+
|
|
1062
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
1063
|
+
const adapter = IgniterJobsBullMQAdapter.create({ redis });
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
---
|
|
1067
|
+
|
|
1068
|
+
## 🧪 Testing
|
|
1069
|
+
|
|
1070
|
+
### Unit Testing with Memory Adapter
|
|
1071
|
+
|
|
1072
|
+
```typescript
|
|
1073
|
+
import { describe, it, expect } from "vitest";
|
|
1074
|
+
import { IgniterJobs, IgniterQueue } from "@igniter-js/jobs";
|
|
1075
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
1076
|
+
|
|
1077
|
+
describe("jobs", () => {
|
|
1078
|
+
it("dispatches and processes a job", async () => {
|
|
1079
|
+
const queue = IgniterQueue.create("email")
|
|
1080
|
+
.addJob("send", {
|
|
1081
|
+
handler: async ({ input }) => `sent:${(input as any).id}`,
|
|
1082
|
+
})
|
|
1083
|
+
.build();
|
|
1084
|
+
|
|
1085
|
+
const jobs = IgniterJobs.create()
|
|
1086
|
+
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
1087
|
+
.withService("test")
|
|
1088
|
+
.withEnvironment("test")
|
|
1089
|
+
.withContext(async () => ({}))
|
|
1090
|
+
.addQueue(queue)
|
|
1091
|
+
.build();
|
|
1092
|
+
|
|
1093
|
+
const worker = await jobs.worker.create().addQueue("email").start();
|
|
1094
|
+
const id = await jobs.email.send.dispatch({ input: { id: "1" } });
|
|
1095
|
+
|
|
1096
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1097
|
+
|
|
1098
|
+
const job = await jobs.email.send.get(id).retrieve();
|
|
1099
|
+
expect(job?.status).toBe("completed");
|
|
1100
|
+
await worker.close();
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
---
|
|
1106
|
+
|
|
1107
|
+
## 📦 Adapter Deep Dives
|
|
1108
|
+
|
|
1109
|
+
### Memory Adapter
|
|
1110
|
+
|
|
1111
|
+
Use for unit tests and local dev. It stores everything in memory and supports all management APIs.
|
|
1112
|
+
|
|
1113
|
+
Key traits:
|
|
1114
|
+
|
|
1115
|
+
- Fast, deterministic execution
|
|
1116
|
+
- Single-process only
|
|
1117
|
+
- Data lost on restart
|
|
1118
|
+
- Great for CI and tests
|
|
1119
|
+
|
|
1120
|
+
```typescript
|
|
1121
|
+
const adapter = IgniterJobsMemoryAdapter.create({ maxJobHistory: 1000 });
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### SQLite Adapter
|
|
1125
|
+
|
|
1126
|
+
Use for desktop apps, CLI tools, or local persistence without Redis.
|
|
1127
|
+
|
|
1128
|
+
Key traits:
|
|
1129
|
+
|
|
1130
|
+
- Persistent storage on disk
|
|
1131
|
+
- Single-process (WAL helps but not distributed)
|
|
1132
|
+
- Polling-based worker loop
|
|
1133
|
+
- Great for edge or MCP servers
|
|
1134
|
+
|
|
1135
|
+
```typescript
|
|
1136
|
+
const adapter = IgniterJobsSQLiteAdapter.create({
|
|
1137
|
+
path: "./data/jobs.sqlite",
|
|
1138
|
+
pollingInterval: 500,
|
|
1139
|
+
enableWAL: true,
|
|
1140
|
+
});
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
### BullMQ Adapter
|
|
1144
|
+
|
|
1145
|
+
Use for production and distributed workers. Wraps `@igniter-js/adapter-bullmq`.
|
|
1146
|
+
|
|
1147
|
+
Key traits:
|
|
1148
|
+
|
|
1149
|
+
- Redis-backed persistence
|
|
1150
|
+
- Distributed workers
|
|
1151
|
+
- Advanced scheduling
|
|
1152
|
+
- Strong throughput
|
|
1153
|
+
|
|
1154
|
+
```typescript
|
|
1155
|
+
const adapter = IgniterJobsBullMQAdapter.create({ redis });
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
Known limitations:
|
|
1159
|
+
|
|
1160
|
+
- Pause/resume for a single job type is not supported; pause the whole queue instead.
|
|
1161
|
+
|
|
1162
|
+
---
|
|
1163
|
+
|
|
1164
|
+
## 📊 Observability & Events
|
|
1165
|
+
|
|
1166
|
+
### Event Types
|
|
1167
|
+
|
|
1168
|
+
Jobs events are emitted through the adapter pub/sub channel and are scoped by service, environment, and optional scope.
|
|
1169
|
+
|
|
1170
|
+
Examples:
|
|
1171
|
+
|
|
1172
|
+
- `email:sendWelcome:enqueued`
|
|
1173
|
+
- `email:sendWelcome:started`
|
|
1174
|
+
- `email:sendWelcome:completed`
|
|
1175
|
+
- `email:sendWelcome:failed`
|
|
1176
|
+
- `email:sendWelcome:progress`
|
|
1177
|
+
|
|
1178
|
+
### Event Channel Composition
|
|
1179
|
+
|
|
1180
|
+
The runtime builds the channel using service, environment, and scope metadata. Scoped instances use a scope-specific channel, ensuring multi-tenant isolation.
|
|
1181
|
+
|
|
1182
|
+
---
|
|
1183
|
+
|
|
1184
|
+
## 📈 Performance & Scaling
|
|
1185
|
+
|
|
1186
|
+
### Queue Sizing Tips
|
|
1187
|
+
|
|
1188
|
+
- Keep payloads small and store large blobs in external storage.
|
|
1189
|
+
- Use `priority` for latency-sensitive work.
|
|
1190
|
+
- Set `attempts` and `delay` to smooth burst failures.
|
|
1191
|
+
|
|
1192
|
+
### Worker Scaling Tips
|
|
1193
|
+
|
|
1194
|
+
- Start with low `concurrency`, then scale after measuring throughput.
|
|
1195
|
+
- Use `withLimiter()` for API-bound workloads.
|
|
1196
|
+
- Shard by queue when jobs have very different resource needs.
|
|
1197
|
+
|
|
1198
|
+
### Polling (SQLite)
|
|
1199
|
+
|
|
1200
|
+
- Lower `pollingInterval` means faster reaction but higher CPU.
|
|
1201
|
+
- Higher `pollingInterval` reduces CPU at the cost of latency.
|
|
1202
|
+
|
|
1203
|
+
---
|
|
1204
|
+
|
|
1205
|
+
## 🧾 Error Code Library
|
|
1206
|
+
|
|
1207
|
+
Each error code is defined in `IGNITER_JOBS_ERROR_CODES`.
|
|
1208
|
+
|
|
1209
|
+
### JOBS_ADAPTER_REQUIRED
|
|
1210
|
+
|
|
1211
|
+
- **Context:** `IgniterJobsBuilder.build()`
|
|
1212
|
+
- **Cause:** Adapter not configured.
|
|
1213
|
+
- **Mitigation:** Always call `.withAdapter()`.
|
|
1214
|
+
- **Solution:**
|
|
1215
|
+
```typescript
|
|
1216
|
+
IgniterJobs.create().withAdapter(IgniterJobsMemoryAdapter.create())
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
### JOBS_SERVICE_REQUIRED
|
|
1220
|
+
|
|
1221
|
+
- **Context:** `IgniterJobsBuilder.build()`
|
|
1222
|
+
- **Cause:** Service name not configured.
|
|
1223
|
+
- **Mitigation:** Always call `.withService()`.
|
|
1224
|
+
- **Solution:**
|
|
1225
|
+
```typescript
|
|
1226
|
+
IgniterJobs.create().withService("my-api")
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
### JOBS_CONTEXT_REQUIRED
|
|
1230
|
+
|
|
1231
|
+
- **Context:** `IgniterJobsBuilder.build()`
|
|
1232
|
+
- **Cause:** Context factory not configured.
|
|
1233
|
+
- **Mitigation:** Always call `.withContext()`.
|
|
1234
|
+
- **Solution:**
|
|
1235
|
+
```typescript
|
|
1236
|
+
IgniterJobs.create().withContext(async () => ({ db }))
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
### JOBS_CONFIGURATION_INVALID
|
|
1240
|
+
|
|
1241
|
+
- **Context:** Builder or runtime validation.
|
|
1242
|
+
- **Cause:** Invalid environment, scope conflicts, or invalid options.
|
|
1243
|
+
- **Mitigation:** Validate inputs and ensure scope consistency.
|
|
1244
|
+
- **Solution:** Check builder inputs, then retry.
|
|
1245
|
+
|
|
1246
|
+
### JOBS_QUEUE_NOT_FOUND
|
|
1247
|
+
|
|
1248
|
+
- **Context:** Worker builder or adapter dispatch.
|
|
1249
|
+
- **Cause:** Queue name not registered.
|
|
1250
|
+
- **Mitigation:** Ensure `.addQueue()` includes the queue.
|
|
1251
|
+
- **Solution:** Register the queue before building.
|
|
1252
|
+
|
|
1253
|
+
### JOBS_QUEUE_DUPLICATE
|
|
1254
|
+
|
|
1255
|
+
- **Context:** `IgniterJobsBuilder.addQueue()`
|
|
1256
|
+
- **Cause:** Duplicate queue name.
|
|
1257
|
+
- **Mitigation:** Use unique queue names.
|
|
1258
|
+
- **Solution:** Rename the queue.
|
|
1259
|
+
|
|
1260
|
+
### JOBS_QUEUE_OPERATION_FAILED
|
|
1261
|
+
|
|
1262
|
+
- **Context:** Adapter queue operations.
|
|
1263
|
+
- **Cause:** Unsupported operation (e.g., BullMQ job-type pause).
|
|
1264
|
+
- **Mitigation:** Use queue-level pause.
|
|
1265
|
+
- **Solution:** Pause the whole queue or adjust worker filters.
|
|
1266
|
+
|
|
1267
|
+
### JOBS_INVALID_DEFINITION
|
|
1268
|
+
|
|
1269
|
+
- **Context:** Queue builder `addJob()`
|
|
1270
|
+
- **Cause:** Invalid job definition object.
|
|
1271
|
+
- **Mitigation:** Ensure `handler` exists.
|
|
1272
|
+
- **Solution:** Provide a valid job definition.
|
|
1273
|
+
|
|
1274
|
+
### JOBS_HANDLER_REQUIRED
|
|
1275
|
+
|
|
1276
|
+
- **Context:** Queue builder `addJob()` or `addCron()`
|
|
1277
|
+
- **Cause:** Missing handler function.
|
|
1278
|
+
- **Mitigation:** Provide a valid handler.
|
|
1279
|
+
- **Solution:** Add `handler: async () => { ... }`.
|
|
1280
|
+
|
|
1281
|
+
### JOBS_DUPLICATE_JOB
|
|
1282
|
+
|
|
1283
|
+
- **Context:** Queue builder `addJob()`
|
|
1284
|
+
- **Cause:** Duplicate job name in same queue.
|
|
1285
|
+
- **Mitigation:** Use unique job names.
|
|
1286
|
+
- **Solution:** Rename the job.
|
|
1287
|
+
|
|
1288
|
+
### JOBS_NOT_FOUND
|
|
1289
|
+
|
|
1290
|
+
- **Context:** Job retrieval
|
|
1291
|
+
- **Cause:** Invalid job id or retention policy cleaned it.
|
|
1292
|
+
- **Mitigation:** Keep job ids and tune retention.
|
|
1293
|
+
- **Solution:** Re-dispatch or adjust retention settings.
|
|
1294
|
+
|
|
1295
|
+
### JOBS_NOT_REGISTERED
|
|
1296
|
+
|
|
1297
|
+
- **Context:** Worker execution
|
|
1298
|
+
- **Cause:** Worker executing a job that wasn’t registered.
|
|
1299
|
+
- **Mitigation:** Ensure build() ran and registration completed.
|
|
1300
|
+
- **Solution:** Restart worker with correct config.
|
|
1301
|
+
|
|
1302
|
+
### JOBS_EXECUTION_FAILED
|
|
1303
|
+
|
|
1304
|
+
- **Context:** Handler execution
|
|
1305
|
+
- **Cause:** Handler throws.
|
|
1306
|
+
- **Mitigation:** Add retries and guard logic.
|
|
1307
|
+
- **Solution:** Fix business logic or add `onFailure`.
|
|
1308
|
+
|
|
1309
|
+
### JOBS_TIMEOUT
|
|
1310
|
+
|
|
1311
|
+
- **Context:** Adapter-specific worker
|
|
1312
|
+
- **Cause:** Job exceeded timeout.
|
|
1313
|
+
- **Mitigation:** Reduce job scope or increase timeout in adapter.
|
|
1314
|
+
- **Solution:** Split job or adjust adapter settings.
|
|
1315
|
+
|
|
1316
|
+
### JOBS_CONTEXT_FACTORY_FAILED
|
|
1317
|
+
|
|
1318
|
+
- **Context:** Context factory
|
|
1319
|
+
- **Cause:** Factory threw an error.
|
|
1320
|
+
- **Mitigation:** Make factory resilient and guarded.
|
|
1321
|
+
- **Solution:** Wrap with try/catch and verify dependencies.
|
|
1322
|
+
|
|
1323
|
+
### JOBS_VALIDATION_FAILED
|
|
1324
|
+
|
|
1325
|
+
- **Context:** Dispatch or execution
|
|
1326
|
+
- **Cause:** Input schema mismatch.
|
|
1327
|
+
- **Mitigation:** Validate before dispatch.
|
|
1328
|
+
- **Solution:** Fix input shape or schema.
|
|
1329
|
+
|
|
1330
|
+
### JOBS_INVALID_INPUT
|
|
1331
|
+
|
|
1332
|
+
- **Context:** Runtime
|
|
1333
|
+
- **Cause:** Malformed data for job input.
|
|
1334
|
+
- **Mitigation:** Validate upstream.
|
|
1335
|
+
- **Solution:** Fix caller input.
|
|
1336
|
+
|
|
1337
|
+
### JOBS_INVALID_CRON
|
|
1338
|
+
|
|
1339
|
+
- **Context:** Queue builder `addCron()`
|
|
1340
|
+
- **Cause:** Invalid cron syntax or duplicate name.
|
|
1341
|
+
- **Mitigation:** Validate cron expressions.
|
|
1342
|
+
- **Solution:** Correct cron string.
|
|
1343
|
+
|
|
1344
|
+
### JOBS_INVALID_SCHEDULE
|
|
1345
|
+
|
|
1346
|
+
- **Context:** `schedule()`
|
|
1347
|
+
- **Cause:** Invalid scheduling params (e.g., past date).
|
|
1348
|
+
- **Mitigation:** Validate dates.
|
|
1349
|
+
- **Solution:** Use future date.
|
|
1350
|
+
|
|
1351
|
+
### JOBS_SCOPE_ALREADY_DEFINED
|
|
1352
|
+
|
|
1353
|
+
- **Context:** Builder `addScope()`
|
|
1354
|
+
- **Cause:** Multiple scopes defined.
|
|
1355
|
+
- **Mitigation:** Single scope only.
|
|
1356
|
+
- **Solution:** Remove extra scope.
|
|
1357
|
+
|
|
1358
|
+
### JOBS_WORKER_FAILED
|
|
1359
|
+
|
|
1360
|
+
- **Context:** Worker lifecycle
|
|
1361
|
+
- **Cause:** Adapter or worker error.
|
|
1362
|
+
- **Mitigation:** Monitor worker health.
|
|
1363
|
+
- **Solution:** Restart worker and verify backend.
|
|
1364
|
+
|
|
1365
|
+
### JOBS_ADAPTER_ERROR
|
|
1366
|
+
|
|
1367
|
+
- **Context:** Adapter operations
|
|
1368
|
+
- **Cause:** Backend failure.
|
|
1369
|
+
- **Mitigation:** Monitor backend health.
|
|
1370
|
+
- **Solution:** Retry or failover.
|
|
1371
|
+
|
|
1372
|
+
### JOBS_ADAPTER_CONNECTION_FAILED
|
|
1373
|
+
|
|
1374
|
+
- **Context:** Adapter connection
|
|
1375
|
+
- **Cause:** Redis or database unreachable.
|
|
1376
|
+
- **Mitigation:** Check network.
|
|
1377
|
+
- **Solution:** Restore connectivity.
|
|
1378
|
+
|
|
1379
|
+
### JOBS_SUBSCRIBE_FAILED
|
|
1380
|
+
|
|
1381
|
+
- **Context:** Event subscriptions
|
|
1382
|
+
- **Cause:** Pub/sub connection failure.
|
|
1383
|
+
- **Mitigation:** Reconnect on failures.
|
|
1384
|
+
- **Solution:** Restart subscriber or adapter.
|
|
1385
|
+
|
|
1386
|
+
---
|
|
1387
|
+
|
|
1388
|
+
## 📘 Appendix: Field-by-Field Reference
|
|
1389
|
+
|
|
1390
|
+
### IgniterJobsBuilder State (Conceptual)
|
|
1391
|
+
|
|
1392
|
+
- `adapter`
|
|
1393
|
+
- `service`
|
|
1394
|
+
- `environment`
|
|
1395
|
+
- `contextFactory`
|
|
1396
|
+
- `queues`
|
|
1397
|
+
- `scopeDefinition`
|
|
1398
|
+
- `queueDefaults`
|
|
1399
|
+
- `workerDefaults`
|
|
1400
|
+
- `autoStartWorker`
|
|
1401
|
+
- `logger`
|
|
1402
|
+
- `telemetry`
|
|
1403
|
+
|
|
1404
|
+
### IgniterJobDefinition Fields
|
|
1405
|
+
|
|
1406
|
+
- `input`
|
|
1407
|
+
- `output`
|
|
1408
|
+
- `queue`
|
|
1409
|
+
- `handler`
|
|
1410
|
+
- `onStart`
|
|
1411
|
+
- `onProgress`
|
|
1412
|
+
- `onSuccess`
|
|
1413
|
+
- `onFailure`
|
|
1414
|
+
- `jobId`
|
|
1415
|
+
- `priority`
|
|
1416
|
+
- `delay`
|
|
1417
|
+
- `attempts`
|
|
1418
|
+
- `removeOnComplete`
|
|
1419
|
+
- `removeOnFail`
|
|
1420
|
+
- `metadata`
|
|
1421
|
+
- `limiter`
|
|
1422
|
+
|
|
1423
|
+
### IgniterCronDefinition Fields
|
|
1424
|
+
|
|
1425
|
+
- `cron`
|
|
1426
|
+
- `tz`
|
|
1427
|
+
- `maxExecutions`
|
|
1428
|
+
- `skipWeekends`
|
|
1429
|
+
- `onlyBusinessHours`
|
|
1430
|
+
- `businessHours`
|
|
1431
|
+
- `onlyWeekdays`
|
|
1432
|
+
- `skipDates`
|
|
1433
|
+
- `startDate`
|
|
1434
|
+
- `endDate`
|
|
1435
|
+
- `handler`
|
|
1436
|
+
|
|
1437
|
+
### IgniterJobsScheduleOptions Fields
|
|
1438
|
+
|
|
1439
|
+
- `at`
|
|
1440
|
+
- `delay`
|
|
1441
|
+
- `cron`
|
|
1442
|
+
- `every`
|
|
1443
|
+
- `maxExecutions`
|
|
1444
|
+
- `tz`
|
|
1445
|
+
- `skipWeekends`
|
|
1446
|
+
- `businessHours`
|
|
1447
|
+
- `onlyBusinessHours`
|
|
1448
|
+
- `onlyWeekdays`
|
|
1449
|
+
- `skipDates`
|
|
1450
|
+
|
|
1451
|
+
### IgniterJobsExecutionContext Fields
|
|
1452
|
+
|
|
1453
|
+
- `input`
|
|
1454
|
+
- `context`
|
|
1455
|
+
- `job.id`
|
|
1456
|
+
- `job.name`
|
|
1457
|
+
- `job.queue`
|
|
1458
|
+
- `job.attemptsMade`
|
|
1459
|
+
- `job.createdAt`
|
|
1460
|
+
- `job.metadata`
|
|
1461
|
+
- `scope`
|
|
1462
|
+
|
|
1463
|
+
### IgniterJobsHookContext Fields
|
|
1464
|
+
|
|
1465
|
+
- `startedAt`
|
|
1466
|
+
- `duration`
|
|
1467
|
+
|
|
1468
|
+
### IgniterJobsQueueInfo Fields
|
|
1469
|
+
|
|
1470
|
+
- `name`
|
|
1471
|
+
- `isPaused`
|
|
1472
|
+
- `jobCounts.waiting`
|
|
1473
|
+
- `jobCounts.active`
|
|
1474
|
+
- `jobCounts.completed`
|
|
1475
|
+
- `jobCounts.failed`
|
|
1476
|
+
- `jobCounts.delayed`
|
|
1477
|
+
- `jobCounts.paused`
|
|
1478
|
+
|
|
1479
|
+
### IgniterJobSearchResult Fields
|
|
1480
|
+
|
|
1481
|
+
- `id`
|
|
1482
|
+
- `name`
|
|
1483
|
+
- `queue`
|
|
1484
|
+
- `status`
|
|
1485
|
+
- `input`
|
|
1486
|
+
- `result`
|
|
1487
|
+
- `error`
|
|
1488
|
+
- `progress`
|
|
1489
|
+
- `attemptsMade`
|
|
1490
|
+
- `priority`
|
|
1491
|
+
- `createdAt`
|
|
1492
|
+
- `startedAt`
|
|
1493
|
+
- `completedAt`
|
|
1494
|
+
- `metadata`
|
|
1495
|
+
- `scope`
|
|
1496
|
+
|
|
1497
|
+
### IgniterJobsWorkerMetrics Fields
|
|
1498
|
+
|
|
1499
|
+
- `processed`
|
|
1500
|
+
- `failed`
|
|
1501
|
+
- `avgDuration`
|
|
1502
|
+
- `concurrency`
|
|
1503
|
+
- `uptime`
|
|
1504
|
+
|
|
1505
|
+
### IgniterJobsWorkerHandle Fields
|
|
1506
|
+
|
|
1507
|
+
- `id`
|
|
1508
|
+
- `queues`
|
|
1509
|
+
- `pause()`
|
|
1510
|
+
- `resume()`
|
|
1511
|
+
- `close()`
|
|
1512
|
+
- `isRunning()`
|
|
1513
|
+
- `isPaused()`
|
|
1514
|
+
- `isClosed()`
|
|
1515
|
+
- `getMetrics()`
|
|
1516
|
+
|
|
1517
|
+
### IgniterJobsAdapter Methods
|
|
1518
|
+
|
|
1519
|
+
- `dispatch()`
|
|
1520
|
+
- `schedule()`
|
|
1521
|
+
- `getJob()`
|
|
1522
|
+
- `getJobState()`
|
|
1523
|
+
- `getJobLogs()`
|
|
1524
|
+
- `getJobProgress()`
|
|
1525
|
+
- `retryJob()`
|
|
1526
|
+
- `removeJob()`
|
|
1527
|
+
- `promoteJob()`
|
|
1528
|
+
- `moveJobToFailed()`
|
|
1529
|
+
- `retryManyJobs()`
|
|
1530
|
+
- `removeManyJobs()`
|
|
1531
|
+
- `getQueueInfo()`
|
|
1532
|
+
- `getQueueJobCounts()`
|
|
1533
|
+
- `listQueues()`
|
|
1534
|
+
- `pauseQueue()`
|
|
1535
|
+
- `resumeQueue()`
|
|
1536
|
+
- `drainQueue()`
|
|
1537
|
+
- `cleanQueue()`
|
|
1538
|
+
- `obliterateQueue()`
|
|
1539
|
+
- `retryAllInQueue()`
|
|
1540
|
+
- `pauseJobType()`
|
|
1541
|
+
- `resumeJobType()`
|
|
1542
|
+
- `searchJobs()`
|
|
1543
|
+
- `searchQueues()`
|
|
1544
|
+
- `searchWorkers()`
|
|
1545
|
+
- `createWorker()`
|
|
1546
|
+
- `getWorkers()`
|
|
1547
|
+
- `publishEvent()`
|
|
1548
|
+
- `subscribeEvent()`
|
|
1549
|
+
- `registerJob()`
|
|
1550
|
+
- `registerCron()`
|
|
1551
|
+
- `shutdown()`
|
|
1552
|
+
|
|
1553
|
+
### IgniterJobsEvent Fields
|
|
1554
|
+
|
|
1555
|
+
- `type`
|
|
1556
|
+
- `data`
|
|
1557
|
+
- `timestamp`
|
|
1558
|
+
- `scope`
|
|
1559
|
+
|
|
1560
|
+
### IgniterJobsScopeEntry Fields
|
|
1561
|
+
|
|
1562
|
+
- `type`
|
|
1563
|
+
- `id`
|
|
1564
|
+
- `tags`
|
|
1565
|
+
|
|
1566
|
+
---
|
|
1567
|
+
|
|
1568
|
+
## 🌍 Real-World Examples
|
|
1569
|
+
|
|
1570
|
+
### 1) E-commerce: Order Expiry
|
|
1571
|
+
|
|
1572
|
+
Cancel unpaid orders after 1 hour.
|
|
1573
|
+
|
|
1574
|
+
```typescript
|
|
1575
|
+
const ordersQueue = IgniterQueue.create("orders")
|
|
1576
|
+
.addJob("cancelUnpaid", {
|
|
1577
|
+
input: z.object({ orderId: z.string() }),
|
|
1578
|
+
handler: async ({ input, context }) => {
|
|
1579
|
+
await context.orders.cancelIfUnpaid(input.orderId);
|
|
1580
|
+
},
|
|
1581
|
+
})
|
|
1582
|
+
.build();
|
|
1583
|
+
|
|
1584
|
+
await jobs.orders.cancelUnpaid.schedule({
|
|
1585
|
+
input: { orderId: "ord_123" },
|
|
1586
|
+
delay: 60 * 60 * 1000,
|
|
1587
|
+
});
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
### 2) Fintech: Nightly Reconciliation
|
|
1591
|
+
|
|
1592
|
+
Batch reconcile bank transactions every night.
|
|
1593
|
+
|
|
1594
|
+
```typescript
|
|
1595
|
+
const reconQueue = IgniterQueue.create("reconciliation")
|
|
1596
|
+
.addCron("nightly", {
|
|
1597
|
+
cron: "0 3 * * *",
|
|
1598
|
+
handler: async ({ context }) => {
|
|
1599
|
+
await context.reconcile.runNightly();
|
|
1600
|
+
},
|
|
1601
|
+
})
|
|
1602
|
+
.build();
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
### 3) SaaS: CSV Import with Progress
|
|
1606
|
+
|
|
1607
|
+
```typescript
|
|
1608
|
+
const importQueue = IgniterQueue.create("imports")
|
|
1609
|
+
.addJob("csv", {
|
|
1610
|
+
input: z.object({ fileId: z.string() }),
|
|
1611
|
+
handler: async ({ input, context }) => {
|
|
1612
|
+
const rows = await context.files.readCsv(input.fileId);
|
|
1613
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1614
|
+
await context.imports.processRow(rows[i]);
|
|
1615
|
+
await context.imports.progress(i / rows.length);
|
|
1616
|
+
}
|
|
1617
|
+
return { processed: rows.length };
|
|
1618
|
+
},
|
|
1619
|
+
})
|
|
1620
|
+
.build();
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
### 4) Media Platform: Video Transcoding
|
|
1624
|
+
|
|
1625
|
+
```typescript
|
|
1626
|
+
const mediaQueue = IgniterQueue.create("media")
|
|
1627
|
+
.addJob("transcode", {
|
|
1628
|
+
input: z.object({ assetId: z.string(), preset: z.string() }),
|
|
1629
|
+
handler: async ({ input, context }) => {
|
|
1630
|
+
await context.media.transcode(input.assetId, input.preset);
|
|
1631
|
+
},
|
|
1632
|
+
})
|
|
1633
|
+
.build();
|
|
1634
|
+
|
|
1635
|
+
await jobs.media.transcode.dispatch({
|
|
1636
|
+
input: { assetId: "vid_1", preset: "1080p" },
|
|
1637
|
+
priority: 10,
|
|
1638
|
+
});
|
|
1639
|
+
```
|
|
1640
|
+
|
|
1641
|
+
### 5) Healthcare: Appointment Reminders
|
|
1642
|
+
|
|
1643
|
+
```typescript
|
|
1644
|
+
const remindersQueue = IgniterQueue.create("reminders")
|
|
1645
|
+
.addJob("appointment", {
|
|
1646
|
+
input: z.object({ appointmentId: z.string(), at: z.string() }),
|
|
1647
|
+
handler: async ({ input, context }) => {
|
|
1648
|
+
await context.reminders.sendAppointment(input.appointmentId);
|
|
1649
|
+
},
|
|
1650
|
+
})
|
|
1651
|
+
.build();
|
|
1652
|
+
|
|
1653
|
+
await jobs.reminders.appointment.schedule({
|
|
1654
|
+
input: { appointmentId: "apt_1", at: "2026-02-10T10:00:00Z" },
|
|
1655
|
+
at: new Date("2026-02-09T10:00:00Z"),
|
|
1656
|
+
});
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
### 6) Marketplace: Fraud Review Queue
|
|
1660
|
+
|
|
1661
|
+
```typescript
|
|
1662
|
+
const fraudQueue = IgniterQueue.create("fraud")
|
|
1663
|
+
.addJob("review", {
|
|
1664
|
+
input: z.object({ transactionId: z.string() }),
|
|
1665
|
+
handler: async ({ input, context }) => {
|
|
1666
|
+
await context.risk.reviewTransaction(input.transactionId);
|
|
1667
|
+
},
|
|
1668
|
+
})
|
|
1669
|
+
.build();
|
|
1670
|
+
|
|
1671
|
+
await jobs.fraud.review.dispatch({
|
|
1672
|
+
input: { transactionId: "txn_99" },
|
|
1673
|
+
priority: 100,
|
|
1674
|
+
});
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
### 7) DevOps: Cleanup Jobs
|
|
1678
|
+
|
|
1679
|
+
```typescript
|
|
1680
|
+
const cleanupQueue = IgniterQueue.create("maintenance")
|
|
1681
|
+
.addCron("cleanupUploads", {
|
|
1682
|
+
cron: "0 4 * * 0",
|
|
1683
|
+
handler: async ({ context }) => {
|
|
1684
|
+
await context.storage.cleanupOrphans();
|
|
1685
|
+
},
|
|
1686
|
+
})
|
|
1687
|
+
.build();
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
---
|
|
1691
|
+
|
|
1692
|
+
## 📚 API Reference
|
|
1693
|
+
|
|
1694
|
+
### IgniterJobs (Factory)
|
|
1695
|
+
|
|
1696
|
+
```typescript
|
|
1697
|
+
export const IgniterJobs: {
|
|
1698
|
+
create: () => IgniterJobsBuilder<unknown>;
|
|
1699
|
+
};
|
|
1700
|
+
```
|
|
1701
|
+
|
|
1702
|
+
### IgniterJobsBuilder
|
|
1703
|
+
|
|
1704
|
+
```typescript
|
|
1705
|
+
class IgniterJobsBuilder<TContext, TQueues, TScope> {
|
|
1706
|
+
static create(): IgniterJobsBuilder<unknown>;
|
|
1707
|
+
|
|
1708
|
+
withAdapter(adapter: IgniterJobsAdapter): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1709
|
+
withService(service: string): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1710
|
+
withEnvironment(environment: string): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1711
|
+
withContext<TNewContext>(factory: () => TNewContext | Promise<TNewContext>): IgniterJobsBuilder<TNewContext, {}, TScope>;
|
|
1712
|
+
addScope(name: string, options?: IgniterJobsScopeOptions): IgniterJobsBuilder<TContext, TQueues, TScope | string>;
|
|
1713
|
+
addQueue(queue: IgniterJobsQueue<TContext, any, any> & { name: string }): IgniterJobsBuilder<TContext, TQueues & Record<string, any>, TScope>;
|
|
1714
|
+
withQueueDefaults(defaults: Partial<IgniterJobDefinition<TContext, any, any>>): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1715
|
+
withWorkerDefaults(defaults: Partial<IgniterJobsWorkerBuilderConfig>): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1716
|
+
withAutoStartWorker(config: { queues: (keyof TQueues)[]; concurrency?: number; limiter?: IgniterJobsLimiter }): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1717
|
+
withTelemetry(telemetry: IgniterJobsTelemetry): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1718
|
+
withLogger(logger: IgniterLogger): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1719
|
+
build(): IgniterJobsRuntime<IgniterJobsConfig<TContext, TQueues, TScope>>;
|
|
1720
|
+
}
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
> Note: `queueDefaults`, `workerDefaults`, and `autoStartWorker` are stored in the runtime configuration.
|
|
1724
|
+
|
|
1725
|
+
### IgniterQueue
|
|
1726
|
+
|
|
1727
|
+
```typescript
|
|
1728
|
+
class IgniterQueue {
|
|
1729
|
+
static create<const TName extends string>(name: TName): IgniterQueueBuilder<unknown, {}, {}, TName>;
|
|
1730
|
+
}
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
### IgniterQueueBuilder
|
|
1734
|
+
|
|
1735
|
+
```typescript
|
|
1736
|
+
class IgniterQueueBuilder<TContext, TJobs, TCron, TName> {
|
|
1737
|
+
addJob<TJobName extends string, TInput, TResult>(
|
|
1738
|
+
jobName: TJobName,
|
|
1739
|
+
definition: IgniterJobDefinition<TContext, TInput, TResult>
|
|
1740
|
+
): IgniterQueueBuilder<TContext, TJobs & Record<TJobName, IgniterJobDefinition<TContext, TInput, TResult>>, TCron, TName>;
|
|
1741
|
+
|
|
1742
|
+
addCron<TCronName extends string, TResult>(
|
|
1743
|
+
cronName: TCronName,
|
|
1744
|
+
definition: IgniterCronDefinition<TContext, TResult>
|
|
1745
|
+
): IgniterQueueBuilder<TContext, TJobs, TCron & Record<TCronName, IgniterCronDefinition<TContext, TResult>>, TName>;
|
|
1746
|
+
|
|
1747
|
+
build(): IgniterJobsQueue<TContext, TJobs, TCron> & { name: TName };
|
|
1748
|
+
}
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
### Runtime Methods
|
|
1752
|
+
|
|
1753
|
+
```typescript
|
|
1754
|
+
interface IgniterJobsRuntime<TConfig> {
|
|
1755
|
+
config: TConfig;
|
|
1756
|
+
subscribe(handler: IgniterJobsEventHandler): Promise<() => Promise<void>>;
|
|
1757
|
+
search(target: "jobs" | "queues" | "workers", filter: Record<string, unknown>): Promise<unknown[]>;
|
|
1758
|
+
shutdown(): Promise<void>;
|
|
1759
|
+
worker: { create(): IgniterWorkerBuilder<keyof TConfig["queues"] & string> };
|
|
1760
|
+
scope(type: string, id: string | number, tags?: Record<string, unknown>): IgniterJobsRuntime<TConfig>;
|
|
1761
|
+
|
|
1762
|
+
// Queue accessors (dynamic)
|
|
1763
|
+
[queueName: string]: IgniterJobsQueueAccessor<any>;
|
|
1764
|
+
}
|
|
1765
|
+
```
|
|
1766
|
+
|
|
1767
|
+
### Queue Accessor
|
|
1768
|
+
|
|
1769
|
+
```typescript
|
|
1770
|
+
interface IgniterJobsQueueAccessor {
|
|
1771
|
+
get(): {
|
|
1772
|
+
retrieve(): Promise<IgniterJobsQueueInfo | null>;
|
|
1773
|
+
pause(): Promise<void>;
|
|
1774
|
+
resume(): Promise<void>;
|
|
1775
|
+
drain(): Promise<number>;
|
|
1776
|
+
clean(options: IgniterJobsQueueCleanOptions): Promise<number>;
|
|
1777
|
+
obliterate(options?: { force?: boolean }): Promise<void>;
|
|
1778
|
+
retryAll(): Promise<number>;
|
|
1779
|
+
};
|
|
1780
|
+
list(filter?: { status?: IgniterJobStatus[]; limit?: number; offset?: number }): Promise<IgniterJobSearchResult[]>;
|
|
1781
|
+
subscribe(handler: IgniterJobsEventHandler): Promise<() => Promise<void>>;
|
|
1782
|
+
jobs: Record<string, IgniterJobsJobAccessor>;
|
|
1783
|
+
}
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
### Job Accessor
|
|
1787
|
+
|
|
1788
|
+
```typescript
|
|
1789
|
+
interface IgniterJobsJobAccessor<TInput = unknown> {
|
|
1790
|
+
dispatch(params: IgniterJobsDispatchParams<TInput>): Promise<string>;
|
|
1791
|
+
schedule(params: IgniterJobsScheduleParams<TInput>): Promise<string>;
|
|
1792
|
+
get(id: string): {
|
|
1793
|
+
retrieve(): Promise<IgniterJobSearchResult | null>;
|
|
1794
|
+
retry(): Promise<void>;
|
|
1795
|
+
remove(): Promise<void>;
|
|
1796
|
+
promote(): Promise<void>;
|
|
1797
|
+
move(state: "failed", reason: string): Promise<void>;
|
|
1798
|
+
state(): Promise<IgniterJobStatus | null>;
|
|
1799
|
+
progress(): Promise<number>;
|
|
1800
|
+
logs(): Promise<IgniterJobsJobLog[]>;
|
|
1801
|
+
};
|
|
1802
|
+
many(ids: string[]): { retry(): Promise<void>; remove(): Promise<void> };
|
|
1803
|
+
pause(): Promise<void>;
|
|
1804
|
+
resume(): Promise<void>;
|
|
1805
|
+
subscribe(handler: IgniterJobsEventHandler): Promise<() => Promise<void>>;
|
|
1806
|
+
}
|
|
1807
|
+
```
|
|
1808
|
+
|
|
1809
|
+
### Worker Builder
|
|
1810
|
+
|
|
1811
|
+
```typescript
|
|
1812
|
+
interface IgniterJobsWorkerFluentBuilder<TQueueNames extends string> {
|
|
1813
|
+
addQueue(queue: TQueueNames): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1814
|
+
withConcurrency(concurrency: number): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1815
|
+
withLimiter(limiter: IgniterJobsLimiter): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1816
|
+
onActive(handler: (ctx: { job: IgniterJobSearchResult }) => void | Promise<void>): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1817
|
+
onSuccess(handler: (ctx: { job: IgniterJobSearchResult; result: unknown }) => void | Promise<void>): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1818
|
+
onFailure(handler: (ctx: { job: IgniterJobSearchResult; error: Error }) => void | Promise<void>): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1819
|
+
onIdle(handler: () => void | Promise<void>): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1820
|
+
start(): Promise<IgniterJobsWorkerHandle>;
|
|
1821
|
+
}
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
---
|
|
1825
|
+
|
|
1826
|
+
## ⚙️ Configuration Reference
|
|
1827
|
+
|
|
1828
|
+
### IgniterJobsSQLiteAdapterOptions
|
|
1829
|
+
|
|
1830
|
+
```typescript
|
|
1831
|
+
interface IgniterJobsSQLiteAdapterOptions {
|
|
1832
|
+
path: string;
|
|
1833
|
+
pollingInterval?: number; // default: 500
|
|
1834
|
+
enableWAL?: boolean; // default: true
|
|
1835
|
+
}
|
|
1836
|
+
```
|
|
1837
|
+
|
|
1838
|
+
### IgniterJobsScheduleOptions
|
|
1839
|
+
|
|
1840
|
+
```typescript
|
|
1841
|
+
interface IgniterJobsScheduleOptions {
|
|
1842
|
+
at?: Date;
|
|
1843
|
+
delay?: number;
|
|
1844
|
+
cron?: string;
|
|
1845
|
+
every?: number;
|
|
1846
|
+
maxExecutions?: number;
|
|
1847
|
+
tz?: string;
|
|
1848
|
+
skipWeekends?: boolean;
|
|
1849
|
+
businessHours?: { start: number; end: number; timezone?: string };
|
|
1850
|
+
onlyBusinessHours?: boolean;
|
|
1851
|
+
onlyWeekdays?: number[];
|
|
1852
|
+
skipDates?: Array<string | Date>;
|
|
1853
|
+
}
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
---
|
|
1857
|
+
|
|
1858
|
+
## ✅ Best Practices
|
|
1859
|
+
|
|
1860
|
+
| Do | Why | Example |
|
|
1861
|
+
|----|-----|---------|
|
|
1862
|
+
| ✅ Use input schemas | Prevent invalid jobs | `input: z.object({ ... })` |
|
|
1863
|
+
| ✅ Keep payloads small | Faster serialization | `{ id: "order_1" }` |
|
|
1864
|
+
| ✅ Use scopes | Tenant isolation | `jobs.scope("org", "org_1")` |
|
|
1865
|
+
| ✅ Use retries | Resilience | `attempts: 5` |
|
|
1866
|
+
| ✅ Use worker hooks | Observability | `onFailure(...)` |
|
|
1867
|
+
|
|
1868
|
+
### Anti-Patterns
|
|
1869
|
+
|
|
1870
|
+
| Don’t | Why | Alternative |
|
|
1871
|
+
|------|-----|-------------|
|
|
1872
|
+
| ❌ Store PII in metadata | Metadata is observable | Store IDs only |
|
|
1873
|
+
| ❌ Use sync I/O in handlers | Blocks workers | Use async I/O |
|
|
1874
|
+
| ❌ Dispatch without schema | Runtime surprises | Add `input` schema |
|
|
1875
|
+
| ❌ Long-running jobs without progress | No visibility | Use `onProgress` |
|
|
1876
|
+
|
|
1877
|
+
---
|
|
1878
|
+
|
|
1879
|
+
## 🧯 Troubleshooting
|
|
1880
|
+
|
|
1881
|
+
### JOBS_ADAPTER_REQUIRED
|
|
1882
|
+
|
|
1883
|
+
**Cause:** No adapter configured.
|
|
1884
|
+
**Fix:** Call `.withAdapter(...)`.
|
|
1885
|
+
|
|
1886
|
+
### JOBS_SERVICE_REQUIRED
|
|
1887
|
+
|
|
1888
|
+
**Cause:** Missing service name.
|
|
1889
|
+
**Fix:** Call `.withService("my-service")`.
|
|
1890
|
+
|
|
1891
|
+
### JOBS_CONTEXT_REQUIRED
|
|
1892
|
+
|
|
1893
|
+
**Cause:** Missing context factory.
|
|
1894
|
+
**Fix:** Call `.withContext(() => ({ ... }))`.
|
|
1895
|
+
|
|
1896
|
+
### JOBS_INVALID_SCHEDULE
|
|
1897
|
+
|
|
1898
|
+
**Cause:** `at` time is in the past.
|
|
1899
|
+
**Fix:** Use a future date.
|
|
1900
|
+
|
|
1901
|
+
### JOBS_QUEUE_OPERATION_FAILED
|
|
1902
|
+
|
|
1903
|
+
**Cause:** BullMQ adapter cannot pause a single job type.
|
|
1904
|
+
**Fix:** Pause the entire queue or filter queues per worker.
|
|
1905
|
+
|
|
1906
|
+
---
|
|
1907
|
+
|
|
1908
|
+
## 🧩 Framework Integration
|
|
1909
|
+
|
|
1910
|
+
### Next.js API Route
|
|
1911
|
+
|
|
1912
|
+
```typescript
|
|
1913
|
+
// app/api/queue/route.ts
|
|
1914
|
+
import { NextResponse } from "next/server";
|
|
1915
|
+
import { jobs } from "@/lib/jobs";
|
|
1916
|
+
|
|
1917
|
+
export async function POST() {
|
|
1918
|
+
const id = await jobs.email.sendWelcome.dispatch({
|
|
1919
|
+
input: { email: "user@example.com" },
|
|
1920
|
+
});
|
|
1921
|
+
return NextResponse.json({ jobId: id });
|
|
1922
|
+
}
|
|
1923
|
+
```
|
|
1924
|
+
|
|
1925
|
+
### Express
|
|
1926
|
+
|
|
1927
|
+
```typescript
|
|
1928
|
+
import express from "express";
|
|
1929
|
+
import { jobs } from "./jobs";
|
|
1930
|
+
|
|
1931
|
+
const app = express();
|
|
1932
|
+
app.post("/send", async (_req, res) => {
|
|
1933
|
+
const jobId = await jobs.email.sendWelcome.dispatch({
|
|
1934
|
+
input: { email: "user@example.com" },
|
|
1935
|
+
});
|
|
1936
|
+
res.json({ jobId });
|
|
1937
|
+
});
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
### Fastify
|
|
1941
|
+
|
|
1942
|
+
```typescript
|
|
1943
|
+
import Fastify from "fastify";
|
|
1944
|
+
import { jobs } from "./jobs";
|
|
1945
|
+
|
|
1946
|
+
const app = Fastify();
|
|
1947
|
+
app.post("/send", async (_req, res) => {
|
|
1948
|
+
const jobId = await jobs.email.sendWelcome.dispatch({
|
|
1949
|
+
input: { email: "user@example.com" },
|
|
1950
|
+
});
|
|
1951
|
+
return res.send({ jobId });
|
|
1952
|
+
});
|
|
1953
|
+
```
|
|
1954
|
+
|
|
1955
|
+
---
|
|
1956
|
+
|
|
1957
|
+
## 🔐 Server-Only Safety
|
|
1958
|
+
|
|
1959
|
+
`@igniter-js/jobs` is server-only. Browser builds resolve to a shim that throws an explicit error.
|
|
1960
|
+
Do not import this package in client-side bundles.
|
|
1961
|
+
|
|
1962
|
+
---
|
|
1963
|
+
|
|
1964
|
+
## 🧭 Migration Guides
|
|
1965
|
+
|
|
1966
|
+
### Memory → SQLite
|
|
1967
|
+
|
|
1968
|
+
```typescript
|
|
1969
|
+
import { IgniterJobsSQLiteAdapter } from "@igniter-js/jobs/adapters";
|
|
1970
|
+
|
|
1971
|
+
const adapter = IgniterJobsSQLiteAdapter.create({
|
|
1972
|
+
path: "./jobs.sqlite",
|
|
1973
|
+
});
|
|
1974
|
+
```
|
|
1975
|
+
|
|
1976
|
+
### SQLite → BullMQ
|
|
1977
|
+
|
|
1978
|
+
```typescript
|
|
1979
|
+
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters";
|
|
1980
|
+
import Redis from "ioredis";
|
|
1981
|
+
|
|
1982
|
+
const adapter = IgniterJobsBullMQAdapter.create({
|
|
1983
|
+
redis: new Redis(process.env.REDIS_URL),
|
|
1984
|
+
});
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
---
|
|
1988
|
+
|
|
1989
|
+
## ❓ FAQ
|
|
1990
|
+
|
|
1991
|
+
### Which adapter should I use?
|
|
1992
|
+
|
|
1993
|
+
- **Memory** — Unit tests and local development.
|
|
1994
|
+
- **SQLite** — CLI tools, desktop apps, local environments.
|
|
1995
|
+
- **BullMQ** — Production and distributed workers.
|
|
1996
|
+
|
|
1997
|
+
### Does `@igniter-js/jobs` require Redis?
|
|
1998
|
+
|
|
1999
|
+
No. Redis is only required for the BullMQ adapter.
|
|
2000
|
+
|
|
2001
|
+
### Can I use multiple queues in one runtime?
|
|
2002
|
+
|
|
2003
|
+
Yes. Add as many queues as you need via `.addQueue(...)`.
|
|
2004
|
+
|
|
2005
|
+
### Can I switch adapters later?
|
|
2006
|
+
|
|
2007
|
+
Yes. The adapter interface is stable and jobs/queues remain unchanged.
|
|
2008
|
+
|
|
2009
|
+
---
|
|
2010
|
+
|
|
2011
|
+
## 🧪 Full Example (SQLite + Worker)
|
|
2012
|
+
|
|
2013
|
+
```typescript
|
|
2014
|
+
import { IgniterJobs, IgniterQueue } from "@igniter-js/jobs";
|
|
2015
|
+
import { IgniterJobsSQLiteAdapter } from "@igniter-js/jobs/adapters";
|
|
2016
|
+
import { z } from "zod";
|
|
2017
|
+
|
|
2018
|
+
type AppContext = { uploads: { process: (id: string) => Promise<void> } };
|
|
2019
|
+
|
|
2020
|
+
const queue = IgniterQueue.create("uploads")
|
|
2021
|
+
.addJob("process", {
|
|
2022
|
+
input: z.object({ id: z.string() }),
|
|
2023
|
+
handler: async ({ input, context }) => {
|
|
2024
|
+
await context.uploads.process(input.id);
|
|
2025
|
+
},
|
|
2026
|
+
})
|
|
2027
|
+
.build();
|
|
2028
|
+
|
|
2029
|
+
const jobs = IgniterJobs.create()
|
|
2030
|
+
.withAdapter(IgniterJobsSQLiteAdapter.create({ path: "./jobs.sqlite" }))
|
|
2031
|
+
.withService("uploader")
|
|
2032
|
+
.withEnvironment("local")
|
|
2033
|
+
.withContext(async () => ({ uploads }))
|
|
2034
|
+
.addQueue(queue)
|
|
2035
|
+
.build();
|
|
2036
|
+
|
|
2037
|
+
const worker = await jobs.worker.create().addQueue("uploads").start();
|
|
2038
|
+
|
|
2039
|
+
const id = await jobs.uploads.process.dispatch({ input: { id: "file_1" } });
|
|
2040
|
+
console.log("dispatched", id);
|
|
2041
|
+
|
|
2042
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2043
|
+
|
|
2044
|
+
const job = await jobs.uploads.process.get(id).retrieve();
|
|
2045
|
+
console.log(job?.status);
|
|
2046
|
+
|
|
2047
|
+
await worker.close();
|
|
2048
|
+
await jobs.shutdown();
|
|
2049
|
+
```
|
|
2050
|
+
|
|
2051
|
+
---
|
|
2052
|
+
|
|
2053
|
+
## 📑 Appendix: Event Matrix
|
|
2054
|
+
|
|
2055
|
+
### Job Events (Runtime)
|
|
2056
|
+
|
|
2057
|
+
| Event | When | Payload Keys |
|
|
2058
|
+
|------|------|--------------|
|
|
2059
|
+
| `enqueued` | After dispatch | `jobId`, `queue`, `jobName` |
|
|
2060
|
+
| `scheduled` | After schedule | `jobId`, `queue`, `jobName` |
|
|
2061
|
+
| `started` | Before handler | `jobId`, `jobName`, `queue`, `attemptsMade`, `startedAt` |
|
|
2062
|
+
| `completed` | After handler | `jobId`, `jobName`, `queue`, `result`, `duration`, `completedAt` |
|
|
2063
|
+
| `failed` | On error | `jobId`, `jobName`, `queue`, `error`, `attemptsMade`, `isFinalAttempt`, `duration`, `failedAt` |
|
|
2064
|
+
| `progress` | On progress hook | `jobId`, `jobName`, `queue`, `progress`, `message`, `timestamp` |
|
|
2065
|
+
|
|
2066
|
+
### Telemetry Event Attributes
|
|
2067
|
+
|
|
2068
|
+
#### Job Group
|
|
2069
|
+
|
|
2070
|
+
- `ctx.job.id`
|
|
2071
|
+
- `ctx.job.name`
|
|
2072
|
+
- `ctx.job.queue`
|
|
2073
|
+
- `ctx.job.priority`
|
|
2074
|
+
- `ctx.job.delay`
|
|
2075
|
+
- `ctx.job.attempt`
|
|
2076
|
+
- `ctx.job.maxAttempts`
|
|
2077
|
+
- `ctx.job.duration`
|
|
2078
|
+
- `ctx.job.error.message`
|
|
2079
|
+
- `ctx.job.error.code`
|
|
2080
|
+
- `ctx.job.isFinalAttempt`
|
|
2081
|
+
- `ctx.job.progress`
|
|
2082
|
+
- `ctx.job.progress.message`
|
|
2083
|
+
- `ctx.job.scheduledAt`
|
|
2084
|
+
- `ctx.job.cron`
|
|
2085
|
+
|
|
2086
|
+
#### Worker Group
|
|
2087
|
+
|
|
2088
|
+
- `ctx.worker.id`
|
|
2089
|
+
- `ctx.worker.queues`
|
|
2090
|
+
- `ctx.worker.concurrency`
|
|
2091
|
+
- `ctx.worker.processed`
|
|
2092
|
+
- `ctx.worker.failed`
|
|
2093
|
+
- `ctx.worker.uptime`
|
|
2094
|
+
|
|
2095
|
+
#### Queue Group
|
|
2096
|
+
|
|
2097
|
+
- `ctx.queue.name`
|
|
2098
|
+
- `ctx.queue.drained.count`
|
|
2099
|
+
- `ctx.queue.cleaned.count`
|
|
2100
|
+
- `ctx.queue.cleaned.status`
|
|
2101
|
+
- `ctx.queue.obliterated.force`
|
|
2102
|
+
|
|
2103
|
+
---
|
|
2104
|
+
|
|
2105
|
+
## 📑 Appendix: Adapter API Matrix
|
|
2106
|
+
|
|
2107
|
+
| API | Memory | SQLite | BullMQ |
|
|
2108
|
+
|-----|--------|--------|--------|
|
|
2109
|
+
| `dispatch()` | ✅ | ✅ | ✅ |
|
|
2110
|
+
| `schedule()` | ✅ | ✅ | ✅ |
|
|
2111
|
+
| `getJob()` | ✅ | ✅ | ✅ |
|
|
2112
|
+
| `getJobState()` | ✅ | ✅ | ✅ |
|
|
2113
|
+
| `getJobLogs()` | ✅ | ✅ | ✅ |
|
|
2114
|
+
| `getJobProgress()` | ✅ | ✅ | ✅ |
|
|
2115
|
+
| `pauseJobType()` | ✅ | ✅ | ❌ |
|
|
2116
|
+
| `resumeJobType()` | ✅ | ✅ | ❌ |
|
|
2117
|
+
| `pauseQueue()` | ✅ | ✅ | ✅ |
|
|
2118
|
+
| `resumeQueue()` | ✅ | ✅ | ✅ |
|
|
2119
|
+
| `drainQueue()` | ✅ | ✅ | ✅ |
|
|
2120
|
+
| `cleanQueue()` | ✅ | ✅ | ✅ |
|
|
2121
|
+
| `obliterateQueue()` | ✅ | ✅ | ✅ |
|
|
2122
|
+
| `retryAllInQueue()` | ✅ | ✅ | ✅ |
|
|
2123
|
+
|
|
2124
|
+
---
|
|
2125
|
+
|
|
2126
|
+
## 📑 Appendix: Method-by-Method Examples
|
|
2127
|
+
|
|
2128
|
+
### Adapter `dispatch()`
|
|
2129
|
+
|
|
2130
|
+
```typescript
|
|
2131
|
+
await adapter.dispatch({
|
|
2132
|
+
queue: "email",
|
|
2133
|
+
jobName: "send",
|
|
2134
|
+
input: { id: "1" },
|
|
2135
|
+
priority: 10,
|
|
2136
|
+
});
|
|
2137
|
+
```
|
|
2138
|
+
|
|
2139
|
+
### Adapter `schedule()`
|
|
2140
|
+
|
|
2141
|
+
```typescript
|
|
2142
|
+
await adapter.schedule({
|
|
2143
|
+
queue: "email",
|
|
2144
|
+
jobName: "send",
|
|
2145
|
+
input: { id: "1" },
|
|
2146
|
+
delay: 5_000,
|
|
2147
|
+
});
|
|
2148
|
+
```
|
|
2149
|
+
|
|
2150
|
+
### Adapter `getQueueInfo()`
|
|
2151
|
+
|
|
2152
|
+
```typescript
|
|
2153
|
+
const info = await adapter.getQueueInfo("email");
|
|
2154
|
+
```
|
|
2155
|
+
|
|
2156
|
+
### Adapter `getQueueJobCounts()`
|
|
2157
|
+
|
|
2158
|
+
```typescript
|
|
2159
|
+
const counts = await adapter.getQueueJobCounts("email");
|
|
2160
|
+
```
|
|
2161
|
+
|
|
2162
|
+
### Adapter `pauseQueue()`
|
|
2163
|
+
|
|
2164
|
+
```typescript
|
|
2165
|
+
await adapter.pauseQueue("email");
|
|
2166
|
+
```
|
|
2167
|
+
|
|
2168
|
+
### Adapter `resumeQueue()`
|
|
2169
|
+
|
|
2170
|
+
```typescript
|
|
2171
|
+
await adapter.resumeQueue("email");
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2174
|
+
### Adapter `retryJob()`
|
|
2175
|
+
|
|
2176
|
+
```typescript
|
|
2177
|
+
await adapter.retryJob("job-id", "email");
|
|
2178
|
+
```
|
|
2179
|
+
|
|
2180
|
+
### Adapter `removeJob()`
|
|
2181
|
+
|
|
2182
|
+
```typescript
|
|
2183
|
+
await adapter.removeJob("job-id", "email");
|
|
2184
|
+
```
|
|
2185
|
+
|
|
2186
|
+
### Adapter `promoteJob()`
|
|
2187
|
+
|
|
2188
|
+
```typescript
|
|
2189
|
+
await adapter.promoteJob("job-id", "email");
|
|
2190
|
+
```
|
|
2191
|
+
|
|
2192
|
+
### Adapter `moveJobToFailed()`
|
|
2193
|
+
|
|
2194
|
+
```typescript
|
|
2195
|
+
await adapter.moveJobToFailed("job-id", "Manual fail", "email");
|
|
2196
|
+
```
|
|
2197
|
+
|
|
2198
|
+
### Adapter `publishEvent()`
|
|
2199
|
+
|
|
2200
|
+
```typescript
|
|
2201
|
+
await adapter.publishEvent("channel", { type: "event", data: {} });
|
|
2202
|
+
```
|
|
2203
|
+
|
|
2204
|
+
### Adapter `subscribeEvent()`
|
|
2205
|
+
|
|
2206
|
+
```typescript
|
|
2207
|
+
const unsubscribe = await adapter.subscribeEvent("channel", (payload) => {
|
|
2208
|
+
console.log(payload);
|
|
2209
|
+
});
|
|
2210
|
+
await unsubscribe();
|
|
2211
|
+
```
|
|
2212
|
+
|
|
2213
|
+
---
|
|
2214
|
+
|
|
2215
|
+
## 🤝 Contributing
|
|
2216
|
+
|
|
2217
|
+
- Keep TSDoc up-to-date for all public APIs.
|
|
2218
|
+
- Add tests for new adapters, utils, and core behaviors.
|
|
2219
|
+
- Keep examples accurate and runnable.
|
|
165
2220
|
|
|
166
|
-
|
|
167
|
-
- Preserve adapter features from `@igniter-js/adapter-bullmq`.
|
|
168
|
-
- Update `.specs/jobs.spec.md` time tracking after each task with timestamps.
|
|
2221
|
+
---
|
|
169
2222
|
|
|
170
|
-
## License
|
|
2223
|
+
## 📝 License
|
|
171
2224
|
|
|
172
2225
|
MIT
|