@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/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