@igniter-js/jobs 0.1.13 → 0.1.15
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 +119 -243
- package/README.md +352 -158
- package/dist/{adapter-CXZxomI9.d.mts → adapter-DDyMVche.d.mts} +125 -20
- package/dist/{adapter-CXZxomI9.d.ts → adapter-DDyMVche.d.ts} +125 -20
- package/dist/adapters/bun.d.mts +101 -0
- package/dist/adapters/bun.d.ts +101 -0
- package/dist/adapters/bun.js +1048 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/bun.mjs +1046 -0
- package/dist/adapters/bun.mjs.map +1 -0
- package/dist/adapters/{memory.adapter.d.ts → mock.d.mts} +7 -3
- package/dist/adapters/{memory.adapter.d.mts → mock.d.ts} +7 -3
- package/dist/adapters/{memory.adapter.js → mock.js} +122 -25
- package/dist/adapters/mock.js.map +1 -0
- package/dist/adapters/{memory.adapter.mjs → mock.mjs} +122 -25
- package/dist/adapters/mock.mjs.map +1 -0
- package/dist/adapters/{bullmq.adapter.d.mts → node.d.mts} +8 -3
- package/dist/adapters/{bullmq.adapter.d.ts → node.d.ts} +8 -3
- package/dist/adapters/{bullmq.adapter.js → node.js} +194 -40
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/{bullmq.adapter.mjs → node.mjs} +194 -40
- package/dist/adapters/node.mjs.map +1 -0
- package/dist/index.d.mts +41 -38
- package/dist/index.d.ts +41 -38
- package/dist/index.js +145 -1856
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +146 -1854
- package/dist/index.mjs.map +1 -1
- package/package.json +29 -41
- package/CHANGELOG.md +0 -13
- package/dist/adapters/bullmq.adapter.js.map +0 -1
- package/dist/adapters/bullmq.adapter.mjs.map +0 -1
- package/dist/adapters/index.d.mts +0 -143
- package/dist/adapters/index.d.ts +0 -143
- package/dist/adapters/index.js +0 -1891
- package/dist/adapters/index.js.map +0 -1
- package/dist/adapters/index.mjs +0 -1887
- package/dist/adapters/index.mjs.map +0 -1
- package/dist/adapters/memory.adapter.js.map +0 -1
- package/dist/adapters/memory.adapter.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Background processing is notoriously hard to keep reliable and observable. `@ign
|
|
|
25
25
|
- ✅ **Runtime validation** — Zod or Standard Schema V1 input validation.
|
|
26
26
|
- ✅ **Scoped jobs** — First-class multi-tenant support via `scope()`.
|
|
27
27
|
- ✅ **Observability built-in** — Telemetry events and pub/sub job lifecycle events.
|
|
28
|
+
- ✅ **Typed job streams** — Emit live per-job stream events with optional persistence and replay.
|
|
28
29
|
- ✅ **Adapter-based backends** — In-memory for tests, SQLite for local, BullMQ for production.
|
|
29
30
|
|
|
30
31
|
---
|
|
@@ -57,7 +58,7 @@ bun add @igniter-js/jobs zod
|
|
|
57
58
|
|
|
58
59
|
```typescript
|
|
59
60
|
import { IgniterJobs, IgniterQueue } from "@igniter-js/jobs";
|
|
60
|
-
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
61
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters/mock";
|
|
61
62
|
import { z } from "zod";
|
|
62
63
|
|
|
63
64
|
type AppContext = { mailer: { sendWelcome: (email: string) => Promise<void> } };
|
|
@@ -109,7 +110,7 @@ await jobs.email.sendWelcome.dispatch({ input: { email: "user@example.com" } });
|
|
|
109
110
|
▼
|
|
110
111
|
┌─────────────────────────────────────────────────────────────┐
|
|
111
112
|
│ Adapter Layer │
|
|
112
|
-
│ Memory Adapter • SQLite Adapter • BullMQ Adapter
|
|
113
|
+
│ Memory Adapter • Bun SQLite Adapter • BullMQ Adapter │
|
|
113
114
|
└────────────┬────────────────────────────────────────────────┘
|
|
114
115
|
│
|
|
115
116
|
▼
|
|
@@ -184,8 +185,8 @@ await jobs.email.sendWelcome.dispatch({ input: { email: "user@example.com" } });
|
|
|
184
185
|
48. Graceful shutdown in process
|
|
185
186
|
49. Global events to analytics
|
|
186
187
|
50. Event filtering pattern
|
|
187
|
-
51. SQLite adapter usage
|
|
188
|
-
52. SQLite adapter persistence
|
|
188
|
+
51. Bun SQLite adapter usage
|
|
189
|
+
52. Bun SQLite adapter persistence
|
|
189
190
|
53. Memory adapter testing
|
|
190
191
|
54. BullMQ adapter usage
|
|
191
192
|
55. Custom adapter skeleton
|
|
@@ -229,7 +230,7 @@ const uploadQueue = IgniterQueue.create("uploads")
|
|
|
229
230
|
|
|
230
231
|
```typescript
|
|
231
232
|
import { IgniterJobs } from "@igniter-js/jobs";
|
|
232
|
-
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
233
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters/mock";
|
|
233
234
|
|
|
234
235
|
const jobs = IgniterJobs.create()
|
|
235
236
|
.withAdapter(IgniterJobsMemoryAdapter.create())
|
|
@@ -402,16 +403,7 @@ await jobs.email.sendWelcome.many(["job-1", "job-2"]).retry();
|
|
|
402
403
|
await jobs.email.sendWelcome.many(["job-1", "job-2"]).remove();
|
|
403
404
|
```
|
|
404
405
|
|
|
405
|
-
### 22)
|
|
406
|
-
|
|
407
|
-
```typescript
|
|
408
|
-
await jobs.email.sendWelcome.pause();
|
|
409
|
-
await jobs.email.sendWelcome.resume();
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
> Note: The BullMQ adapter throws `JOBS_QUEUE_OPERATION_FAILED` for pause/resume job type.
|
|
413
|
-
|
|
414
|
-
### 23) Create a Worker
|
|
406
|
+
### 22) Create a Worker
|
|
415
407
|
|
|
416
408
|
```typescript
|
|
417
409
|
const worker = await jobs.worker
|
|
@@ -645,7 +637,42 @@ const queue = IgniterQueue.create("import")
|
|
|
645
637
|
.build();
|
|
646
638
|
```
|
|
647
639
|
|
|
648
|
-
### 44) Job
|
|
640
|
+
### 44) Typed Job Streams
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
const queue = IgniterQueue.create("ai")
|
|
644
|
+
.addJob("generate", {
|
|
645
|
+
stream: {
|
|
646
|
+
persistence: { enabled: true, maxEvents: 1000 },
|
|
647
|
+
events: {
|
|
648
|
+
"text-delta": z.string(),
|
|
649
|
+
status: z.object({ phase: z.string() }),
|
|
650
|
+
done: z.object({ finishReason: z.string() }),
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
handler: async ({ job }) => {
|
|
654
|
+
await job.stream.emit("status", { phase: "thinking" });
|
|
655
|
+
await job.stream.emit("text-delta", "Hello");
|
|
656
|
+
await job.stream.emit("done", { finishReason: "stop" });
|
|
657
|
+
return { ok: true };
|
|
658
|
+
},
|
|
659
|
+
})
|
|
660
|
+
.build();
|
|
661
|
+
|
|
662
|
+
const jobId = await jobs.ai.generate.dispatch({ input: {} });
|
|
663
|
+
|
|
664
|
+
const off = await jobs.ai.generate
|
|
665
|
+
.get(jobId)
|
|
666
|
+
.stream()
|
|
667
|
+
.subscribe((event) => {
|
|
668
|
+
console.log(event.type, event.data);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const history = await jobs.ai.generate.get(jobId).stream().read({ limit: 100 });
|
|
672
|
+
await off();
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### 45) Job Logs Inspection
|
|
649
676
|
|
|
650
677
|
```typescript
|
|
651
678
|
const logs = await jobs.email.sendWelcome.get("job-id").logs();
|
|
@@ -654,7 +681,7 @@ for (const entry of logs) {
|
|
|
654
681
|
}
|
|
655
682
|
```
|
|
656
683
|
|
|
657
|
-
###
|
|
684
|
+
### 46) Queue List with Paging
|
|
658
685
|
|
|
659
686
|
```typescript
|
|
660
687
|
const jobsInQueue = await jobs.email.list({
|
|
@@ -664,7 +691,7 @@ const jobsInQueue = await jobs.email.list({
|
|
|
664
691
|
});
|
|
665
692
|
```
|
|
666
693
|
|
|
667
|
-
###
|
|
694
|
+
### 47) Worker Limiter Pattern
|
|
668
695
|
|
|
669
696
|
```typescript
|
|
670
697
|
const worker = await jobs.worker
|
|
@@ -674,14 +701,14 @@ const worker = await jobs.worker
|
|
|
674
701
|
.start();
|
|
675
702
|
```
|
|
676
703
|
|
|
677
|
-
###
|
|
704
|
+
### 48) Worker Sharding by Queue
|
|
678
705
|
|
|
679
706
|
```typescript
|
|
680
707
|
const workerA = await jobs.worker.create().addQueue("email").start();
|
|
681
708
|
const workerB = await jobs.worker.create().addQueue("analytics").start();
|
|
682
709
|
```
|
|
683
710
|
|
|
684
|
-
###
|
|
711
|
+
### 49) Graceful Shutdown in Process
|
|
685
712
|
|
|
686
713
|
```typescript
|
|
687
714
|
process.on("SIGINT", async () => {
|
|
@@ -712,21 +739,24 @@ const unsubscribe = await jobs.subscribe((event) => {
|
|
|
712
739
|
});
|
|
713
740
|
```
|
|
714
741
|
|
|
715
|
-
### 51) SQLite Adapter Usage
|
|
742
|
+
### 51) Bun SQLite Adapter Usage
|
|
716
743
|
|
|
717
744
|
```typescript
|
|
718
|
-
const adapter =
|
|
745
|
+
const adapter = IgniterJobsBunSQLiteAdapter.create({
|
|
719
746
|
path: "./jobs.sqlite",
|
|
720
|
-
|
|
721
|
-
enableWAL: true,
|
|
747
|
+
durable: true,
|
|
722
748
|
});
|
|
723
749
|
```
|
|
724
750
|
|
|
725
|
-
### 52) SQLite Adapter Persistence
|
|
751
|
+
### 52) Bun SQLite Adapter Persistence
|
|
726
752
|
|
|
727
753
|
```typescript
|
|
728
|
-
const adapter =
|
|
729
|
-
await adapter.dispatch({
|
|
754
|
+
const adapter = IgniterJobsBunSQLiteAdapter.create({ path: "./jobs.sqlite" });
|
|
755
|
+
await adapter.dispatch({
|
|
756
|
+
queue: "emails",
|
|
757
|
+
jobName: "send",
|
|
758
|
+
input: { id: "1" },
|
|
759
|
+
});
|
|
730
760
|
await adapter.shutdown();
|
|
731
761
|
```
|
|
732
762
|
|
|
@@ -748,39 +778,120 @@ const adapter = IgniterJobsBullMQAdapter.create({ redis });
|
|
|
748
778
|
class CustomAdapter implements IgniterJobsAdapter {
|
|
749
779
|
readonly client = {};
|
|
750
780
|
readonly queues = null as any;
|
|
751
|
-
async dispatch(params: IgniterJobsAdapterDispatchParams) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
async
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
async
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
async
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
async
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
async
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
async
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
async
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
async
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
781
|
+
async dispatch(params: IgniterJobsAdapterDispatchParams) {
|
|
782
|
+
return "job-id";
|
|
783
|
+
}
|
|
784
|
+
async schedule(params: IgniterJobsAdapterScheduleParams) {
|
|
785
|
+
return "job-id";
|
|
786
|
+
}
|
|
787
|
+
async getJob(jobId: string) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
async getJobState(jobId: string) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
async getJobLogs(jobId: string) {
|
|
794
|
+
return [];
|
|
795
|
+
}
|
|
796
|
+
async getJobProgress(jobId: string) {
|
|
797
|
+
return 0;
|
|
798
|
+
}
|
|
799
|
+
async retryJob(jobId: string) {
|
|
800
|
+
/* ... */
|
|
801
|
+
}
|
|
802
|
+
async removeJob(jobId: string) {
|
|
803
|
+
/* ... */
|
|
804
|
+
}
|
|
805
|
+
async promoteJob(jobId: string) {
|
|
806
|
+
/* ... */
|
|
807
|
+
}
|
|
808
|
+
async moveJobToFailed(jobId: string, reason: string) {
|
|
809
|
+
/* ... */
|
|
810
|
+
}
|
|
811
|
+
async retryManyJobs(jobIds: string[]) {
|
|
812
|
+
/* ... */
|
|
813
|
+
}
|
|
814
|
+
async removeManyJobs(jobIds: string[]) {
|
|
815
|
+
/* ... */
|
|
816
|
+
}
|
|
817
|
+
async getQueueInfo(queue: string) {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
async getQueueJobCounts(queue: string) {
|
|
821
|
+
return {
|
|
822
|
+
waiting: 0,
|
|
823
|
+
active: 0,
|
|
824
|
+
completed: 0,
|
|
825
|
+
failed: 0,
|
|
826
|
+
delayed: 0,
|
|
827
|
+
paused: 0,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
async listQueues() {
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
async pauseQueue(queue: string) {
|
|
834
|
+
/* ... */
|
|
835
|
+
}
|
|
836
|
+
async resumeQueue(queue: string) {
|
|
837
|
+
/* ... */
|
|
838
|
+
}
|
|
839
|
+
async drainQueue(queue: string) {
|
|
840
|
+
return 0;
|
|
841
|
+
}
|
|
842
|
+
async cleanQueue(queue: string, options: IgniterJobsQueueCleanOptions) {
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
async obliterateQueue(queue: string, options?: { force?: boolean }) {
|
|
846
|
+
/* ... */
|
|
847
|
+
}
|
|
848
|
+
async retryAllInQueue(queue: string) {
|
|
849
|
+
return 0;
|
|
850
|
+
}
|
|
851
|
+
async pauseJobType(queue: string, jobName: string) {
|
|
852
|
+
/* ... */
|
|
853
|
+
}
|
|
854
|
+
async resumeJobType(queue: string, jobName: string) {
|
|
855
|
+
/* ... */
|
|
856
|
+
}
|
|
857
|
+
async searchJobs(filter: Record<string, unknown>) {
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
async searchQueues(filter: Record<string, unknown>) {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
async searchWorkers(filter: Record<string, unknown>) {
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
async createWorker(config: IgniterJobsWorkerBuilderConfig) {
|
|
867
|
+
return {} as IgniterJobsWorkerHandle;
|
|
868
|
+
}
|
|
869
|
+
getWorkers() {
|
|
870
|
+
return new Map();
|
|
871
|
+
}
|
|
872
|
+
async publishEvent(channel: string, payload: unknown) {
|
|
873
|
+
/* ... */
|
|
874
|
+
}
|
|
875
|
+
async subscribeEvent(channel: string, handler: IgniterJobsEventHandler) {
|
|
876
|
+
return async () => {};
|
|
877
|
+
}
|
|
878
|
+
registerJob(
|
|
879
|
+
queueName: string,
|
|
880
|
+
jobName: string,
|
|
881
|
+
definition: IgniterJobDefinition<any, any, any>,
|
|
882
|
+
) {
|
|
883
|
+
/* ... */
|
|
884
|
+
}
|
|
885
|
+
registerCron(
|
|
886
|
+
queueName: string,
|
|
887
|
+
cronName: string,
|
|
888
|
+
definition: IgniterCronDefinition<any, any>,
|
|
889
|
+
) {
|
|
890
|
+
/* ... */
|
|
891
|
+
}
|
|
892
|
+
async shutdown() {
|
|
893
|
+
/* ... */
|
|
894
|
+
}
|
|
784
895
|
}
|
|
785
896
|
```
|
|
786
897
|
|
|
@@ -798,7 +909,9 @@ const telemetry = IgniterTelemetry.create()
|
|
|
798
909
|
|
|
799
910
|
```typescript
|
|
800
911
|
export async function POST() {
|
|
801
|
-
const id = await jobs.email.sendWelcome.dispatch({
|
|
912
|
+
const id = await jobs.email.sendWelcome.dispatch({
|
|
913
|
+
input: { email: "user@example.com" },
|
|
914
|
+
});
|
|
802
915
|
return NextResponse.json({ jobId: id });
|
|
803
916
|
}
|
|
804
917
|
```
|
|
@@ -807,7 +920,9 @@ export async function POST() {
|
|
|
807
920
|
|
|
808
921
|
```typescript
|
|
809
922
|
app.post("/send", async (_req, res) => {
|
|
810
|
-
const jobId = await jobs.email.sendWelcome.dispatch({
|
|
923
|
+
const jobId = await jobs.email.sendWelcome.dispatch({
|
|
924
|
+
input: { email: "user@example.com" },
|
|
925
|
+
});
|
|
811
926
|
res.json({ jobId });
|
|
812
927
|
});
|
|
813
928
|
```
|
|
@@ -816,7 +931,9 @@ app.post("/send", async (_req, res) => {
|
|
|
816
931
|
|
|
817
932
|
```typescript
|
|
818
933
|
app.post("/send", async (_req, res) => {
|
|
819
|
-
const jobId = await jobs.email.sendWelcome.dispatch({
|
|
934
|
+
const jobId = await jobs.email.sendWelcome.dispatch({
|
|
935
|
+
input: { email: "user@example.com" },
|
|
936
|
+
});
|
|
820
937
|
return res.send({ jobId });
|
|
821
938
|
});
|
|
822
939
|
```
|
|
@@ -1019,44 +1136,45 @@ const jobs = IgniterJobs.create()
|
|
|
1019
1136
|
|
|
1020
1137
|
## 🔌 Adapters
|
|
1021
1138
|
|
|
1022
|
-
Adapters implement the `IgniterJobsAdapter` interface and are
|
|
1139
|
+
Adapters implement the `IgniterJobsAdapter` interface and are grouped by runtime:
|
|
1023
1140
|
|
|
1024
1141
|
```typescript
|
|
1025
|
-
import { IgniterJobsMemoryAdapter
|
|
1142
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters/mock";
|
|
1143
|
+
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters/node";
|
|
1144
|
+
import { IgniterJobsBunSQLiteAdapter } from "@igniter-js/jobs/adapters/bun";
|
|
1026
1145
|
```
|
|
1027
1146
|
|
|
1028
1147
|
### Adapter Comparison
|
|
1029
1148
|
|
|
1030
|
-
| Adapter | Persistence | Multi-process
|
|
1031
|
-
|
|
1032
|
-
| Memory
|
|
1033
|
-
| SQLite
|
|
1034
|
-
| BullMQ
|
|
1149
|
+
| Adapter | Persistence | Multi-process | Use Case |
|
|
1150
|
+
| ------- | ----------- | ------------------- | --------------------- |
|
|
1151
|
+
| Memory | ❌ | ❌ | Unit tests, local dev |
|
|
1152
|
+
| SQLite | ✅ | ⚠️ (single process) | Desktop, CLI, local |
|
|
1153
|
+
| BullMQ | ✅ | ✅ | Production scale |
|
|
1035
1154
|
|
|
1036
1155
|
### Memory Adapter
|
|
1037
1156
|
|
|
1038
1157
|
```typescript
|
|
1039
|
-
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
1158
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters/mock";
|
|
1040
1159
|
|
|
1041
1160
|
const adapter = IgniterJobsMemoryAdapter.create();
|
|
1042
1161
|
```
|
|
1043
1162
|
|
|
1044
|
-
### SQLite Adapter
|
|
1163
|
+
### Bun SQLite Adapter
|
|
1045
1164
|
|
|
1046
1165
|
```typescript
|
|
1047
|
-
import {
|
|
1166
|
+
import { IgniterJobsBunSQLiteAdapter } from "@igniter-js/jobs/adapters/bun";
|
|
1048
1167
|
|
|
1049
|
-
const adapter =
|
|
1168
|
+
const adapter = IgniterJobsBunSQLiteAdapter.create({
|
|
1050
1169
|
path: "./data/jobs.sqlite",
|
|
1051
|
-
|
|
1052
|
-
enableWAL: true,
|
|
1170
|
+
durable: true,
|
|
1053
1171
|
});
|
|
1054
1172
|
```
|
|
1055
1173
|
|
|
1056
1174
|
### BullMQ Adapter
|
|
1057
1175
|
|
|
1058
1176
|
```typescript
|
|
1059
|
-
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters";
|
|
1177
|
+
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters/node";
|
|
1060
1178
|
import Redis from "ioredis";
|
|
1061
1179
|
|
|
1062
1180
|
const redis = new Redis(process.env.REDIS_URL);
|
|
@@ -1067,12 +1185,23 @@ const adapter = IgniterJobsBullMQAdapter.create({ redis });
|
|
|
1067
1185
|
|
|
1068
1186
|
## 🧪 Testing
|
|
1069
1187
|
|
|
1188
|
+
### Bun SQLite Adapter
|
|
1189
|
+
|
|
1190
|
+
```typescript
|
|
1191
|
+
import { IgniterJobsBunSQLiteAdapter } from "@igniter-js/jobs/adapters/bun";
|
|
1192
|
+
|
|
1193
|
+
const adapter = IgniterJobsBunSQLiteAdapter.create({
|
|
1194
|
+
path: "./data/jobs.sqlite",
|
|
1195
|
+
durable: true,
|
|
1196
|
+
});
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1070
1199
|
### Unit Testing with Memory Adapter
|
|
1071
1200
|
|
|
1072
1201
|
```typescript
|
|
1073
1202
|
import { describe, it, expect } from "vitest";
|
|
1074
1203
|
import { IgniterJobs, IgniterQueue } from "@igniter-js/jobs";
|
|
1075
|
-
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters";
|
|
1204
|
+
import { IgniterJobsMemoryAdapter } from "@igniter-js/jobs/adapters/mock";
|
|
1076
1205
|
|
|
1077
1206
|
describe("jobs", () => {
|
|
1078
1207
|
it("dispatches and processes a job", async () => {
|
|
@@ -1121,22 +1250,21 @@ describe("jobs", () => {
|
|
|
1121
1250
|
const adapter = IgniterJobsMemoryAdapter.create({ maxJobHistory: 1000 });
|
|
1122
1251
|
```
|
|
1123
1252
|
|
|
1124
|
-
### SQLite Adapter
|
|
1253
|
+
### Bun SQLite Adapter
|
|
1125
1254
|
|
|
1126
1255
|
Use for desktop apps, CLI tools, or local persistence without Redis.
|
|
1127
1256
|
|
|
1128
1257
|
Key traits:
|
|
1129
1258
|
|
|
1130
1259
|
- Persistent storage on disk
|
|
1131
|
-
- Single-process (
|
|
1260
|
+
- Single-process local runtime (persistent, but not distributed)
|
|
1132
1261
|
- Polling-based worker loop
|
|
1133
1262
|
- Great for edge or MCP servers
|
|
1134
1263
|
|
|
1135
1264
|
```typescript
|
|
1136
|
-
const adapter =
|
|
1265
|
+
const adapter = IgniterJobsBunSQLiteAdapter.create({
|
|
1137
1266
|
path: "./data/jobs.sqlite",
|
|
1138
|
-
|
|
1139
|
-
enableWAL: true,
|
|
1267
|
+
durable: true,
|
|
1140
1268
|
});
|
|
1141
1269
|
```
|
|
1142
1270
|
|
|
@@ -1195,10 +1323,10 @@ describe("jobs", () => {
|
|
|
1195
1323
|
- Use `withLimiter()` for API-bound workloads.
|
|
1196
1324
|
- Shard by queue when jobs have very different resource needs.
|
|
1197
1325
|
|
|
1198
|
-
###
|
|
1326
|
+
### Local Bun SQLite Runtime
|
|
1199
1327
|
|
|
1200
|
-
-
|
|
1201
|
-
-
|
|
1328
|
+
- Use `durable: true` only for jobs that must survive abrupt exits.
|
|
1329
|
+
- Tune `batchSize`, `pollTimeout`, and `heartbeatInterval` for throughput-sensitive workloads.
|
|
1202
1330
|
|
|
1203
1331
|
---
|
|
1204
1332
|
|
|
@@ -1705,17 +1833,40 @@ export const IgniterJobs: {
|
|
|
1705
1833
|
class IgniterJobsBuilder<TContext, TQueues, TScope> {
|
|
1706
1834
|
static create(): IgniterJobsBuilder<unknown>;
|
|
1707
1835
|
|
|
1708
|
-
withAdapter(
|
|
1836
|
+
withAdapter(
|
|
1837
|
+
adapter: IgniterJobsAdapter,
|
|
1838
|
+
): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1709
1839
|
withService(service: string): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1710
|
-
withEnvironment(
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1840
|
+
withEnvironment(
|
|
1841
|
+
environment: string,
|
|
1842
|
+
): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1843
|
+
withContext<TNewContext>(
|
|
1844
|
+
factory: () => TNewContext | Promise<TNewContext>,
|
|
1845
|
+
): IgniterJobsBuilder<TNewContext, {}, TScope>;
|
|
1846
|
+
addScope(
|
|
1847
|
+
name: string,
|
|
1848
|
+
options?: IgniterJobsScopeOptions,
|
|
1849
|
+
): IgniterJobsBuilder<TContext, TQueues, TScope | string>;
|
|
1850
|
+
addQueue(
|
|
1851
|
+
queue: IgniterJobsQueue<TContext, any, any> & { name: string },
|
|
1852
|
+
): IgniterJobsBuilder<TContext, TQueues & Record<string, any>, TScope>;
|
|
1853
|
+
withQueueDefaults(
|
|
1854
|
+
defaults: Partial<IgniterJobDefinition<TContext, any, any>>,
|
|
1855
|
+
): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1856
|
+
withWorkerDefaults(
|
|
1857
|
+
defaults: Partial<IgniterJobsWorkerBuilderConfig>,
|
|
1858
|
+
): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1859
|
+
withAutoStartWorker(config: {
|
|
1860
|
+
queues: (keyof TQueues)[];
|
|
1861
|
+
concurrency?: number;
|
|
1862
|
+
limiter?: IgniterJobsLimiter;
|
|
1863
|
+
}): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1864
|
+
withTelemetry(
|
|
1865
|
+
telemetry: IgniterJobsTelemetry,
|
|
1866
|
+
): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1867
|
+
withLogger(
|
|
1868
|
+
logger: IgniterLogger,
|
|
1869
|
+
): IgniterJobsBuilder<TContext, TQueues, TScope>;
|
|
1719
1870
|
build(): IgniterJobsRuntime<IgniterJobsConfig<TContext, TQueues, TScope>>;
|
|
1720
1871
|
}
|
|
1721
1872
|
```
|
|
@@ -1726,7 +1877,9 @@ class IgniterJobsBuilder<TContext, TQueues, TScope> {
|
|
|
1726
1877
|
|
|
1727
1878
|
```typescript
|
|
1728
1879
|
class IgniterQueue {
|
|
1729
|
-
static create<const TName extends string>(
|
|
1880
|
+
static create<const TName extends string>(
|
|
1881
|
+
name: TName,
|
|
1882
|
+
): IgniterQueueBuilder<unknown, {}, {}, TName>;
|
|
1730
1883
|
}
|
|
1731
1884
|
```
|
|
1732
1885
|
|
|
@@ -1736,13 +1889,23 @@ class IgniterQueue {
|
|
|
1736
1889
|
class IgniterQueueBuilder<TContext, TJobs, TCron, TName> {
|
|
1737
1890
|
addJob<TJobName extends string, TInput, TResult>(
|
|
1738
1891
|
jobName: TJobName,
|
|
1739
|
-
definition: IgniterJobDefinition<TContext, TInput, TResult
|
|
1740
|
-
): IgniterQueueBuilder<
|
|
1892
|
+
definition: IgniterJobDefinition<TContext, TInput, TResult>,
|
|
1893
|
+
): IgniterQueueBuilder<
|
|
1894
|
+
TContext,
|
|
1895
|
+
TJobs & Record<TJobName, IgniterJobDefinition<TContext, TInput, TResult>>,
|
|
1896
|
+
TCron,
|
|
1897
|
+
TName
|
|
1898
|
+
>;
|
|
1741
1899
|
|
|
1742
1900
|
addCron<TCronName extends string, TResult>(
|
|
1743
1901
|
cronName: TCronName,
|
|
1744
|
-
definition: IgniterCronDefinition<TContext, TResult
|
|
1745
|
-
): IgniterQueueBuilder<
|
|
1902
|
+
definition: IgniterCronDefinition<TContext, TResult>,
|
|
1903
|
+
): IgniterQueueBuilder<
|
|
1904
|
+
TContext,
|
|
1905
|
+
TJobs,
|
|
1906
|
+
TCron & Record<TCronName, IgniterCronDefinition<TContext, TResult>>,
|
|
1907
|
+
TName
|
|
1908
|
+
>;
|
|
1746
1909
|
|
|
1747
1910
|
build(): IgniterJobsQueue<TContext, TJobs, TCron> & { name: TName };
|
|
1748
1911
|
}
|
|
@@ -1754,10 +1917,17 @@ class IgniterQueueBuilder<TContext, TJobs, TCron, TName> {
|
|
|
1754
1917
|
interface IgniterJobsRuntime<TConfig> {
|
|
1755
1918
|
config: TConfig;
|
|
1756
1919
|
subscribe(handler: IgniterJobsEventHandler): Promise<() => Promise<void>>;
|
|
1757
|
-
search(
|
|
1920
|
+
search(
|
|
1921
|
+
target: "jobs" | "queues" | "workers",
|
|
1922
|
+
filter: Record<string, unknown>,
|
|
1923
|
+
): Promise<unknown[]>;
|
|
1758
1924
|
shutdown(): Promise<void>;
|
|
1759
1925
|
worker: { create(): IgniterWorkerBuilder<keyof TConfig["queues"] & string> };
|
|
1760
|
-
scope(
|
|
1926
|
+
scope(
|
|
1927
|
+
type: string,
|
|
1928
|
+
id: string | number,
|
|
1929
|
+
tags?: Record<string, unknown>,
|
|
1930
|
+
): IgniterJobsRuntime<TConfig>;
|
|
1761
1931
|
|
|
1762
1932
|
// Queue accessors (dynamic)
|
|
1763
1933
|
[queueName: string]: IgniterJobsQueueAccessor<any>;
|
|
@@ -1777,7 +1947,11 @@ interface IgniterJobsQueueAccessor {
|
|
|
1777
1947
|
obliterate(options?: { force?: boolean }): Promise<void>;
|
|
1778
1948
|
retryAll(): Promise<number>;
|
|
1779
1949
|
};
|
|
1780
|
-
list(filter?: {
|
|
1950
|
+
list(filter?: {
|
|
1951
|
+
status?: IgniterJobStatus[];
|
|
1952
|
+
limit?: number;
|
|
1953
|
+
offset?: number;
|
|
1954
|
+
}): Promise<IgniterJobSearchResult[]>;
|
|
1781
1955
|
subscribe(handler: IgniterJobsEventHandler): Promise<() => Promise<void>>;
|
|
1782
1956
|
jobs: Record<string, IgniterJobsJobAccessor>;
|
|
1783
1957
|
}
|
|
@@ -1800,8 +1974,6 @@ interface IgniterJobsJobAccessor<TInput = unknown> {
|
|
|
1800
1974
|
logs(): Promise<IgniterJobsJobLog[]>;
|
|
1801
1975
|
};
|
|
1802
1976
|
many(ids: string[]): { retry(): Promise<void>; remove(): Promise<void> };
|
|
1803
|
-
pause(): Promise<void>;
|
|
1804
|
-
resume(): Promise<void>;
|
|
1805
1977
|
subscribe(handler: IgniterJobsEventHandler): Promise<() => Promise<void>>;
|
|
1806
1978
|
}
|
|
1807
1979
|
```
|
|
@@ -1811,12 +1983,30 @@ interface IgniterJobsJobAccessor<TInput = unknown> {
|
|
|
1811
1983
|
```typescript
|
|
1812
1984
|
interface IgniterJobsWorkerFluentBuilder<TQueueNames extends string> {
|
|
1813
1985
|
addQueue(queue: TQueueNames): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1814
|
-
withConcurrency(
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1986
|
+
withConcurrency(
|
|
1987
|
+
concurrency: number,
|
|
1988
|
+
): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1989
|
+
withLimiter(
|
|
1990
|
+
limiter: IgniterJobsLimiter,
|
|
1991
|
+
): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1992
|
+
onActive(
|
|
1993
|
+
handler: (ctx: { job: IgniterJobSearchResult }) => void | Promise<void>,
|
|
1994
|
+
): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1995
|
+
onSuccess(
|
|
1996
|
+
handler: (ctx: {
|
|
1997
|
+
job: IgniterJobSearchResult;
|
|
1998
|
+
result: unknown;
|
|
1999
|
+
}) => void | Promise<void>,
|
|
2000
|
+
): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
2001
|
+
onFailure(
|
|
2002
|
+
handler: (ctx: {
|
|
2003
|
+
job: IgniterJobSearchResult;
|
|
2004
|
+
error: Error;
|
|
2005
|
+
}) => void | Promise<void>,
|
|
2006
|
+
): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
2007
|
+
onIdle(
|
|
2008
|
+
handler: () => void | Promise<void>,
|
|
2009
|
+
): IgniterJobsWorkerFluentBuilder<TQueueNames>;
|
|
1820
2010
|
start(): Promise<IgniterJobsWorkerHandle>;
|
|
1821
2011
|
}
|
|
1822
2012
|
```
|
|
@@ -1825,13 +2015,17 @@ interface IgniterJobsWorkerFluentBuilder<TQueueNames extends string> {
|
|
|
1825
2015
|
|
|
1826
2016
|
## ⚙️ Configuration Reference
|
|
1827
2017
|
|
|
1828
|
-
###
|
|
2018
|
+
### IgniterJobsBunSQLiteAdapterOptions
|
|
1829
2019
|
|
|
1830
2020
|
```typescript
|
|
1831
|
-
interface
|
|
2021
|
+
interface IgniterJobsBunSQLiteAdapterOptions {
|
|
1832
2022
|
path: string;
|
|
1833
|
-
|
|
1834
|
-
|
|
2023
|
+
durable?: boolean;
|
|
2024
|
+
heartbeatInterval?: number;
|
|
2025
|
+
pollTimeout?: number;
|
|
2026
|
+
batchSize?: number;
|
|
2027
|
+
lockDuration?: number;
|
|
2028
|
+
maxStalledCount?: number;
|
|
1835
2029
|
}
|
|
1836
2030
|
```
|
|
1837
2031
|
|
|
@@ -1857,22 +2051,22 @@ interface IgniterJobsScheduleOptions {
|
|
|
1857
2051
|
|
|
1858
2052
|
## ✅ Best Practices
|
|
1859
2053
|
|
|
1860
|
-
| Do
|
|
1861
|
-
|
|
1862
|
-
| ✅ Use input schemas
|
|
1863
|
-
| ✅ Keep payloads small | Faster serialization | `{ id: "order_1" }`
|
|
1864
|
-
| ✅ Use scopes
|
|
1865
|
-
| ✅ Use retries
|
|
1866
|
-
| ✅ Use worker hooks
|
|
2054
|
+
| Do | Why | Example |
|
|
2055
|
+
| ---------------------- | -------------------- | ---------------------------- |
|
|
2056
|
+
| ✅ Use input schemas | Prevent invalid jobs | `input: z.object({ ... })` |
|
|
2057
|
+
| ✅ Keep payloads small | Faster serialization | `{ id: "order_1" }` |
|
|
2058
|
+
| ✅ Use scopes | Tenant isolation | `jobs.scope("org", "org_1")` |
|
|
2059
|
+
| ✅ Use retries | Resilience | `attempts: 5` |
|
|
2060
|
+
| ✅ Use worker hooks | Observability | `onFailure(...)` |
|
|
1867
2061
|
|
|
1868
2062
|
### Anti-Patterns
|
|
1869
2063
|
|
|
1870
|
-
| Don’t
|
|
1871
|
-
|
|
1872
|
-
| ❌ Store PII in metadata
|
|
1873
|
-
| ❌ Use sync I/O in handlers
|
|
1874
|
-
| ❌ Dispatch without schema
|
|
1875
|
-
| ❌ Long-running jobs without progress | No visibility
|
|
2064
|
+
| Don’t | Why | Alternative |
|
|
2065
|
+
| ------------------------------------- | ---------------------- | ------------------ |
|
|
2066
|
+
| ❌ Store PII in metadata | Metadata is observable | Store IDs only |
|
|
2067
|
+
| ❌ Use sync I/O in handlers | Blocks workers | Use async I/O |
|
|
2068
|
+
| ❌ Dispatch without schema | Runtime surprises | Add `input` schema |
|
|
2069
|
+
| ❌ Long-running jobs without progress | No visibility | Use `onProgress` |
|
|
1876
2070
|
|
|
1877
2071
|
---
|
|
1878
2072
|
|
|
@@ -1966,9 +2160,9 @@ Do not import this package in client-side bundles.
|
|
|
1966
2160
|
### Memory → SQLite
|
|
1967
2161
|
|
|
1968
2162
|
```typescript
|
|
1969
|
-
import {
|
|
2163
|
+
import { IgniterJobsBunSQLiteAdapter } from "@igniter-js/jobs/adapters/bun";
|
|
1970
2164
|
|
|
1971
|
-
const adapter =
|
|
2165
|
+
const adapter = IgniterJobsBunSQLiteAdapter.create({
|
|
1972
2166
|
path: "./jobs.sqlite",
|
|
1973
2167
|
});
|
|
1974
2168
|
```
|
|
@@ -1976,7 +2170,7 @@ const adapter = IgniterJobsSQLiteAdapter.create({
|
|
|
1976
2170
|
### SQLite → BullMQ
|
|
1977
2171
|
|
|
1978
2172
|
```typescript
|
|
1979
|
-
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters";
|
|
2173
|
+
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters/node";
|
|
1980
2174
|
import Redis from "ioredis";
|
|
1981
2175
|
|
|
1982
2176
|
const adapter = IgniterJobsBullMQAdapter.create({
|
|
@@ -2012,7 +2206,7 @@ Yes. The adapter interface is stable and jobs/queues remain unchanged.
|
|
|
2012
2206
|
|
|
2013
2207
|
```typescript
|
|
2014
2208
|
import { IgniterJobs, IgniterQueue } from "@igniter-js/jobs";
|
|
2015
|
-
import {
|
|
2209
|
+
import { IgniterJobsBunSQLiteAdapter } from "@igniter-js/jobs/adapters/bun";
|
|
2016
2210
|
import { z } from "zod";
|
|
2017
2211
|
|
|
2018
2212
|
type AppContext = { uploads: { process: (id: string) => Promise<void> } };
|
|
@@ -2027,7 +2221,7 @@ const queue = IgniterQueue.create("uploads")
|
|
|
2027
2221
|
.build();
|
|
2028
2222
|
|
|
2029
2223
|
const jobs = IgniterJobs.create()
|
|
2030
|
-
.withAdapter(
|
|
2224
|
+
.withAdapter(IgniterJobsBunSQLiteAdapter.create({ path: "./jobs.sqlite" }))
|
|
2031
2225
|
.withService("uploader")
|
|
2032
2226
|
.withEnvironment("local")
|
|
2033
2227
|
.withContext(async () => ({ uploads }))
|
|
@@ -2054,14 +2248,14 @@ await jobs.shutdown();
|
|
|
2054
2248
|
|
|
2055
2249
|
### Job Events (Runtime)
|
|
2056
2250
|
|
|
2057
|
-
| Event
|
|
2058
|
-
|
|
2059
|
-
| `enqueued`
|
|
2060
|
-
| `scheduled` | After schedule
|
|
2061
|
-
| `started`
|
|
2062
|
-
| `completed` | After handler
|
|
2063
|
-
| `failed`
|
|
2064
|
-
| `progress`
|
|
2251
|
+
| Event | When | Payload Keys |
|
|
2252
|
+
| ----------- | ---------------- | ---------------------------------------------------------------------------------------------- |
|
|
2253
|
+
| `enqueued` | After dispatch | `jobId`, `queue`, `jobName` |
|
|
2254
|
+
| `scheduled` | After schedule | `jobId`, `queue`, `jobName` |
|
|
2255
|
+
| `started` | Before handler | `jobId`, `jobName`, `queue`, `attemptsMade`, `startedAt` |
|
|
2256
|
+
| `completed` | After handler | `jobId`, `jobName`, `queue`, `result`, `duration`, `completedAt` |
|
|
2257
|
+
| `failed` | On error | `jobId`, `jobName`, `queue`, `error`, `attemptsMade`, `isFinalAttempt`, `duration`, `failedAt` |
|
|
2258
|
+
| `progress` | On progress hook | `jobId`, `jobName`, `queue`, `progress`, `message`, `timestamp` |
|
|
2065
2259
|
|
|
2066
2260
|
### Telemetry Event Attributes
|
|
2067
2261
|
|
|
@@ -2104,22 +2298,22 @@ await jobs.shutdown();
|
|
|
2104
2298
|
|
|
2105
2299
|
## 📑 Appendix: Adapter API Matrix
|
|
2106
2300
|
|
|
2107
|
-
| API
|
|
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()` | ✅
|
|
2301
|
+
| API | Memory | SQLite | BullMQ |
|
|
2302
|
+
| ------------------- | ------ | ------ | ------ |
|
|
2303
|
+
| `dispatch()` | ✅ | ✅ | ✅ |
|
|
2304
|
+
| `schedule()` | ✅ | ✅ | ✅ |
|
|
2305
|
+
| `getJob()` | ✅ | ✅ | ✅ |
|
|
2306
|
+
| `getJobState()` | ✅ | ✅ | ✅ |
|
|
2307
|
+
| `getJobLogs()` | ✅ | ✅ | ✅ |
|
|
2308
|
+
| `getJobProgress()` | ✅ | ✅ | ✅ |
|
|
2309
|
+
| `pauseJobType()` | ✅ | ✅ | ❌ |
|
|
2310
|
+
| `resumeJobType()` | ✅ | ✅ | ❌ |
|
|
2311
|
+
| `pauseQueue()` | ✅ | ✅ | ✅ |
|
|
2312
|
+
| `resumeQueue()` | ✅ | ✅ | ✅ |
|
|
2313
|
+
| `drainQueue()` | ✅ | ✅ | ✅ |
|
|
2314
|
+
| `cleanQueue()` | ✅ | ✅ | ✅ |
|
|
2315
|
+
| `obliterateQueue()` | ✅ | ✅ | ✅ |
|
|
2316
|
+
| `retryAllInQueue()` | ✅ | ✅ | ✅ |
|
|
2123
2317
|
|
|
2124
2318
|
---
|
|
2125
2319
|
|