@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.
Files changed (42) hide show
  1. package/AGENTS.md +1118 -96
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +2146 -93
  4. package/dist/{adapter-PiDCQWQd.d.mts → adapter-CXZxomI9.d.mts} +2 -2
  5. package/dist/{adapter-PiDCQWQd.d.ts → adapter-CXZxomI9.d.ts} +2 -2
  6. package/dist/adapters/bullmq.adapter.d.mts +2 -2
  7. package/dist/adapters/bullmq.adapter.d.ts +2 -2
  8. package/dist/adapters/bullmq.adapter.js +2 -2
  9. package/dist/adapters/bullmq.adapter.js.map +1 -1
  10. package/dist/adapters/bullmq.adapter.mjs +1 -1
  11. package/dist/adapters/bullmq.adapter.mjs.map +1 -1
  12. package/dist/adapters/index.d.mts +140 -2
  13. package/dist/adapters/index.d.ts +140 -2
  14. package/dist/adapters/index.js +864 -31
  15. package/dist/adapters/index.js.map +1 -1
  16. package/dist/adapters/index.mjs +863 -31
  17. package/dist/adapters/index.mjs.map +1 -1
  18. package/dist/adapters/memory.adapter.d.mts +2 -2
  19. package/dist/adapters/memory.adapter.d.ts +2 -2
  20. package/dist/adapters/memory.adapter.js +122 -30
  21. package/dist/adapters/memory.adapter.js.map +1 -1
  22. package/dist/adapters/memory.adapter.mjs +121 -29
  23. package/dist/adapters/memory.adapter.mjs.map +1 -1
  24. package/dist/index.d.mts +452 -342
  25. package/dist/index.d.ts +452 -342
  26. package/dist/index.js +1923 -1002
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +1921 -1001
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/shim.d.mts +36 -0
  31. package/dist/shim.d.ts +36 -0
  32. package/dist/shim.js +75 -0
  33. package/dist/shim.js.map +1 -0
  34. package/dist/shim.mjs +67 -0
  35. package/dist/shim.mjs.map +1 -0
  36. package/dist/telemetry/index.d.mts +281 -0
  37. package/dist/telemetry/index.d.ts +281 -0
  38. package/dist/telemetry/index.js +97 -0
  39. package/dist/telemetry/index.js.map +1 -0
  40. package/dist/telemetry/index.mjs +95 -0
  41. package/dist/telemetry/index.mjs.map +1 -0
  42. package/package.json +44 -11
package/README.md CHANGED
@@ -1,57 +1,78 @@
1
1
  # @igniter-js/jobs
2
2
 
3
- Type-safe jobs, scheduling, and worker builder for Igniter.js. Provides a fluent API to define queues, jobs, cron tasks, scope-aware dispatch, typed events, and adapter-based backends (BullMQ first, in-memory for tests).
3
+ <div align="center">
4
4
 
