@igniter-js/jobs 0.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/AGENTS.md +557 -0
- package/README.md +410 -0
- package/dist/adapter-CcQCatSa.d.mts +1411 -0
- package/dist/adapter-CcQCatSa.d.ts +1411 -0
- package/dist/adapters/bullmq.adapter.d.mts +131 -0
- package/dist/adapters/bullmq.adapter.d.ts +131 -0
- package/dist/adapters/bullmq.adapter.js +598 -0
- package/dist/adapters/bullmq.adapter.js.map +1 -0
- package/dist/adapters/bullmq.adapter.mjs +596 -0
- package/dist/adapters/bullmq.adapter.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +1129 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/index.mjs +1126 -0
- package/dist/adapters/index.mjs.map +1 -0
- package/dist/adapters/memory.adapter.d.mts +118 -0
- package/dist/adapters/memory.adapter.d.ts +118 -0
- package/dist/adapters/memory.adapter.js +571 -0
- package/dist/adapters/memory.adapter.js.map +1 -0
- package/dist/adapters/memory.adapter.mjs +569 -0
- package/dist/adapters/memory.adapter.mjs.map +1 -0
- package/dist/index.d.mts +1107 -0
- package/dist/index.d.ts +1107 -0
- package/dist/index.js +1137 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1128 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# @igniter-js/jobs
|
|
2
|
+
|
|
3
|
+
Type-safe background job processing for TypeScript applications with Redis queues.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔷 **Full Type Safety** - End-to-end TypeScript inference from job definition to dispatch
|
|
8
|
+
- 🚀 **Builder API** - Fluent, chainable API for defining jobs and queues
|
|
9
|
+
- ⚡ **BullMQ Powered** - Production-ready job processing with Redis
|
|
10
|
+
- 🎯 **Scopes & Actors** - Multi-tenant support with scope and actor tracking
|
|
11
|
+
- 📊 **Real-time Events** - Subscribe to job lifecycle events
|
|
12
|
+
- 🔄 **Cron Jobs** - Built-in scheduled job support
|
|
13
|
+
- 🔍 **Search & Discovery** - Find jobs and queues with powerful filters
|
|
14
|
+
- 📈 **Telemetry Ready** - Optional OpenTelemetry integration
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @igniter-js/jobs bullmq ioredis zod
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Define Your Queues
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { IgniterQueue } from '@igniter-js/jobs'
|
|
28
|
+
import { z } from 'zod'
|
|
29
|
+
|
|
30
|
+
const emailQueue = IgniterQueue.create('email')
|
|
31
|
+
.addJob('sendWelcome', {
|
|
32
|
+
schema: z.object({
|
|
33
|
+
email: z.string().email(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
}),
|
|
36
|
+
handler: async ({ data, log }) => {
|
|
37
|
+
await log('info', `Sending welcome email to ${data.email}`)
|
|
38
|
+
await sendEmail(data.email, 'Welcome!', `Hello ${data.name}!`)
|
|
39
|
+
},
|
|
40
|
+
options: {
|
|
41
|
+
attempts: 3,
|
|
42
|
+
backoff: { type: 'exponential', delay: 1000 },
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
.addJob('sendPasswordReset', {
|
|
46
|
+
schema: z.object({
|
|
47
|
+
email: z.string().email(),
|
|
48
|
+
token: z.string(),
|
|
49
|
+
}),
|
|
50
|
+
handler: async ({ data }) => {
|
|
51
|
+
await sendEmail(data.email, 'Password Reset', `Your token: ${data.token}`)
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
.addCron('dailyDigest', {
|
|
55
|
+
pattern: '0 8 * * *', // 8 AM daily
|
|
56
|
+
handler: async () => {
|
|
57
|
+
await generateAndSendDailyDigest()
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
.build()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Create Jobs Instance
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { IgniterJobs } from '@igniter-js/jobs'
|
|
67
|
+
import { BullMQAdapter } from '@igniter-js/jobs/adapters'
|
|
68
|
+
import Redis from 'ioredis'
|
|
69
|
+
|
|
70
|
+
const connection = new Redis()
|
|
71
|
+
|
|
72
|
+
const jobs = IgniterJobs.create<AppContext>()
|
|
73
|
+
.withAdapter(
|
|
74
|
+
BullMQAdapter.create({
|
|
75
|
+
connection,
|
|
76
|
+
defaultJobOptions: {
|
|
77
|
+
removeOnComplete: { count: 100 },
|
|
78
|
+
removeOnFail: { count: 500 },
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
.withContext(() => ({
|
|
83
|
+
db: prisma,
|
|
84
|
+
logger: winston,
|
|
85
|
+
}))
|
|
86
|
+
.addQueue(emailQueue)
|
|
87
|
+
.addQueue(notificationQueue)
|
|
88
|
+
.addScope('organization', {
|
|
89
|
+
resolver: (id) => prisma.organization.findUnique({ where: { id } }),
|
|
90
|
+
})
|
|
91
|
+
.addActor('user', {
|
|
92
|
+
resolver: (id) => prisma.user.findUnique({ where: { id } }),
|
|
93
|
+
})
|
|
94
|
+
.build()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3. Dispatch Jobs
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Simple dispatch with type-safe payload
|
|
101
|
+
await jobs.email.sendWelcome.dispatch({
|
|
102
|
+
email: 'user@example.com',
|
|
103
|
+
name: 'John',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Dispatch with options
|
|
107
|
+
await jobs.email.sendPasswordReset.dispatch(
|
|
108
|
+
{ email: 'user@example.com', token: 'abc123' },
|
|
109
|
+
{
|
|
110
|
+
delay: 5000,
|
|
111
|
+
priority: 10,
|
|
112
|
+
scope: { type: 'organization', id: 'org-123' },
|
|
113
|
+
actor: { type: 'user', id: 'user-456' },
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Schedule for future time
|
|
118
|
+
await jobs.email.sendWelcome.schedule({
|
|
119
|
+
data: { email: 'user@example.com', name: 'Jane' },
|
|
120
|
+
at: new Date('2025-01-01T00:00:00Z'),
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 4. Start Workers
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const worker = await jobs.worker
|
|
128
|
+
.forQueues(['email', 'notifications'])
|
|
129
|
+
.withConcurrency(10)
|
|
130
|
+
.withLimiter({ max: 100, duration: 60000 })
|
|
131
|
+
.onIdle(() => console.log('Worker idle'))
|
|
132
|
+
.start()
|
|
133
|
+
|
|
134
|
+
// Graceful shutdown
|
|
135
|
+
process.on('SIGTERM', async () => {
|
|
136
|
+
await worker.close()
|
|
137
|
+
await jobs.shutdown()
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Job Management
|
|
142
|
+
|
|
143
|
+
### Get Job Info
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const job = await jobs.job.get('email', 'job-id')
|
|
147
|
+
console.log(job?.state) // 'completed' | 'failed' | 'waiting' | etc.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Retry Failed Jobs
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Single job
|
|
154
|
+
await jobs.job.retry('email', 'job-id')
|
|
155
|
+
|
|
156
|
+
// All failed jobs in queue
|
|
157
|
+
await jobs.queue.retryAll('email')
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Job State
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const state = await jobs.job.state('email', 'job-id')
|
|
164
|
+
const progress = await jobs.job.progress('email', 'job-id')
|
|
165
|
+
const logs = await jobs.job.logs('email', 'job-id')
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Promote Delayed Jobs
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Move delayed job to waiting
|
|
172
|
+
await jobs.job.promote('email', 'job-id')
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Queue Management
|
|
176
|
+
|
|
177
|
+
### Queue Info
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const queue = await jobs.queue.retrieve('email')
|
|
181
|
+
console.log(queue.isPaused)
|
|
182
|
+
console.log(queue.jobCounts) // { waiting: 5, active: 2, completed: 100, ... }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Pause/Resume
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
await jobs.queue.pause('email')
|
|
189
|
+
await jobs.queue.resume('email')
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Clean Queue
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Remove completed jobs older than 24 hours
|
|
196
|
+
await jobs.queue.clean('email', {
|
|
197
|
+
status: 'completed',
|
|
198
|
+
olderThan: 24 * 60 * 60 * 1000,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Drain waiting/delayed jobs
|
|
202
|
+
await jobs.queue.drain('email')
|
|
203
|
+
|
|
204
|
+
// Obliterate entire queue
|
|
205
|
+
await jobs.queue.obliterate('email', { force: true })
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Events
|
|
209
|
+
|
|
210
|
+
### Subscribe to Events
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// All events from email queue
|
|
214
|
+
const unsubscribe = await jobs.subscribe('email:*', async (event) => {
|
|
215
|
+
console.log(event.type, event.data)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// Specific job type events
|
|
219
|
+
await jobs.subscribe('email:sendWelcome:completed', async (event) => {
|
|
220
|
+
console.log('Welcome email sent:', event.data.jobId)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Global events
|
|
224
|
+
await jobs.subscribe('*:*:failed', async (event) => {
|
|
225
|
+
await alertOps('Job failed:', event.data)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Cleanup
|
|
229
|
+
await unsubscribe()
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Event Types
|
|
233
|
+
|
|
234
|
+
- `enqueued` - Job added to queue
|
|
235
|
+
- `started` - Job processing started
|
|
236
|
+
- `progress` - Job progress updated
|
|
237
|
+
- `completed` - Job completed successfully
|
|
238
|
+
- `failed` - Job failed
|
|
239
|
+
- `retrying` - Job being retried
|
|
240
|
+
|
|
241
|
+
## Search
|
|
242
|
+
|
|
243
|
+
### Find Jobs
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
const results = await jobs.search.jobs({
|
|
247
|
+
status: ['failed', 'waiting'],
|
|
248
|
+
queue: 'email',
|
|
249
|
+
jobName: 'sendWelcome',
|
|
250
|
+
scopeId: 'org-123',
|
|
251
|
+
dateRange: {
|
|
252
|
+
from: new Date('2025-01-01'),
|
|
253
|
+
to: new Date('2025-01-31'),
|
|
254
|
+
},
|
|
255
|
+
orderBy: 'createdAt:desc',
|
|
256
|
+
limit: 50,
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Find Queues
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
const queues = await jobs.search.queues({
|
|
264
|
+
name: 'email',
|
|
265
|
+
isPaused: false,
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Scopes & Actors
|
|
270
|
+
|
|
271
|
+
Scopes and actors enable multi-tenant job processing with tracking.
|
|
272
|
+
|
|
273
|
+
### Define Scopes
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const jobs = IgniterJobs.create()
|
|
277
|
+
.addScope('organization', {
|
|
278
|
+
resolver: async (id) => {
|
|
279
|
+
return prisma.organization.findUnique({ where: { id } })
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
.addScope('workspace', {
|
|
283
|
+
resolver: async (id) => {
|
|
284
|
+
return prisma.workspace.findUnique({ where: { id } })
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
.build()
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Define Actors
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
const jobs = IgniterJobs.create()
|
|
294
|
+
.addActor('user', {
|
|
295
|
+
resolver: async (id) => {
|
|
296
|
+
return prisma.user.findUnique({ where: { id } })
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
.addActor('system', {
|
|
300
|
+
resolver: async (id) => ({ id, type: 'system' }),
|
|
301
|
+
})
|
|
302
|
+
.build()
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Use in Jobs
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
await jobs.email.sendWelcome.dispatch(
|
|
309
|
+
{ email: 'user@example.com', name: 'John' },
|
|
310
|
+
{
|
|
311
|
+
scope: { type: 'organization', id: 'org-123' },
|
|
312
|
+
actor: { type: 'user', id: 'user-456' },
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
// Search by scope/actor
|
|
317
|
+
const orgJobs = await jobs.search.jobs({
|
|
318
|
+
scopeId: 'org-123',
|
|
319
|
+
actorId: 'user-456',
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Telemetry
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { TelemetryAdapter } from '@igniter-js/telemetry'
|
|
327
|
+
|
|
328
|
+
const jobs = IgniterJobs.create()
|
|
329
|
+
.withTelemetry(
|
|
330
|
+
TelemetryAdapter.create({
|
|
331
|
+
serviceName: 'my-service',
|
|
332
|
+
})
|
|
333
|
+
)
|
|
334
|
+
.build()
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Testing
|
|
338
|
+
|
|
339
|
+
Use the MemoryAdapter for testing:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { MemoryAdapter } from '@igniter-js/jobs/adapters'
|
|
343
|
+
|
|
344
|
+
const adapter = MemoryAdapter.create()
|
|
345
|
+
|
|
346
|
+
const jobs = IgniterJobs.create()
|
|
347
|
+
.withAdapter(adapter)
|
|
348
|
+
.addQueue(emailQueue)
|
|
349
|
+
.build()
|
|
350
|
+
|
|
351
|
+
// Test job dispatch
|
|
352
|
+
await jobs.email.sendWelcome.dispatch({ email: 'test@example.com', name: 'Test' })
|
|
353
|
+
|
|
354
|
+
// Verify job was created
|
|
355
|
+
const searchResults = await jobs.search.jobs({ queue: 'email' })
|
|
356
|
+
expect(searchResults.length).toBe(1)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## API Reference
|
|
360
|
+
|
|
361
|
+
### IgniterQueue
|
|
362
|
+
|
|
363
|
+
| Method | Description |
|
|
364
|
+
|--------|-------------|
|
|
365
|
+
| `create(name, options?)` | Create queue builder |
|
|
366
|
+
| `addJob(name, config)` | Add job definition |
|
|
367
|
+
| `addCron(name, config)` | Add cron job |
|
|
368
|
+
| `build()` | Build queue configuration |
|
|
369
|
+
|
|
370
|
+
### IgniterJobs
|
|
371
|
+
|
|
372
|
+
| Method | Description |
|
|
373
|
+
|--------|-------------|
|
|
374
|
+
| `create<Context>()` | Create jobs builder |
|
|
375
|
+
| `withAdapter(adapter)` | Set adapter |
|
|
376
|
+
| `withContext(factory)` | Set context factory |
|
|
377
|
+
| `addQueue(queue)` | Add queue |
|
|
378
|
+
| `addScope(name, config)` | Add scope definition |
|
|
379
|
+
| `addActor(name, config)` | Add actor definition |
|
|
380
|
+
| `withTelemetry(adapter)` | Enable telemetry |
|
|
381
|
+
| `withDefaults(options)` | Set default job options |
|
|
382
|
+
| `build()` | Build jobs instance |
|
|
383
|
+
|
|
384
|
+
### Jobs Runtime
|
|
385
|
+
|
|
386
|
+
| Property/Method | Description |
|
|
387
|
+
|----------------|-------------|
|
|
388
|
+
| `[queueName].[jobName].dispatch(data, options?)` | Dispatch job |
|
|
389
|
+
| `[queueName].[jobName].schedule(params)` | Schedule job |
|
|
390
|
+
| `job.get(queue, id)` | Get job info |
|
|
391
|
+
| `job.state(queue, id)` | Get job state |
|
|
392
|
+
| `job.retry(queue, id)` | Retry job |
|
|
393
|
+
| `job.remove(queue, id)` | Remove job |
|
|
394
|
+
| `job.promote(queue, id)` | Promote delayed job |
|
|
395
|
+
| `queue.retrieve(name)` | Get queue info |
|
|
396
|
+
| `queue.pause(name)` | Pause queue |
|
|
397
|
+
| `queue.resume(name)` | Resume queue |
|
|
398
|
+
| `queue.drain(name)` | Drain queue |
|
|
399
|
+
| `queue.clean(name, options)` | Clean queue |
|
|
400
|
+
| `queue.obliterate(name, options?)` | Delete queue |
|
|
401
|
+
| `queue.retryAll(name)` | Retry all failed |
|
|
402
|
+
| `subscribe(pattern, handler)` | Subscribe to events |
|
|
403
|
+
| `search.jobs(filter)` | Search jobs |
|
|
404
|
+
| `search.queues(filter)` | Search queues |
|
|
405
|
+
| `worker` | Worker builder |
|
|
406
|
+
| `shutdown()` | Shutdown |
|
|
407
|
+
|
|
408
|
+
## License
|
|
409
|
+
|
|
410
|
+
MIT
|