@igniter-js/jobs 0.1.1 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1118 -96
- package/CHANGELOG.md +8 -0
- package/README.md +2146 -93
- package/dist/{adapter-PiDCQWQd.d.mts → adapter-CXZxomI9.d.mts} +2 -2
- package/dist/{adapter-PiDCQWQd.d.ts → adapter-CXZxomI9.d.ts} +2 -2
- package/dist/adapters/bullmq.adapter.d.mts +2 -2
- package/dist/adapters/bullmq.adapter.d.ts +2 -2
- package/dist/adapters/bullmq.adapter.js +2 -2
- package/dist/adapters/bullmq.adapter.js.map +1 -1
- package/dist/adapters/bullmq.adapter.mjs +1 -1
- package/dist/adapters/bullmq.adapter.mjs.map +1 -1
- package/dist/adapters/index.d.mts +140 -2
- package/dist/adapters/index.d.ts +140 -2
- package/dist/adapters/index.js +864 -31
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs +863 -31
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/adapters/memory.adapter.d.mts +2 -2
- package/dist/adapters/memory.adapter.d.ts +2 -2
- package/dist/adapters/memory.adapter.js +122 -30
- package/dist/adapters/memory.adapter.js.map +1 -1
- package/dist/adapters/memory.adapter.mjs +121 -29
- package/dist/adapters/memory.adapter.mjs.map +1 -1
- package/dist/index.d.mts +452 -342
- package/dist/index.d.ts +452 -342
- package/dist/index.js +1923 -1002
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1921 -1001
- package/dist/index.mjs.map +1 -1
- package/dist/shim.d.mts +36 -0
- package/dist/shim.d.ts +36 -0
- package/dist/shim.js +75 -0
- package/dist/shim.js.map +1 -0
- package/dist/shim.mjs +67 -0
- package/dist/shim.mjs.map +1 -0
- package/dist/telemetry/index.d.mts +281 -0
- package/dist/telemetry/index.d.ts +281 -0
- package/dist/telemetry/index.js +97 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/index.mjs +95 -0
- package/dist/telemetry/index.mjs.map +1 -0
- package/package.json +44 -11
package/dist/adapters/index.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createBullMQAdapter } from '@igniter-js/adapter-bullmq';
|
|
2
|
-
import { IgniterError } from '@igniter-js/
|
|
2
|
+
import { IgniterError } from '@igniter-js/common';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
5
10
|
|
|
6
11
|
// src/utils/prefix.ts
|
|
7
12
|
var _IgniterJobsPrefix = class _IgniterJobsPrefix {
|
|
@@ -653,10 +658,16 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
653
658
|
async retryJob(jobId, queue) {
|
|
654
659
|
const job = this.jobsById.get(jobId);
|
|
655
660
|
if (!job) {
|
|
656
|
-
throw new IgniterJobsError({
|
|
661
|
+
throw new IgniterJobsError({
|
|
662
|
+
code: "JOBS_NOT_FOUND",
|
|
663
|
+
message: `Job "${jobId}" not found.`
|
|
664
|
+
});
|
|
657
665
|
}
|
|
658
666
|
if (queue && job.queue !== queue) {
|
|
659
|
-
throw new IgniterJobsError({
|
|
667
|
+
throw new IgniterJobsError({
|
|
668
|
+
code: "JOBS_NOT_FOUND",
|
|
669
|
+
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
670
|
+
});
|
|
660
671
|
}
|
|
661
672
|
job.status = "waiting";
|
|
662
673
|
job.error = void 0;
|
|
@@ -670,15 +681,25 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
670
681
|
if (queue && job.queue !== queue) return;
|
|
671
682
|
this.jobsById.delete(jobId);
|
|
672
683
|
const list = this.jobsByQueue.get(job.queue);
|
|
673
|
-
if (list)
|
|
684
|
+
if (list)
|
|
685
|
+
this.jobsByQueue.set(
|
|
686
|
+
job.queue,
|
|
687
|
+
list.filter((id) => id !== jobId)
|
|
688
|
+
);
|
|
674
689
|
}
|
|
675
690
|
async promoteJob(jobId, queue) {
|
|
676
691
|
const job = this.jobsById.get(jobId);
|
|
677
692
|
if (!job) {
|
|
678
|
-
throw new IgniterJobsError({
|
|
693
|
+
throw new IgniterJobsError({
|
|
694
|
+
code: "JOBS_NOT_FOUND",
|
|
695
|
+
message: `Job "${jobId}" not found.`
|
|
696
|
+
});
|
|
679
697
|
}
|
|
680
698
|
if (queue && job.queue !== queue) {
|
|
681
|
-
throw new IgniterJobsError({
|
|
699
|
+
throw new IgniterJobsError({
|
|
700
|
+
code: "JOBS_NOT_FOUND",
|
|
701
|
+
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
702
|
+
});
|
|
682
703
|
}
|
|
683
704
|
if (job.status === "delayed" || job.status === "paused") {
|
|
684
705
|
job.status = this.pausedQueues.has(job.queue) ? "paused" : "waiting";
|
|
@@ -688,10 +709,16 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
688
709
|
async moveJobToFailed(jobId, reason, queue) {
|
|
689
710
|
const job = this.jobsById.get(jobId);
|
|
690
711
|
if (!job) {
|
|
691
|
-
throw new IgniterJobsError({
|
|
712
|
+
throw new IgniterJobsError({
|
|
713
|
+
code: "JOBS_NOT_FOUND",
|
|
714
|
+
message: `Job "${jobId}" not found.`
|
|
715
|
+
});
|
|
692
716
|
}
|
|
693
717
|
if (queue && job.queue !== queue) {
|
|
694
|
-
throw new IgniterJobsError({
|
|
718
|
+
throw new IgniterJobsError({
|
|
719
|
+
code: "JOBS_NOT_FOUND",
|
|
720
|
+
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
721
|
+
});
|
|
695
722
|
}
|
|
696
723
|
job.status = "failed";
|
|
697
724
|
job.error = reason;
|
|
@@ -731,7 +758,13 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
731
758
|
return counts;
|
|
732
759
|
}
|
|
733
760
|
async listQueues() {
|
|
734
|
-
const queues = Array.from(
|
|
761
|
+
const queues = Array.from(
|
|
762
|
+
/* @__PURE__ */ new Set([
|
|
763
|
+
...this.jobsByQueue.keys(),
|
|
764
|
+
...this.registeredJobs.keys(),
|
|
765
|
+
...this.registeredCrons.keys()
|
|
766
|
+
])
|
|
767
|
+
);
|
|
735
768
|
const result = [];
|
|
736
769
|
for (const q of queues) {
|
|
737
770
|
result.push(await this.getQueueInfo(q));
|
|
@@ -768,7 +801,10 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
768
801
|
removed++;
|
|
769
802
|
}
|
|
770
803
|
}
|
|
771
|
-
this.jobsByQueue.set(
|
|
804
|
+
this.jobsByQueue.set(
|
|
805
|
+
queue,
|
|
806
|
+
jobIds.filter((id) => this.jobsById.has(id))
|
|
807
|
+
);
|
|
772
808
|
return removed;
|
|
773
809
|
}
|
|
774
810
|
async cleanQueue(queue, options) {
|
|
@@ -788,7 +824,10 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
788
824
|
this.jobsById.delete(id);
|
|
789
825
|
cleaned++;
|
|
790
826
|
}
|
|
791
|
-
this.jobsByQueue.set(
|
|
827
|
+
this.jobsByQueue.set(
|
|
828
|
+
queue,
|
|
829
|
+
jobIds.filter((id) => this.jobsById.has(id))
|
|
830
|
+
);
|
|
792
831
|
return cleaned;
|
|
793
832
|
}
|
|
794
833
|
async obliterateQueue(queue, options) {
|
|
@@ -817,7 +856,8 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
817
856
|
for (const id of jobIds) {
|
|
818
857
|
const job = this.jobsById.get(id);
|
|
819
858
|
if (!job) continue;
|
|
820
|
-
if (job.name === jobName && job.status === "waiting")
|
|
859
|
+
if (job.name === jobName && job.status === "waiting")
|
|
860
|
+
job.status = "paused";
|
|
821
861
|
}
|
|
822
862
|
}
|
|
823
863
|
async resumeJobType(queue, jobName) {
|
|
@@ -825,7 +865,8 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
825
865
|
for (const id of jobIds) {
|
|
826
866
|
const job = this.jobsById.get(id);
|
|
827
867
|
if (!job) continue;
|
|
828
|
-
if (job.name === jobName && job.status === "paused")
|
|
868
|
+
if (job.name === jobName && job.status === "paused")
|
|
869
|
+
job.status = "waiting";
|
|
829
870
|
}
|
|
830
871
|
void this.kickWorkers(queue);
|
|
831
872
|
}
|
|
@@ -834,19 +875,25 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
834
875
|
const statuses = filter?.status;
|
|
835
876
|
const limit = filter?.limit ?? 100;
|
|
836
877
|
const offset = filter?.offset ?? 0;
|
|
837
|
-
const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort(
|
|
878
|
+
const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort(
|
|
879
|
+
(a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
|
|
880
|
+
);
|
|
838
881
|
return all.slice(offset, offset + limit).map((j) => this.toSearchResult(j));
|
|
839
882
|
}
|
|
840
883
|
async searchQueues(filter) {
|
|
841
884
|
const name = filter?.name;
|
|
842
885
|
const isPaused = filter?.isPaused;
|
|
843
886
|
const all = await this.listQueues();
|
|
844
|
-
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
887
|
+
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
888
|
+
(q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
|
|
889
|
+
);
|
|
845
890
|
}
|
|
846
891
|
async searchWorkers(filter) {
|
|
847
892
|
const queue = filter?.queue;
|
|
848
893
|
const isRunning = filter?.isRunning;
|
|
849
|
-
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
894
|
+
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
895
|
+
(w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
|
|
896
|
+
).map((w) => this.toWorkerHandle(w));
|
|
850
897
|
}
|
|
851
898
|
async createWorker(config) {
|
|
852
899
|
const workerId = IgniterJobsIdGenerator.generate("worker");
|
|
@@ -866,7 +913,8 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
866
913
|
}
|
|
867
914
|
getWorkers() {
|
|
868
915
|
const out = /* @__PURE__ */ new Map();
|
|
869
|
-
for (const [id, state] of this.workers)
|
|
916
|
+
for (const [id, state] of this.workers)
|
|
917
|
+
out.set(id, this.toWorkerHandle(state));
|
|
870
918
|
return out;
|
|
871
919
|
}
|
|
872
920
|
async publishEvent(channel, payload) {
|
|
@@ -941,7 +989,9 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
941
989
|
}
|
|
942
990
|
async kickWorkers(queue) {
|
|
943
991
|
if (this.pausedQueues.has(queue)) return;
|
|
944
|
-
const relevant = Array.from(this.workers.values()).filter(
|
|
992
|
+
const relevant = Array.from(this.workers.values()).filter(
|
|
993
|
+
(w) => !w.closed && !w.paused && (w.queues.length === 0 || w.queues.includes(queue))
|
|
994
|
+
);
|
|
945
995
|
if (relevant.length === 0) return;
|
|
946
996
|
for (const w of relevant) {
|
|
947
997
|
void this.processLoop(w, queue);
|
|
@@ -966,7 +1016,9 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
966
1016
|
}
|
|
967
1017
|
nextJob(queue) {
|
|
968
1018
|
const ids = this.jobsByQueue.get(queue) ?? [];
|
|
969
|
-
const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort(
|
|
1019
|
+
const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort(
|
|
1020
|
+
(a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
|
|
1021
|
+
);
|
|
970
1022
|
return candidates[0] ?? null;
|
|
971
1023
|
}
|
|
972
1024
|
async processJob(worker, job) {
|
|
@@ -977,8 +1029,13 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
977
1029
|
job.status = "active";
|
|
978
1030
|
job.startedAt = /* @__PURE__ */ new Date();
|
|
979
1031
|
job.attemptsMade += 1;
|
|
980
|
-
job.logs.push({
|
|
981
|
-
|
|
1032
|
+
job.logs.push({
|
|
1033
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1034
|
+
level: "info",
|
|
1035
|
+
message: "Job started"
|
|
1036
|
+
});
|
|
1037
|
+
if (worker.handlers?.onActive)
|
|
1038
|
+
await worker.handlers.onActive({ job: this.toSearchResult(job) });
|
|
982
1039
|
const start = Date.now();
|
|
983
1040
|
try {
|
|
984
1041
|
const definition = this.registeredJobs.get(job.queue)?.get(job.name);
|
|
@@ -992,7 +1049,13 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
992
1049
|
await definition.onStart({
|
|
993
1050
|
input: job.input,
|
|
994
1051
|
context: {},
|
|
995
|
-
job: {
|
|
1052
|
+
job: {
|
|
1053
|
+
id: job.id,
|
|
1054
|
+
name: job.name,
|
|
1055
|
+
queue: job.queue,
|
|
1056
|
+
attemptsMade: job.attemptsMade,
|
|
1057
|
+
metadata: job.metadata
|
|
1058
|
+
},
|
|
996
1059
|
scope: job.scope,
|
|
997
1060
|
startedAt: job.startedAt
|
|
998
1061
|
});
|
|
@@ -1000,7 +1063,13 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1000
1063
|
const result = await definition.handler({
|
|
1001
1064
|
input: job.input,
|
|
1002
1065
|
context: {},
|
|
1003
|
-
job: {
|
|
1066
|
+
job: {
|
|
1067
|
+
id: job.id,
|
|
1068
|
+
name: job.name,
|
|
1069
|
+
queue: job.queue,
|
|
1070
|
+
attemptsMade: job.attemptsMade,
|
|
1071
|
+
metadata: job.metadata
|
|
1072
|
+
},
|
|
1004
1073
|
scope: job.scope
|
|
1005
1074
|
});
|
|
1006
1075
|
const duration = Date.now() - start;
|
|
@@ -1008,23 +1077,41 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1008
1077
|
job.completedAt = /* @__PURE__ */ new Date();
|
|
1009
1078
|
job.result = result;
|
|
1010
1079
|
job.progress = 100;
|
|
1011
|
-
job.logs.push({
|
|
1080
|
+
job.logs.push({
|
|
1081
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1082
|
+
level: "info",
|
|
1083
|
+
message: `Job completed in ${duration}ms`
|
|
1084
|
+
});
|
|
1012
1085
|
worker.metrics.processed += 1;
|
|
1013
1086
|
worker.metrics.totalDuration += duration;
|
|
1014
1087
|
if (definition.onSuccess) {
|
|
1015
1088
|
await definition.onSuccess({
|
|
1016
1089
|
input: job.input,
|
|
1017
1090
|
context: {},
|
|
1018
|
-
job: {
|
|
1091
|
+
job: {
|
|
1092
|
+
id: job.id,
|
|
1093
|
+
name: job.name,
|
|
1094
|
+
queue: job.queue,
|
|
1095
|
+
attemptsMade: job.attemptsMade,
|
|
1096
|
+
metadata: job.metadata
|
|
1097
|
+
},
|
|
1019
1098
|
scope: job.scope,
|
|
1020
1099
|
result,
|
|
1021
1100
|
duration
|
|
1022
1101
|
});
|
|
1023
1102
|
}
|
|
1024
|
-
if (worker.handlers?.onSuccess)
|
|
1103
|
+
if (worker.handlers?.onSuccess)
|
|
1104
|
+
await worker.handlers.onSuccess({
|
|
1105
|
+
job: this.toSearchResult(job),
|
|
1106
|
+
result
|
|
1107
|
+
});
|
|
1025
1108
|
} catch (error) {
|
|
1026
1109
|
job.error = error?.message ?? String(error);
|
|
1027
|
-
job.logs.push({
|
|
1110
|
+
job.logs.push({
|
|
1111
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1112
|
+
level: "error",
|
|
1113
|
+
message: job.error ?? "Unknown error"
|
|
1114
|
+
});
|
|
1028
1115
|
const isFinalAttempt = job.attemptsMade >= job.maxAttempts;
|
|
1029
1116
|
if (isFinalAttempt) {
|
|
1030
1117
|
job.status = "failed";
|
|
@@ -1035,13 +1122,23 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1035
1122
|
await definition.onFailure({
|
|
1036
1123
|
input: job.input,
|
|
1037
1124
|
context: {},
|
|
1038
|
-
job: {
|
|
1125
|
+
job: {
|
|
1126
|
+
id: job.id,
|
|
1127
|
+
name: job.name,
|
|
1128
|
+
queue: job.queue,
|
|
1129
|
+
attemptsMade: job.attemptsMade,
|
|
1130
|
+
metadata: job.metadata
|
|
1131
|
+
},
|
|
1039
1132
|
scope: job.scope,
|
|
1040
1133
|
error,
|
|
1041
1134
|
isFinalAttempt: true
|
|
1042
1135
|
});
|
|
1043
1136
|
}
|
|
1044
|
-
if (worker.handlers?.onFailure)
|
|
1137
|
+
if (worker.handlers?.onFailure)
|
|
1138
|
+
await worker.handlers.onFailure({
|
|
1139
|
+
job: this.toSearchResult(job),
|
|
1140
|
+
error
|
|
1141
|
+
});
|
|
1045
1142
|
} else {
|
|
1046
1143
|
job.status = "waiting";
|
|
1047
1144
|
void this.kickWorkers(job.queue);
|
|
@@ -1050,6 +1147,741 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1050
1147
|
}
|
|
1051
1148
|
};
|
|
1052
1149
|
|
|
1053
|
-
|
|
1150
|
+
// src/adapters/sqlite.adapter.ts
|
|
1151
|
+
var IgniterJobsSQLiteAdapter = class _IgniterJobsSQLiteAdapter {
|
|
1152
|
+
constructor(options) {
|
|
1153
|
+
this.registeredJobs = /* @__PURE__ */ new Map();
|
|
1154
|
+
this.registeredCrons = /* @__PURE__ */ new Map();
|
|
1155
|
+
this.workers = /* @__PURE__ */ new Map();
|
|
1156
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
1157
|
+
this.pausedQueues = /* @__PURE__ */ new Set();
|
|
1158
|
+
this.queues = {
|
|
1159
|
+
list: async () => this.listQueues(),
|
|
1160
|
+
get: async (name) => this.getQueueInfo(name),
|
|
1161
|
+
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
1162
|
+
getJobs: async (name, filter) => {
|
|
1163
|
+
const statuses = filter?.status;
|
|
1164
|
+
const limit = filter?.limit ?? 100;
|
|
1165
|
+
const offset = filter?.offset ?? 0;
|
|
1166
|
+
const results = await this.searchJobs({
|
|
1167
|
+
queue: name,
|
|
1168
|
+
status: statuses,
|
|
1169
|
+
limit,
|
|
1170
|
+
offset
|
|
1171
|
+
});
|
|
1172
|
+
return results;
|
|
1173
|
+
},
|
|
1174
|
+
pause: async (name) => this.pauseQueue(name),
|
|
1175
|
+
resume: async (name) => this.resumeQueue(name),
|
|
1176
|
+
isPaused: async (name) => {
|
|
1177
|
+
const info = await this.getQueueInfo(name);
|
|
1178
|
+
return info?.isPaused ?? false;
|
|
1179
|
+
},
|
|
1180
|
+
drain: async (name) => this.drainQueue(name),
|
|
1181
|
+
clean: async (name, options) => this.cleanQueue(name, options),
|
|
1182
|
+
obliterate: async (name, options) => this.obliterateQueue(name, options)
|
|
1183
|
+
};
|
|
1184
|
+
this.options = {
|
|
1185
|
+
path: options.path,
|
|
1186
|
+
pollingInterval: options.pollingInterval ?? 500,
|
|
1187
|
+
enableWAL: options.enableWAL ?? true
|
|
1188
|
+
};
|
|
1189
|
+
this.client = {
|
|
1190
|
+
type: "sqlite",
|
|
1191
|
+
path: this.options.path
|
|
1192
|
+
};
|
|
1193
|
+
const Database = __require("better-sqlite3");
|
|
1194
|
+
this.db = new Database(this.options.path);
|
|
1195
|
+
this.initializeSchema();
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Creates a new SQLite adapter instance.
|
|
1199
|
+
*
|
|
1200
|
+
* @param options - Configuration options
|
|
1201
|
+
* @returns A new adapter instance
|
|
1202
|
+
*
|
|
1203
|
+
* @example
|
|
1204
|
+
* ```ts
|
|
1205
|
+
* // File-based database (persistent)
|
|
1206
|
+
* const adapter = IgniterJobsSQLiteAdapter.create({
|
|
1207
|
+
* path: './data/jobs.sqlite'
|
|
1208
|
+
* });
|
|
1209
|
+
*
|
|
1210
|
+
* // In-memory database (for testing)
|
|
1211
|
+
* const testAdapter = IgniterJobsSQLiteAdapter.create({
|
|
1212
|
+
* path: ':memory:'
|
|
1213
|
+
* });
|
|
1214
|
+
* ```
|
|
1215
|
+
*/
|
|
1216
|
+
static create(options) {
|
|
1217
|
+
return new _IgniterJobsSQLiteAdapter(options);
|
|
1218
|
+
}
|
|
1219
|
+
initializeSchema() {
|
|
1220
|
+
if (this.options.enableWAL) {
|
|
1221
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
1222
|
+
}
|
|
1223
|
+
this.db.exec(`
|
|
1224
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
1225
|
+
id TEXT PRIMARY KEY,
|
|
1226
|
+
name TEXT NOT NULL,
|
|
1227
|
+
queue TEXT NOT NULL,
|
|
1228
|
+
input TEXT NOT NULL,
|
|
1229
|
+
status TEXT NOT NULL DEFAULT 'waiting',
|
|
1230
|
+
progress REAL NOT NULL DEFAULT 0,
|
|
1231
|
+
attempts_made INTEGER NOT NULL DEFAULT 0,
|
|
1232
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
1233
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
1234
|
+
created_at TEXT NOT NULL,
|
|
1235
|
+
started_at TEXT,
|
|
1236
|
+
completed_at TEXT,
|
|
1237
|
+
scheduled_at TEXT,
|
|
1238
|
+
result TEXT,
|
|
1239
|
+
error TEXT,
|
|
1240
|
+
metadata TEXT,
|
|
1241
|
+
scope TEXT
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_queue_status ON jobs(queue, status);
|
|
1245
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
1246
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_scheduled_at ON jobs(scheduled_at);
|
|
1247
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_priority ON jobs(priority DESC, created_at ASC);
|
|
1248
|
+
`);
|
|
1249
|
+
this.db.exec(`
|
|
1250
|
+
CREATE TABLE IF NOT EXISTS job_logs (
|
|
1251
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1252
|
+
job_id TEXT NOT NULL,
|
|
1253
|
+
timestamp TEXT NOT NULL,
|
|
1254
|
+
level TEXT NOT NULL,
|
|
1255
|
+
message TEXT NOT NULL,
|
|
1256
|
+
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
CREATE INDEX IF NOT EXISTS idx_job_logs_job_id ON job_logs(job_id);
|
|
1260
|
+
`);
|
|
1261
|
+
this.db.exec(`
|
|
1262
|
+
CREATE TABLE IF NOT EXISTS paused_queues (
|
|
1263
|
+
name TEXT PRIMARY KEY
|
|
1264
|
+
);
|
|
1265
|
+
`);
|
|
1266
|
+
const pausedRows = this.db.prepare(
|
|
1267
|
+
"SELECT name FROM paused_queues"
|
|
1268
|
+
).all();
|
|
1269
|
+
for (const row of pausedRows) {
|
|
1270
|
+
this.pausedQueues.add(row.name);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
registerJob(queueName, jobName, definition) {
|
|
1274
|
+
const queueJobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
1275
|
+
if (queueJobs.has(jobName)) {
|
|
1276
|
+
throw new IgniterJobsError({
|
|
1277
|
+
code: "JOBS_DUPLICATE_JOB",
|
|
1278
|
+
message: `Job "${jobName}" already registered for queue "${queueName}".`
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
queueJobs.set(jobName, definition);
|
|
1282
|
+
this.registeredJobs.set(queueName, queueJobs);
|
|
1283
|
+
}
|
|
1284
|
+
registerCron(queueName, cronName, definition) {
|
|
1285
|
+
const queueCrons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
1286
|
+
if (queueCrons.has(cronName)) {
|
|
1287
|
+
throw new IgniterJobsError({
|
|
1288
|
+
code: "JOBS_INVALID_CRON",
|
|
1289
|
+
message: `Cron "${cronName}" already registered for queue "${queueName}".`
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
queueCrons.set(cronName, definition);
|
|
1293
|
+
this.registeredCrons.set(queueName, queueCrons);
|
|
1294
|
+
}
|
|
1295
|
+
async dispatch(params) {
|
|
1296
|
+
const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
|
|
1297
|
+
const maxAttempts = params.attempts ?? 1;
|
|
1298
|
+
const now = /* @__PURE__ */ new Date();
|
|
1299
|
+
let status = "waiting";
|
|
1300
|
+
let scheduledAt = null;
|
|
1301
|
+
if (this.pausedQueues.has(params.queue)) {
|
|
1302
|
+
status = "paused";
|
|
1303
|
+
} else if (params.delay && params.delay > 0) {
|
|
1304
|
+
status = "delayed";
|
|
1305
|
+
scheduledAt = new Date(now.getTime() + params.delay);
|
|
1306
|
+
}
|
|
1307
|
+
const stmt = this.db.prepare(`
|
|
1308
|
+
INSERT INTO jobs (
|
|
1309
|
+
id, name, queue, input, status, progress, attempts_made, max_attempts,
|
|
1310
|
+
priority, created_at, scheduled_at, metadata, scope
|
|
1311
|
+
) VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
|
|
1312
|
+
`);
|
|
1313
|
+
stmt.run(
|
|
1314
|
+
jobId,
|
|
1315
|
+
params.jobName,
|
|
1316
|
+
params.queue,
|
|
1317
|
+
JSON.stringify(params.input ?? {}),
|
|
1318
|
+
status,
|
|
1319
|
+
maxAttempts,
|
|
1320
|
+
params.priority ?? 0,
|
|
1321
|
+
now.toISOString(),
|
|
1322
|
+
scheduledAt?.toISOString() ?? null,
|
|
1323
|
+
params.metadata ? JSON.stringify(params.metadata) : null,
|
|
1324
|
+
params.scope ? JSON.stringify(params.scope) : null
|
|
1325
|
+
);
|
|
1326
|
+
if (params.delay && params.delay > 0) {
|
|
1327
|
+
setTimeout(() => {
|
|
1328
|
+
this.promoteDelayedJob(jobId, params.queue);
|
|
1329
|
+
}, params.delay);
|
|
1330
|
+
}
|
|
1331
|
+
return jobId;
|
|
1332
|
+
}
|
|
1333
|
+
promoteDelayedJob(jobId, queue) {
|
|
1334
|
+
if (this.pausedQueues.has(queue)) return;
|
|
1335
|
+
const stmt = this.db.prepare(`
|
|
1336
|
+
UPDATE jobs SET status = 'waiting', scheduled_at = NULL
|
|
1337
|
+
WHERE id = ? AND status = 'delayed'
|
|
1338
|
+
`);
|
|
1339
|
+
stmt.run(jobId);
|
|
1340
|
+
}
|
|
1341
|
+
async schedule(params) {
|
|
1342
|
+
if (params.at) {
|
|
1343
|
+
const delay = params.at.getTime() - Date.now();
|
|
1344
|
+
if (delay <= 0) {
|
|
1345
|
+
throw new IgniterJobsError({
|
|
1346
|
+
code: "JOBS_INVALID_SCHEDULE",
|
|
1347
|
+
message: "Scheduled time must be in the future."
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
return this.dispatch({ ...params, delay });
|
|
1351
|
+
}
|
|
1352
|
+
if (params.cron || params.every) {
|
|
1353
|
+
return this.dispatch({ ...params, delay: params.delay ?? 0 });
|
|
1354
|
+
}
|
|
1355
|
+
return this.dispatch(params);
|
|
1356
|
+
}
|
|
1357
|
+
async getJob(jobId, queue) {
|
|
1358
|
+
let sql = "SELECT * FROM jobs WHERE id = ?";
|
|
1359
|
+
const params = [jobId];
|
|
1360
|
+
if (queue) {
|
|
1361
|
+
sql += " AND queue = ?";
|
|
1362
|
+
params.push(queue);
|
|
1363
|
+
}
|
|
1364
|
+
const row = this.db.prepare(sql).get(...params);
|
|
1365
|
+
if (!row) return null;
|
|
1366
|
+
return this.rowToSearchResult(row);
|
|
1367
|
+
}
|
|
1368
|
+
async getJobState(jobId, queue) {
|
|
1369
|
+
let sql = "SELECT status FROM jobs WHERE id = ?";
|
|
1370
|
+
const params = [jobId];
|
|
1371
|
+
if (queue) {
|
|
1372
|
+
sql += " AND queue = ?";
|
|
1373
|
+
params.push(queue);
|
|
1374
|
+
}
|
|
1375
|
+
const row = this.db.prepare(sql).get(...params);
|
|
1376
|
+
return row?.status ?? null;
|
|
1377
|
+
}
|
|
1378
|
+
async getJobLogs(jobId, queue) {
|
|
1379
|
+
if (queue) {
|
|
1380
|
+
const job = await this.getJob(jobId, queue);
|
|
1381
|
+
if (!job) return [];
|
|
1382
|
+
}
|
|
1383
|
+
const rows = this.db.prepare("SELECT * FROM job_logs WHERE job_id = ? ORDER BY timestamp ASC").all(jobId);
|
|
1384
|
+
return rows.map((row) => ({
|
|
1385
|
+
timestamp: new Date(row.timestamp),
|
|
1386
|
+
level: row.level,
|
|
1387
|
+
message: row.message
|
|
1388
|
+
}));
|
|
1389
|
+
}
|
|
1390
|
+
async getJobProgress(jobId, queue) {
|
|
1391
|
+
let sql = "SELECT progress FROM jobs WHERE id = ?";
|
|
1392
|
+
const params = [jobId];
|
|
1393
|
+
if (queue) {
|
|
1394
|
+
sql += " AND queue = ?";
|
|
1395
|
+
params.push(queue);
|
|
1396
|
+
}
|
|
1397
|
+
const row = this.db.prepare(sql).get(...params);
|
|
1398
|
+
return row?.progress ?? 0;
|
|
1399
|
+
}
|
|
1400
|
+
async retryJob(jobId, queue) {
|
|
1401
|
+
let sql = "SELECT id FROM jobs WHERE id = ?";
|
|
1402
|
+
const checkParams = [jobId];
|
|
1403
|
+
if (queue) {
|
|
1404
|
+
sql += " AND queue = ?";
|
|
1405
|
+
checkParams.push(queue);
|
|
1406
|
+
}
|
|
1407
|
+
const exists = this.db.prepare(sql).get(...checkParams);
|
|
1408
|
+
if (!exists) {
|
|
1409
|
+
throw new IgniterJobsError({
|
|
1410
|
+
code: "JOBS_NOT_FOUND",
|
|
1411
|
+
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
let updateSql = "UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0 WHERE id = ?";
|
|
1415
|
+
const updateParams = [jobId];
|
|
1416
|
+
if (queue) {
|
|
1417
|
+
updateSql = updateSql.replace("WHERE id = ?", "WHERE id = ? AND queue = ?");
|
|
1418
|
+
updateParams.push(queue);
|
|
1419
|
+
}
|
|
1420
|
+
this.db.prepare(updateSql).run(...updateParams);
|
|
1421
|
+
}
|
|
1422
|
+
async removeJob(jobId, queue) {
|
|
1423
|
+
let sql = "DELETE FROM jobs WHERE id = ?";
|
|
1424
|
+
const params = [jobId];
|
|
1425
|
+
if (queue) {
|
|
1426
|
+
sql += " AND queue = ?";
|
|
1427
|
+
params.push(queue);
|
|
1428
|
+
}
|
|
1429
|
+
this.db.prepare(sql).run(...params);
|
|
1430
|
+
}
|
|
1431
|
+
async promoteJob(jobId, queue) {
|
|
1432
|
+
let sql = "SELECT id, status, queue FROM jobs WHERE id = ?";
|
|
1433
|
+
const checkParams = [jobId];
|
|
1434
|
+
if (queue) {
|
|
1435
|
+
sql += " AND queue = ?";
|
|
1436
|
+
checkParams.push(queue);
|
|
1437
|
+
}
|
|
1438
|
+
const row = this.db.prepare(sql).get(...checkParams);
|
|
1439
|
+
if (!row) {
|
|
1440
|
+
throw new IgniterJobsError({
|
|
1441
|
+
code: "JOBS_NOT_FOUND",
|
|
1442
|
+
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
if (row.status === "delayed" || row.status === "paused") {
|
|
1446
|
+
const newStatus = this.pausedQueues.has(row.queue) ? "paused" : "waiting";
|
|
1447
|
+
this.db.prepare("UPDATE jobs SET status = ?, scheduled_at = NULL WHERE id = ?").run(newStatus, jobId);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async moveJobToFailed(jobId, reason, queue) {
|
|
1451
|
+
let sql = "SELECT id FROM jobs WHERE id = ?";
|
|
1452
|
+
const checkParams = [jobId];
|
|
1453
|
+
if (queue) {
|
|
1454
|
+
sql += " AND queue = ?";
|
|
1455
|
+
checkParams.push(queue);
|
|
1456
|
+
}
|
|
1457
|
+
const exists = this.db.prepare(sql).get(...checkParams);
|
|
1458
|
+
if (!exists) {
|
|
1459
|
+
throw new IgniterJobsError({
|
|
1460
|
+
code: "JOBS_NOT_FOUND",
|
|
1461
|
+
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
this.db.prepare(`
|
|
1465
|
+
UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
|
|
1466
|
+
WHERE id = ?
|
|
1467
|
+
`).run(reason, (/* @__PURE__ */ new Date()).toISOString(), jobId);
|
|
1468
|
+
}
|
|
1469
|
+
async retryManyJobs(jobIds, queue) {
|
|
1470
|
+
await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
|
|
1471
|
+
}
|
|
1472
|
+
async removeManyJobs(jobIds, queue) {
|
|
1473
|
+
await Promise.all(jobIds.map((id) => this.removeJob(id, queue)));
|
|
1474
|
+
}
|
|
1475
|
+
async getQueueInfo(queue) {
|
|
1476
|
+
const counts = await this.getQueueJobCounts(queue);
|
|
1477
|
+
return {
|
|
1478
|
+
name: queue,
|
|
1479
|
+
isPaused: this.pausedQueues.has(queue),
|
|
1480
|
+
jobCounts: counts
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
async getQueueJobCounts(queue) {
|
|
1484
|
+
const counts = {
|
|
1485
|
+
waiting: 0,
|
|
1486
|
+
active: 0,
|
|
1487
|
+
completed: 0,
|
|
1488
|
+
failed: 0,
|
|
1489
|
+
delayed: 0,
|
|
1490
|
+
paused: 0
|
|
1491
|
+
};
|
|
1492
|
+
const rows = this.db.prepare(
|
|
1493
|
+
"SELECT status, COUNT(*) as count FROM jobs WHERE queue = ? GROUP BY status"
|
|
1494
|
+
).all(queue);
|
|
1495
|
+
for (const row of rows) {
|
|
1496
|
+
if (row.status in counts) {
|
|
1497
|
+
counts[row.status] = row.count;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return counts;
|
|
1501
|
+
}
|
|
1502
|
+
async listQueues() {
|
|
1503
|
+
const jobQueues = this.db.prepare("SELECT DISTINCT queue FROM jobs").all().map((r) => r.queue);
|
|
1504
|
+
const allQueues = /* @__PURE__ */ new Set([
|
|
1505
|
+
...jobQueues,
|
|
1506
|
+
...this.registeredJobs.keys(),
|
|
1507
|
+
...this.registeredCrons.keys()
|
|
1508
|
+
]);
|
|
1509
|
+
const result = [];
|
|
1510
|
+
for (const q of allQueues) {
|
|
1511
|
+
const info = await this.getQueueInfo(q);
|
|
1512
|
+
if (info) result.push(info);
|
|
1513
|
+
}
|
|
1514
|
+
return result;
|
|
1515
|
+
}
|
|
1516
|
+
async pauseQueue(queue) {
|
|
1517
|
+
this.pausedQueues.add(queue);
|
|
1518
|
+
this.db.prepare("INSERT OR IGNORE INTO paused_queues (name) VALUES (?)").run(queue);
|
|
1519
|
+
this.db.prepare(
|
|
1520
|
+
"UPDATE jobs SET status = 'paused' WHERE queue = ? AND status = 'waiting'"
|
|
1521
|
+
).run(queue);
|
|
1522
|
+
}
|
|
1523
|
+
async resumeQueue(queue) {
|
|
1524
|
+
this.pausedQueues.delete(queue);
|
|
1525
|
+
this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
|
|
1526
|
+
this.db.prepare(
|
|
1527
|
+
"UPDATE jobs SET status = 'waiting' WHERE queue = ? AND status = 'paused'"
|
|
1528
|
+
).run(queue);
|
|
1529
|
+
}
|
|
1530
|
+
async drainQueue(queue) {
|
|
1531
|
+
const result = this.db.prepare(
|
|
1532
|
+
"DELETE FROM jobs WHERE queue = ? AND status IN ('waiting', 'paused')"
|
|
1533
|
+
).run(queue);
|
|
1534
|
+
return result.changes;
|
|
1535
|
+
}
|
|
1536
|
+
async cleanQueue(queue, options) {
|
|
1537
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
1538
|
+
const olderThan = options.olderThan ?? 0;
|
|
1539
|
+
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
1540
|
+
const cutoffTime = new Date(Date.now() - olderThan).toISOString();
|
|
1541
|
+
const statusPlaceholders = statuses.map(() => "?").join(", ");
|
|
1542
|
+
let sql = `
|
|
1543
|
+
DELETE FROM jobs WHERE id IN (
|
|
1544
|
+
SELECT id FROM jobs
|
|
1545
|
+
WHERE queue = ? AND status IN (${statusPlaceholders}) AND created_at < ?
|
|
1546
|
+
ORDER BY created_at ASC
|
|
1547
|
+
LIMIT ?
|
|
1548
|
+
)
|
|
1549
|
+
`;
|
|
1550
|
+
const result = this.db.prepare(sql).run(
|
|
1551
|
+
queue,
|
|
1552
|
+
...statuses,
|
|
1553
|
+
cutoffTime,
|
|
1554
|
+
limit === Number.POSITIVE_INFINITY ? -1 : limit
|
|
1555
|
+
);
|
|
1556
|
+
return result.changes;
|
|
1557
|
+
}
|
|
1558
|
+
async obliterateQueue(queue, _options) {
|
|
1559
|
+
this.db.prepare("DELETE FROM jobs WHERE queue = ?").run(queue);
|
|
1560
|
+
this.registeredJobs.delete(queue);
|
|
1561
|
+
this.registeredCrons.delete(queue);
|
|
1562
|
+
this.pausedQueues.delete(queue);
|
|
1563
|
+
this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
|
|
1564
|
+
}
|
|
1565
|
+
async retryAllInQueue(queue) {
|
|
1566
|
+
const result = this.db.prepare(`
|
|
1567
|
+
UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0
|
|
1568
|
+
WHERE queue = ? AND status = 'failed'
|
|
1569
|
+
`).run(queue);
|
|
1570
|
+
return result.changes;
|
|
1571
|
+
}
|
|
1572
|
+
async pauseJobType(queue, jobName) {
|
|
1573
|
+
this.db.prepare(
|
|
1574
|
+
"UPDATE jobs SET status = 'paused' WHERE queue = ? AND name = ? AND status = 'waiting'"
|
|
1575
|
+
).run(queue, jobName);
|
|
1576
|
+
}
|
|
1577
|
+
async resumeJobType(queue, jobName) {
|
|
1578
|
+
this.db.prepare(
|
|
1579
|
+
"UPDATE jobs SET status = 'waiting' WHERE queue = ? AND name = ? AND status = 'paused'"
|
|
1580
|
+
).run(queue, jobName);
|
|
1581
|
+
}
|
|
1582
|
+
async searchJobs(filter) {
|
|
1583
|
+
const queue = filter?.queue;
|
|
1584
|
+
const statuses = filter?.status;
|
|
1585
|
+
const limit = filter?.limit ?? 100;
|
|
1586
|
+
const offset = filter?.offset ?? 0;
|
|
1587
|
+
let sql = "SELECT * FROM jobs WHERE 1=1";
|
|
1588
|
+
const params = [];
|
|
1589
|
+
if (queue) {
|
|
1590
|
+
sql += " AND queue = ?";
|
|
1591
|
+
params.push(queue);
|
|
1592
|
+
}
|
|
1593
|
+
if (statuses && statuses.length > 0) {
|
|
1594
|
+
const placeholders = statuses.map(() => "?").join(", ");
|
|
1595
|
+
sql += ` AND status IN (${placeholders})`;
|
|
1596
|
+
params.push(...statuses);
|
|
1597
|
+
}
|
|
1598
|
+
sql += " ORDER BY priority DESC, created_at ASC LIMIT ? OFFSET ?";
|
|
1599
|
+
params.push(limit, offset);
|
|
1600
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1601
|
+
return rows.map((row) => this.rowToSearchResult(row));
|
|
1602
|
+
}
|
|
1603
|
+
async searchQueues(filter) {
|
|
1604
|
+
const name = filter?.name;
|
|
1605
|
+
const isPaused = filter?.isPaused;
|
|
1606
|
+
const all = await this.listQueues();
|
|
1607
|
+
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
1608
|
+
(q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
async searchWorkers(filter) {
|
|
1612
|
+
const queue = filter?.queue;
|
|
1613
|
+
const isRunning = filter?.isRunning;
|
|
1614
|
+
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
1615
|
+
(w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
|
|
1616
|
+
).map((w) => this.toWorkerHandle(w));
|
|
1617
|
+
}
|
|
1618
|
+
async createWorker(config) {
|
|
1619
|
+
const workerId = IgniterJobsIdGenerator.generate("worker");
|
|
1620
|
+
const state = {
|
|
1621
|
+
id: workerId,
|
|
1622
|
+
queues: config.queues ?? [],
|
|
1623
|
+
concurrency: config.concurrency ?? 1,
|
|
1624
|
+
paused: false,
|
|
1625
|
+
closed: false,
|
|
1626
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
1627
|
+
metrics: { processed: 0, failed: 0, totalDuration: 0 },
|
|
1628
|
+
handlers: config.handlers,
|
|
1629
|
+
activeJobs: 0
|
|
1630
|
+
};
|
|
1631
|
+
this.workers.set(workerId, state);
|
|
1632
|
+
this.startPollingLoop(state);
|
|
1633
|
+
return this.toWorkerHandle(state);
|
|
1634
|
+
}
|
|
1635
|
+
getWorkers() {
|
|
1636
|
+
const out = /* @__PURE__ */ new Map();
|
|
1637
|
+
for (const [id, state] of this.workers) {
|
|
1638
|
+
out.set(id, this.toWorkerHandle(state));
|
|
1639
|
+
}
|
|
1640
|
+
return out;
|
|
1641
|
+
}
|
|
1642
|
+
async publishEvent(channel, payload) {
|
|
1643
|
+
const handlers = this.subscribers.get(channel);
|
|
1644
|
+
if (!handlers) return;
|
|
1645
|
+
await Promise.all(Array.from(handlers).map(async (h) => h(payload)));
|
|
1646
|
+
}
|
|
1647
|
+
async subscribeEvent(channel, handler) {
|
|
1648
|
+
const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
|
|
1649
|
+
set.add(handler);
|
|
1650
|
+
this.subscribers.set(channel, set);
|
|
1651
|
+
return async () => {
|
|
1652
|
+
const current = this.subscribers.get(channel);
|
|
1653
|
+
if (!current) return;
|
|
1654
|
+
current.delete(handler);
|
|
1655
|
+
if (current.size === 0) this.subscribers.delete(channel);
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
async shutdown() {
|
|
1659
|
+
for (const worker of this.workers.values()) {
|
|
1660
|
+
worker.closed = true;
|
|
1661
|
+
if (worker.pollingTimer) {
|
|
1662
|
+
clearInterval(worker.pollingTimer);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
this.workers.clear();
|
|
1666
|
+
this.subscribers.clear();
|
|
1667
|
+
this.db.close();
|
|
1668
|
+
}
|
|
1669
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1670
|
+
// Private helpers
|
|
1671
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1672
|
+
rowToSearchResult(row) {
|
|
1673
|
+
return {
|
|
1674
|
+
id: row.id,
|
|
1675
|
+
name: row.name,
|
|
1676
|
+
queue: row.queue,
|
|
1677
|
+
status: row.status,
|
|
1678
|
+
input: JSON.parse(row.input),
|
|
1679
|
+
result: row.result ? JSON.parse(row.result) : void 0,
|
|
1680
|
+
error: row.error ?? void 0,
|
|
1681
|
+
progress: row.progress,
|
|
1682
|
+
attemptsMade: row.attempts_made,
|
|
1683
|
+
priority: row.priority,
|
|
1684
|
+
createdAt: new Date(row.created_at),
|
|
1685
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1686
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : void 0,
|
|
1687
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
1688
|
+
scope: row.scope ? JSON.parse(row.scope) : void 0
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
toWorkerHandle(worker) {
|
|
1692
|
+
return {
|
|
1693
|
+
id: worker.id,
|
|
1694
|
+
queues: worker.queues,
|
|
1695
|
+
pause: async () => {
|
|
1696
|
+
worker.paused = true;
|
|
1697
|
+
},
|
|
1698
|
+
resume: async () => {
|
|
1699
|
+
worker.paused = false;
|
|
1700
|
+
},
|
|
1701
|
+
close: async () => {
|
|
1702
|
+
worker.closed = true;
|
|
1703
|
+
if (worker.pollingTimer) {
|
|
1704
|
+
clearInterval(worker.pollingTimer);
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
isRunning: () => !worker.closed && !worker.paused,
|
|
1708
|
+
isPaused: () => worker.paused,
|
|
1709
|
+
isClosed: () => worker.closed,
|
|
1710
|
+
getMetrics: async () => this.toWorkerMetrics(worker)
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
toWorkerMetrics(worker) {
|
|
1714
|
+
const uptime = Date.now() - worker.startedAt.getTime();
|
|
1715
|
+
const processed = worker.metrics.processed;
|
|
1716
|
+
return {
|
|
1717
|
+
processed,
|
|
1718
|
+
failed: worker.metrics.failed,
|
|
1719
|
+
avgDuration: processed > 0 ? worker.metrics.totalDuration / processed : 0,
|
|
1720
|
+
concurrency: worker.concurrency,
|
|
1721
|
+
uptime
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
startPollingLoop(worker) {
|
|
1725
|
+
const poll = () => {
|
|
1726
|
+
if (worker.closed || worker.paused) return;
|
|
1727
|
+
void this.processNextJobs(worker);
|
|
1728
|
+
};
|
|
1729
|
+
poll();
|
|
1730
|
+
worker.pollingTimer = setInterval(poll, this.options.pollingInterval);
|
|
1731
|
+
}
|
|
1732
|
+
async processNextJobs(worker) {
|
|
1733
|
+
if (worker.closed || worker.paused) return;
|
|
1734
|
+
const availableSlots = worker.concurrency - worker.activeJobs;
|
|
1735
|
+
if (availableSlots <= 0) return;
|
|
1736
|
+
const queueFilter = worker.queues.length > 0 ? `queue IN (${worker.queues.map(() => "?").join(", ")})` : "1=1";
|
|
1737
|
+
const params = worker.queues.length > 0 ? [...worker.queues, availableSlots] : [availableSlots];
|
|
1738
|
+
const rows = this.db.prepare(`
|
|
1739
|
+
SELECT * FROM jobs
|
|
1740
|
+
WHERE status = 'waiting' AND ${queueFilter}
|
|
1741
|
+
ORDER BY priority DESC, created_at ASC
|
|
1742
|
+
LIMIT ?
|
|
1743
|
+
`).all(...params);
|
|
1744
|
+
for (const row of rows) {
|
|
1745
|
+
if (worker.closed || worker.paused) break;
|
|
1746
|
+
if (worker.activeJobs >= worker.concurrency) break;
|
|
1747
|
+
const claimed = this.db.prepare(`
|
|
1748
|
+
UPDATE jobs SET status = 'active', started_at = ?
|
|
1749
|
+
WHERE id = ? AND status = 'waiting'
|
|
1750
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), row.id);
|
|
1751
|
+
if (claimed.changes === 0) continue;
|
|
1752
|
+
worker.activeJobs++;
|
|
1753
|
+
void this.processJob(worker, row.id).finally(() => {
|
|
1754
|
+
worker.activeJobs--;
|
|
1755
|
+
if (worker.handlers?.onIdle && worker.activeJobs === 0) {
|
|
1756
|
+
void worker.handlers.onIdle();
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async processJob(worker, jobId) {
|
|
1762
|
+
const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(jobId);
|
|
1763
|
+
if (!row) return;
|
|
1764
|
+
const job = this.rowToSearchResult(row);
|
|
1765
|
+
this.addJobLog(jobId, "info", "Job started");
|
|
1766
|
+
if (worker.handlers?.onActive) {
|
|
1767
|
+
await worker.handlers.onActive({ job });
|
|
1768
|
+
}
|
|
1769
|
+
const start = Date.now();
|
|
1770
|
+
try {
|
|
1771
|
+
const definition = this.registeredJobs.get(row.queue)?.get(row.name);
|
|
1772
|
+
if (!definition) {
|
|
1773
|
+
throw new IgniterJobsError({
|
|
1774
|
+
code: "JOBS_NOT_REGISTERED",
|
|
1775
|
+
message: `Job "${row.name}" is not registered for queue "${row.queue}".`
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
this.db.prepare("UPDATE jobs SET attempts_made = attempts_made + 1 WHERE id = ?").run(jobId);
|
|
1779
|
+
if (definition.onStart) {
|
|
1780
|
+
await definition.onStart({
|
|
1781
|
+
input: job.input,
|
|
1782
|
+
context: {},
|
|
1783
|
+
job: {
|
|
1784
|
+
id: job.id,
|
|
1785
|
+
name: job.name,
|
|
1786
|
+
queue: job.queue,
|
|
1787
|
+
attemptsMade: job.attemptsMade + 1,
|
|
1788
|
+
metadata: job.metadata
|
|
1789
|
+
},
|
|
1790
|
+
scope: job.scope,
|
|
1791
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
const result = await definition.handler({
|
|
1795
|
+
input: job.input,
|
|
1796
|
+
context: {},
|
|
1797
|
+
job: {
|
|
1798
|
+
id: job.id,
|
|
1799
|
+
name: job.name,
|
|
1800
|
+
queue: job.queue,
|
|
1801
|
+
attemptsMade: job.attemptsMade + 1,
|
|
1802
|
+
metadata: job.metadata
|
|
1803
|
+
},
|
|
1804
|
+
scope: job.scope
|
|
1805
|
+
});
|
|
1806
|
+
const duration = Date.now() - start;
|
|
1807
|
+
this.db.prepare(`
|
|
1808
|
+
UPDATE jobs SET status = 'completed', completed_at = ?, result = ?, progress = 100
|
|
1809
|
+
WHERE id = ?
|
|
1810
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), JSON.stringify(result), jobId);
|
|
1811
|
+
this.addJobLog(jobId, "info", `Job completed in ${duration}ms`);
|
|
1812
|
+
worker.metrics.processed++;
|
|
1813
|
+
worker.metrics.totalDuration += duration;
|
|
1814
|
+
if (definition.onSuccess) {
|
|
1815
|
+
await definition.onSuccess({
|
|
1816
|
+
input: job.input,
|
|
1817
|
+
context: {},
|
|
1818
|
+
job: {
|
|
1819
|
+
id: job.id,
|
|
1820
|
+
name: job.name,
|
|
1821
|
+
queue: job.queue,
|
|
1822
|
+
attemptsMade: job.attemptsMade + 1,
|
|
1823
|
+
metadata: job.metadata
|
|
1824
|
+
},
|
|
1825
|
+
scope: job.scope,
|
|
1826
|
+
result,
|
|
1827
|
+
duration
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
if (worker.handlers?.onSuccess) {
|
|
1831
|
+
const updatedJob = await this.getJob(jobId);
|
|
1832
|
+
if (updatedJob) {
|
|
1833
|
+
await worker.handlers.onSuccess({ job: updatedJob, result });
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
const errorMessage = error?.message ?? String(error);
|
|
1838
|
+
this.addJobLog(jobId, "error", errorMessage);
|
|
1839
|
+
const current = this.db.prepare(
|
|
1840
|
+
"SELECT attempts_made, max_attempts FROM jobs WHERE id = ?"
|
|
1841
|
+
).get(jobId);
|
|
1842
|
+
const isFinalAttempt = (current?.attempts_made ?? 0) >= (current?.max_attempts ?? 1);
|
|
1843
|
+
if (isFinalAttempt) {
|
|
1844
|
+
this.db.prepare(`
|
|
1845
|
+
UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
|
|
1846
|
+
WHERE id = ?
|
|
1847
|
+
`).run(errorMessage, (/* @__PURE__ */ new Date()).toISOString(), jobId);
|
|
1848
|
+
worker.metrics.failed++;
|
|
1849
|
+
const definition = this.registeredJobs.get(row.queue)?.get(row.name);
|
|
1850
|
+
if (definition?.onFailure) {
|
|
1851
|
+
await definition.onFailure({
|
|
1852
|
+
input: job.input,
|
|
1853
|
+
context: {},
|
|
1854
|
+
job: {
|
|
1855
|
+
id: job.id,
|
|
1856
|
+
name: job.name,
|
|
1857
|
+
queue: job.queue,
|
|
1858
|
+
attemptsMade: current?.attempts_made ?? 1,
|
|
1859
|
+
metadata: job.metadata
|
|
1860
|
+
},
|
|
1861
|
+
scope: job.scope,
|
|
1862
|
+
error,
|
|
1863
|
+
isFinalAttempt: true
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
if (worker.handlers?.onFailure) {
|
|
1867
|
+
const updatedJob = await this.getJob(jobId);
|
|
1868
|
+
if (updatedJob) {
|
|
1869
|
+
await worker.handlers.onFailure({ job: updatedJob, error });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
} else {
|
|
1873
|
+
this.db.prepare("UPDATE jobs SET status = 'waiting' WHERE id = ?").run(jobId);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
addJobLog(jobId, level, message) {
|
|
1878
|
+
this.db.prepare(`
|
|
1879
|
+
INSERT INTO job_logs (job_id, timestamp, level, message)
|
|
1880
|
+
VALUES (?, ?, ?, ?)
|
|
1881
|
+
`).run(jobId, (/* @__PURE__ */ new Date()).toISOString(), level, message);
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
export { IgniterJobsBullMQAdapter, IgniterJobsMemoryAdapter, IgniterJobsSQLiteAdapter };
|
|
1054
1886
|
//# sourceMappingURL=index.mjs.map
|
|
1055
1887
|
//# sourceMappingURL=index.mjs.map
|