5
- > Status: Work in progress. API follows the spec in `.specs/jobs.spec.md` and mirrors the DX of `@igniter-js/store`.
5
+ [![npm version](https://img.shields.io/npm/v/@igniter-js/jobs)](https://www.npmjs.com/package/@igniter-js/jobs)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.6+-blue)](https://www.typescriptlang.org/)
8
+ [![Node.js](https://img.shields.io/badge/Node.js-22+-green)](https://nodejs.org)
6
9
 
7
- ## Features
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
- - Fluent builder API: `IgniterJobs.create<TContext>() ... .build()`
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
- ## Installation
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
- npm install @igniter-js/jobs @igniter-js/core
21
- # Optional adapters/peers
22
- npm install bullmq ioredis zod @igniter-js/adapter-bullmq
52
+ # bun
53
+ bun add @igniter-js/jobs zod
23
54
  ```
24
55
 
25
- ## Quick Start
56
+ ### Your First Queue (60 seconds)
26
57
 
27
- ```ts
58
+ ```typescript
28
59
  import { IgniterJobs, IgniterQueue } from "@igniter-js/jobs";
29
- import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters";
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: 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.send(input);
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<AppContext>()
54
- .withAdapter(IgniterJobsBullMQAdapter.create({ redis }))
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
- ## Scope (single-scope)
85
+ **✅ Success!** You just created a typed job, registered it with a queue, and dispatched it.
65
86
 
66
- ```ts
67
- const jobs = IgniterJobs.create<AppContext>()
68
- .withAdapter(IgniterJobsBullMQAdapter.create({ redis }))
69
- .withService("my-api")
70
- .withEnvironment("development")
71
- .withContext(async () => ({ mailer }))
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
- // Option A: bind scope once
77
- const orgJobs = jobs.scope("organization", "org_123", { plan: "pro" });
78
- await orgJobs.email.sendWelcome.dispatch({
79
- input: { email: "user@example.com" },
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
- // Option B: per-dispatch scope
301
+ ```typescript
83
302
  await jobs.email.sendWelcome.dispatch({
84
- input: { email: "user@example.com" },
85
- scope: { type: "organization", id: "org_123", tags: { plan: "pro" } },
303
+ input: { email: "a@b.com" },
304
+ scope: { type: "organization", id: "org_123" },
86
305
  });
87
306
  ```
88
307
 
89
- ## Events (subscribe)
308
+ ### 10) Subscribe to All Events
90
309
 
91
- ```ts
92
- // Global listener (unscoped)
310
+ ```typescript
93
311
  const unsubscribe = await jobs.subscribe((event) => {
94
- console.log(event.type, event.data, event.timestamp, event.scope);
312
+ console.log(event.type, event.data, event.timestamp);
95
313
  });
96
314
 
97
- // Job-level listener
98
- await jobs.email.sendWelcome.subscribe((event) => {
99
- // type is "email:sendWelcome:enqueued" | "email:sendWelcome:started" | ...
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
- ## Telemetry
328
+ ### 12) Subscribe to Job Events
107
329
 
108
- Integrate with `@igniter-js/telemetry` to automatically emit OpenTelemetry events for job lifecycle, workers, and queues.
330
+ ```typescript
331
+ const unsubscribe = await jobs.email.sendWelcome.subscribe((event) => {
332
+ console.log(event.type, event.data);
333
+ });
109
334
 
110
- ```ts
111
- import { IgniterTelemetry } from "@igniter-js/telemetry";
112
- import { IgniterJobsTelemetryEvents } from "@igniter-js/jobs";
335
+ await unsubscribe();
336
+ ```
113
337
 
114
- // 1. Configure telemetry with job events
115
- const telemetry = IgniterTelemetry.create()
116
- .withService("my-api")
117
- .addEvents(IgniterJobsTelemetryEvents)
118
- .build();
338
+ ### 13) Queue Management APIs
119
339
 
120
- // 2. Pass to jobs instance
121
- const jobs = IgniterJobs.create<AppContext>()
122
- .withTelemetry(telemetry)
123
- // ...
124
- .build();
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
- This will automatically emit events like:
347
+ ### 14) Queue Cleaning
128
348
 
129
- - `igniter.jobs.job.enqueued`
130
- - `igniter.jobs.job.started`
131
- - `igniter.jobs.job.completed` (with duration)
132
- - `igniter.jobs.job.failed` (with error details)
133
- - `igniter.jobs.job.progress`
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
- ## Management APIs (examples)
357
+ ### 15) Queue Obliterate
136
358
 
137
- ```ts
138
- // Queue management
139
- const queues = await jobs.search("queues", {});
140
- await jobs.email.get().pause();
141
- await jobs.email.get().resume();
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
- // Inspect a job
144
- const job = await jobs.email.sendWelcome.get("jobId").retrieve();
145
- const logs = await jobs.email.sendWelcome.get("jobId").logs();
146
- const progress = await jobs.email.sendWelcome.get("jobId").progress();
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
- ## Workers
412
+ > Note: The BullMQ adapter throws `JOBS_QUEUE_OPERATION_FAILED` for pause/resume job type.
413
+
414
+ ### 23) Create a Worker
150
415
 
151
- ```ts
416
+ ```typescript
152
417
  const worker = await jobs.worker
153
418
  .create()
154
419
  .addQueue("email")
155
420
  .withConcurrency(10)
156
- .build();
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
- // Later
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
- ## Contributing
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
- - Keep TSDoc on all public APIs.
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