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