tasklane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+
3
+ var bullmq = require('bullmq');
4
+
5
+ // src/registry.ts
6
+ var _config = null;
7
+ var _connection = null;
8
+ var _queue = null;
9
+ var _worker = null;
10
+ var _flowProducer = null;
11
+ var _handlers = /* @__PURE__ */ new Map();
12
+ var _failedListeners = [];
13
+ var _completedListeners = [];
14
+ function parseRedis(url) {
15
+ const parsed = new URL(url);
16
+ return {
17
+ host: parsed.hostname || "127.0.0.1",
18
+ port: parsed.port ? parseInt(parsed.port, 10) : 6379,
19
+ password: parsed.password || void 0,
20
+ db: parsed.pathname && parsed.pathname !== "/" ? parseInt(parsed.pathname.slice(1), 10) : 0
21
+ };
22
+ }
23
+ function requireQueue() {
24
+ if (!_queue) {
25
+ throw new Error(
26
+ `[tasklane] initJobs() has not been called. Call initJobs({ redis }) before dispatching jobs.`
27
+ );
28
+ }
29
+ return _queue;
30
+ }
31
+ function requireFlowProducer() {
32
+ if (!_flowProducer) {
33
+ throw new Error(
34
+ `[tasklane] initJobs() has not been called. Call initJobs({ redis }) before dispatching flows.`
35
+ );
36
+ }
37
+ return _flowProducer;
38
+ }
39
+ function defaultBackoff(override) {
40
+ return override ?? _config?.backoff ?? { type: "exponential", delay: 1e3 };
41
+ }
42
+ function defaultJobOpts(attempts, backoff) {
43
+ return {
44
+ attempts: attempts ?? _config?.attempts ?? 3,
45
+ backoff: defaultBackoff(backoff),
46
+ removeOnComplete: { count: 1e3 },
47
+ removeOnFail: { count: 5e3 }
48
+ };
49
+ }
50
+ function toFlowJob(node) {
51
+ return {
52
+ name: node.job.jobName,
53
+ queueName: _config?.queue ?? "default",
54
+ data: { args: node.args ?? [] },
55
+ opts: defaultJobOpts(node.attempts, node.backoff),
56
+ children: node.children?.map(toFlowJob)
57
+ };
58
+ }
59
+ function configure(cfg) {
60
+ _config = cfg;
61
+ _connection = parseRedis(cfg.redis);
62
+ _queue = new bullmq.Queue(cfg.queue ?? "default", { connection: _connection });
63
+ _flowProducer = new bullmq.FlowProducer({ connection: _connection });
64
+ }
65
+ function resetListeners() {
66
+ _failedListeners.length = 0;
67
+ _completedListeners.length = 0;
68
+ }
69
+ function registerHandler(name, fn) {
70
+ _handlers.set(name, fn);
71
+ }
72
+ async function enqueue(name, args, opts) {
73
+ await requireQueue().add(name, { args }, {
74
+ ...defaultJobOpts(opts?.attempts, opts?.backoff),
75
+ delay: opts?.delay
76
+ });
77
+ }
78
+ async function enqueueScheduled(name, cronPattern, args, opts) {
79
+ await requireQueue().upsertJobScheduler(
80
+ `${name}::cron`,
81
+ { pattern: cronPattern },
82
+ {
83
+ name,
84
+ data: { args },
85
+ opts: defaultJobOpts(opts?.attempts, opts?.backoff)
86
+ }
87
+ );
88
+ }
89
+ async function dispatchFlow(node) {
90
+ await requireFlowProducer().add(toFlowJob(node));
91
+ }
92
+ async function startWorker() {
93
+ if (!_config || !_connection) {
94
+ throw new Error(`[tasklane] initJobs() has not been called before startWorker().`);
95
+ }
96
+ if (_worker) return;
97
+ _worker = new bullmq.Worker(
98
+ _config.queue ?? "default",
99
+ async (bullJob) => {
100
+ const handler = _handlers.get(bullJob.name);
101
+ if (!handler) {
102
+ throw new Error(
103
+ `[tasklane] No handler registered for job "${bullJob.name}". Make sure the file defining this job is imported before startWorker() is called.`
104
+ );
105
+ }
106
+ const args = bullJob.data.args ?? [];
107
+ return handler(...args);
108
+ },
109
+ { connection: _connection }
110
+ );
111
+ _worker.on("error", (err) => {
112
+ _failedListeners.forEach(
113
+ (fn) => fn({
114
+ jobId: "worker",
115
+ name: "worker-error",
116
+ args: [],
117
+ error: err,
118
+ attemptsMade: 0,
119
+ isFinalFailure: false
120
+ })
121
+ );
122
+ });
123
+ _worker.on("failed", (bullJob, err) => {
124
+ if (!bullJob) return;
125
+ const isFinalFailure = bullJob.attemptsMade >= (bullJob.opts.attempts ?? 1);
126
+ const event = {
127
+ jobId: bullJob.id,
128
+ name: bullJob.name,
129
+ args: bullJob.data.args ?? [],
130
+ error: err,
131
+ attemptsMade: bullJob.attemptsMade,
132
+ isFinalFailure
133
+ };
134
+ _failedListeners.forEach((fn) => fn(event));
135
+ });
136
+ _worker.on("completed", (bullJob, result) => {
137
+ const event = {
138
+ jobId: bullJob.id,
139
+ name: bullJob.name,
140
+ args: bullJob.data.args ?? [],
141
+ result
142
+ };
143
+ _completedListeners.forEach((fn) => fn(event));
144
+ });
145
+ await _worker.waitUntilReady();
146
+ }
147
+ function onFailed(listener) {
148
+ _failedListeners.push(listener);
149
+ return () => {
150
+ const i = _failedListeners.indexOf(listener);
151
+ if (i !== -1) _failedListeners.splice(i, 1);
152
+ };
153
+ }
154
+ function onCompleted(listener) {
155
+ _completedListeners.push(listener);
156
+ return () => {
157
+ const i = _completedListeners.indexOf(listener);
158
+ if (i !== -1) _completedListeners.splice(i, 1);
159
+ };
160
+ }
161
+ async function getFailed(start = 0, limit = 50) {
162
+ const queue = requireQueue();
163
+ const jobs = await queue.getFailed(start, start + limit - 1);
164
+ return jobs.map((j) => ({
165
+ jobId: j.id,
166
+ name: j.name,
167
+ args: j.data.args ?? [],
168
+ failedReason: j.failedReason ?? "Unknown error",
169
+ attemptsMade: j.attemptsMade,
170
+ timestamp: j.timestamp,
171
+ finishedOn: j.finishedOn
172
+ }));
173
+ }
174
+ async function retryJob(jobId) {
175
+ const queue = requireQueue();
176
+ const job2 = await queue.getJob(jobId);
177
+ if (!job2) {
178
+ throw new Error(`[tasklane] retryJob: no job found with id "${jobId}"`);
179
+ }
180
+ await job2.retry();
181
+ }
182
+ async function stopAll() {
183
+ await Promise.all([
184
+ _worker?.close(),
185
+ _queue?.close(),
186
+ _flowProducer?.close()
187
+ ]);
188
+ _worker = null;
189
+ _queue = null;
190
+ _flowProducer = null;
191
+ _connection = null;
192
+ _config = null;
193
+ }
194
+
195
+ // src/job.ts
196
+ function job(fn, opts) {
197
+ const name = fn.name;
198
+ if (!name) {
199
+ throw new Error(
200
+ `[tasklane] job() requires a named function. Use: job(async function myJobName(...) { ... }) \u2014 not an arrow function.`
201
+ );
202
+ }
203
+ registerHandler(name, fn);
204
+ const attempts = opts?.attempts;
205
+ const backoff = opts?.backoff;
206
+ const proxy = async (...args) => {
207
+ await enqueue(name, args, { attempts, backoff });
208
+ };
209
+ return Object.assign(proxy, {
210
+ /** The registered job name — used by flow() to build the job tree */
211
+ jobName: name,
212
+ /** Run the handler directly, bypassing the queue entirely */
213
+ run: (...args) => fn(...args),
214
+ delay: (ms) => async (...args) => {
215
+ await enqueue(name, args, { delay: ms, attempts, backoff });
216
+ },
217
+ at: (date) => async (...args) => {
218
+ const delay = date.getTime() - Date.now();
219
+ if (delay < 0) throw new Error(`[tasklane] .at() date is in the past`);
220
+ await enqueue(name, args, { delay, attempts, backoff });
221
+ },
222
+ cron: (pattern) => async (...args) => {
223
+ await enqueueScheduled(name, pattern, args, { attempts, backoff });
224
+ }
225
+ });
226
+ }
227
+
228
+ // src/index.ts
229
+ function initJobs(config) {
230
+ configure(config);
231
+ }
232
+ async function startWorker2() {
233
+ return startWorker();
234
+ }
235
+ async function stopWorker() {
236
+ resetListeners();
237
+ return stopAll();
238
+ }
239
+ function onJobFailed(listener) {
240
+ return onFailed(listener);
241
+ }
242
+ function onJobCompleted(listener) {
243
+ return onCompleted(listener);
244
+ }
245
+ async function getFailed2(start = 0, limit = 50) {
246
+ return getFailed(start, limit);
247
+ }
248
+ async function retryJob2(jobId) {
249
+ return retryJob(jobId);
250
+ }
251
+ async function flow(node) {
252
+ return dispatchFlow(node);
253
+ }
254
+
255
+ exports.flow = flow;
256
+ exports.getFailed = getFailed2;
257
+ exports.initJobs = initJobs;
258
+ exports.job = job;
259
+ exports.onJobCompleted = onJobCompleted;
260
+ exports.onJobFailed = onJobFailed;
261
+ exports.retryJob = retryJob2;
262
+ exports.startWorker = startWorker2;
263
+ exports.stopWorker = stopWorker;
264
+ //# sourceMappingURL=index.cjs.map
265
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry.ts","../src/job.ts","../src/index.ts"],"names":["Queue","FlowProducer","Worker","job","startWorker","getFailed","retryJob"],"mappings":";;;;;AAMA,IAAI,OAAA,GAA6B,IAAA;AACjC,IAAI,WAAA,GAAwC,IAAA;AAC5C,IAAI,MAAA,GAAuB,IAAA;AAC3B,IAAI,OAAA,GAAyB,IAAA;AAC7B,IAAI,aAAA,GAAqC,IAAA;AAEzC,IAAM,SAAA,uBAAgB,GAAA,EAAsD;AAC5E,IAAM,mBAAiD,EAAC;AACxD,IAAM,sBAAuD,EAAC;AAI9D,SAAS,WAAW,GAAA,EAAgC;AAClD,EAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAO,QAAA,IAAY,WAAA;AAAA,IACzB,MAAM,MAAA,CAAO,IAAA,GAAO,SAAS,MAAA,CAAO,IAAA,EAAM,EAAE,CAAA,GAAI,IAAA;AAAA,IAChD,QAAA,EAAU,OAAO,QAAA,IAAY,MAAA;AAAA,IAC7B,EAAA,EAAI,MAAA,CAAO,QAAA,IAAY,MAAA,CAAO,QAAA,KAAa,GAAA,GAAM,QAAA,CAAS,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,EAAG,EAAE,CAAA,GAAI;AAAA,GAC5F;AACF;AAEA,SAAS,YAAA,GAAsB;AAC7B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,4FAAA;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,mBAAA,GAAoC;AAC3C,EAAA,IAAI,CAAC,aAAA,EAAe;AAClB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,6FAAA;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,aAAA;AACT;AAEA,SAAS,eAAe,QAAA,EAA4B;AAClD,EAAA,OAAO,YAAY,OAAA,EAAS,OAAA,IAAW,EAAE,IAAA,EAAM,aAAA,EAAwB,OAAO,GAAA,EAAK;AACrF;AAEA,SAAS,cAAA,CAAe,UAAmB,OAAA,EAA2B;AACpE,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,QAAA,IAAY,OAAA,EAAS,QAAA,IAAY,CAAA;AAAA,IAC3C,OAAA,EAAS,eAAe,OAAO,CAAA;AAAA,IAC/B,gBAAA,EAAkB,EAAE,KAAA,EAAO,GAAA,EAAK;AAAA,IAChC,YAAA,EAAc,EAAE,KAAA,EAAO,GAAA;AAAK,GAC9B;AACF;AAEA,SAAS,UAAU,IAAA,EAAyB;AAC1C,EAAA,OAAO;AAAA,IACL,IAAA,EAAO,KAAK,GAAA,CAA4B,OAAA;AAAA,IACxC,SAAA,EAAW,SAAS,KAAA,IAAS,SAAA;AAAA,IAC7B,MAAM,EAAE,IAAA,EAAM,IAAA,CAAK,IAAA,IAAQ,EAAC,EAAE;AAAA,IAC9B,IAAA,EAAM,cAAA,CAAe,IAAA,CAAK,QAAA,EAAU,KAAK,OAAO,CAAA;AAAA,IAChD,QAAA,EAAU,IAAA,CAAK,QAAA,EAAU,GAAA,CAAI,SAAS;AAAA,GACxC;AACF;AAIO,SAAS,UAAU,GAAA,EAAuB;AAC/C,EAAA,OAAA,GAAU,GAAA;AACV,EAAA,WAAA,GAAc,UAAA,CAAW,IAAI,KAAK,CAAA;AAClC,EAAA,MAAA,GAAS,IAAIA,aAAM,GAAA,CAAI,KAAA,IAAS,WAAW,EAAE,UAAA,EAAY,aAAa,CAAA;AACtE,EAAA,aAAA,GAAgB,IAAIC,mBAAA,CAAa,EAAE,UAAA,EAAY,aAAa,CAAA;AAC9D;AAEO,SAAS,cAAA,GAAuB;AACrC,EAAA,gBAAA,CAAiB,MAAA,GAAS,CAAA;AAC1B,EAAA,mBAAA,CAAoB,MAAA,GAAS,CAAA;AAC/B;AAEO,SAAS,eAAA,CACd,MACA,EAAA,EACM;AACN,EAAA,SAAA,CAAU,GAAA,CAAI,MAAM,EAAE,CAAA;AACxB;AAEA,eAAsB,OAAA,CACpB,IAAA,EACA,IAAA,EACA,IAAA,EACe;AACf,EAAA,MAAM,cAAa,CAAE,GAAA,CAAI,IAAA,EAAM,EAAE,MAAK,EAAG;AAAA,IACvC,GAAG,cAAA,CAAe,IAAA,EAAM,QAAA,EAAU,MAAM,OAAO,CAAA;AAAA,IAC/C,OAAO,IAAA,EAAM;AAAA,GACd,CAAA;AACH;AAEA,eAAsB,gBAAA,CACpB,IAAA,EACA,WAAA,EACA,IAAA,EACA,IAAA,EACe;AACf,EAAA,MAAM,cAAa,CAAE,kBAAA;AAAA,IACnB,GAAG,IAAI,CAAA,MAAA,CAAA;AAAA,IACP,EAAE,SAAS,WAAA,EAAY;AAAA,IACvB;AAAA,MACE,IAAA;AAAA,MACA,IAAA,EAAM,EAAE,IAAA,EAAK;AAAA,MACb,IAAA,EAAM,cAAA,CAAe,IAAA,EAAM,QAAA,EAAU,MAAM,OAAO;AAAA;AACpD,GACF;AACF;AAEA,eAAsB,aAAa,IAAA,EAA+B;AAChE,EAAA,MAAM,mBAAA,EAAoB,CAAE,GAAA,CAAI,SAAA,CAAU,IAAI,CAAC,CAAA;AACjD;AAEA,eAAsB,WAAA,GAA6B;AACjD,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,WAAA,EAAa;AAC5B,IAAA,MAAM,IAAI,MAAM,CAAA,+DAAA,CAAiE,CAAA;AAAA,EACnF;AACA,EAAA,IAAI,OAAA,EAAS;AAEb,EAAA,OAAA,GAAU,IAAIC,aAAA;AAAA,IACZ,QAAQ,KAAA,IAAS,SAAA;AAAA,IACjB,OAAO,OAAA,KAAY;AACjB,MAAA,MAAM,OAAA,GAAU,SAAA,CAAU,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAA;AAC1C,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,0CAAA,EAA6C,QAAQ,IAAI,CAAA,mFAAA;AAAA,SAE3D;AAAA,MACF;AACA,MAAA,MAAM,IAAA,GAAQ,OAAA,CAAQ,IAAA,CAA6B,IAAA,IAAQ,EAAC;AAC5D,MAAA,OAAO,OAAA,CAAQ,GAAG,IAAI,CAAA;AAAA,IACxB,CAAA;AAAA,IACA,EAAE,YAAY,WAAA;AAAY,GAC5B;AAEA,EAAA,OAAA,CAAQ,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAQ;AAC3B,IAAA,gBAAA,CAAiB,OAAA;AAAA,MAAQ,CAAC,OACxB,EAAA,CAAG;AAAA,QACD,KAAA,EAAO,QAAA;AAAA,QACP,IAAA,EAAM,cAAA;AAAA,QACN,MAAM,EAAC;AAAA,QACP,KAAA,EAAO,GAAA;AAAA,QACP,YAAA,EAAc,CAAA;AAAA,QACd,cAAA,EAAgB;AAAA,OACjB;AAAA,KACH;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAA,CAAQ,EAAA,CAAG,QAAA,EAAU,CAAC,OAAA,EAAS,GAAA,KAAQ;AACrC,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,MAAM,cAAA,GAAiB,OAAA,CAAQ,YAAA,KAAiB,OAAA,CAAQ,KAAK,QAAA,IAAY,CAAA,CAAA;AACzE,IAAA,MAAM,KAAA,GAAqB;AAAA,MACzB,OAAO,OAAA,CAAQ,EAAA;AAAA,MACf,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,IAAA,EAAO,OAAA,CAAQ,IAAA,CAA6B,IAAA,IAAQ,EAAC;AAAA,MACrD,KAAA,EAAO,GAAA;AAAA,MACP,cAAc,OAAA,CAAQ,YAAA;AAAA,MACtB;AAAA,KACF;AACA,IAAA,gBAAA,CAAiB,OAAA,CAAQ,CAAC,EAAA,KAAO,EAAA,CAAG,KAAK,CAAC,CAAA;AAAA,EAC5C,CAAC,CAAA;AAED,EAAA,OAAA,CAAQ,EAAA,CAAG,WAAA,EAAa,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,IAAA,MAAM,KAAA,GAAwB;AAAA,MAC5B,OAAO,OAAA,CAAQ,EAAA;AAAA,MACf,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,IAAA,EAAO,OAAA,CAAQ,IAAA,CAA6B,IAAA,IAAQ,EAAC;AAAA,MACrD;AAAA,KACF;AACA,IAAA,mBAAA,CAAoB,OAAA,CAAQ,CAAC,EAAA,KAAO,EAAA,CAAG,KAAK,CAAC,CAAA;AAAA,EAC/C,CAAC,CAAA;AAED,EAAA,MAAM,QAAQ,cAAA,EAAe;AAC/B;AAEO,SAAS,SAAS,QAAA,EAAgD;AACvE,EAAA,gBAAA,CAAiB,KAAK,QAAQ,CAAA;AAC9B,EAAA,OAAO,MAAM;AACX,IAAA,MAAM,CAAA,GAAI,gBAAA,CAAiB,OAAA,CAAQ,QAAQ,CAAA;AAC3C,IAAA,IAAI,CAAA,KAAM,EAAA,EAAI,gBAAA,CAAiB,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,EAC5C,CAAA;AACF;AAEO,SAAS,YAAY,QAAA,EAAmD;AAC7E,EAAA,mBAAA,CAAoB,KAAK,QAAQ,CAAA;AACjC,EAAA,OAAO,MAAM;AACX,IAAA,MAAM,CAAA,GAAI,mBAAA,CAAoB,OAAA,CAAQ,QAAQ,CAAA;AAC9C,IAAA,IAAI,CAAA,KAAM,EAAA,EAAI,mBAAA,CAAoB,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,EAC/C,CAAA;AACF;AAEA,eAAsB,SAAA,CAAU,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,EAAA,EAA0B;AAC3E,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,MAAM,OAAO,MAAM,KAAA,CAAM,UAAU,KAAA,EAAO,KAAA,GAAQ,QAAQ,CAAC,CAAA;AAC3D,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IACtB,OAAO,CAAA,CAAE,EAAA;AAAA,IACT,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,IAAA,EAAO,CAAA,CAAE,IAAA,CAA6B,IAAA,IAAQ,EAAC;AAAA,IAC/C,YAAA,EAAc,EAAE,YAAA,IAAgB,eAAA;AAAA,IAChC,cAAc,CAAA,CAAE,YAAA;AAAA,IAChB,WAAW,CAAA,CAAE,SAAA;AAAA,IACb,YAAY,CAAA,CAAE;AAAA,GAChB,CAAE,CAAA;AACJ;AAEA,eAAsB,SAAS,KAAA,EAA8B;AAC3D,EAAA,MAAM,QAAQ,YAAA,EAAa;AAC3B,EAAA,MAAMC,IAAAA,GAAM,MAAM,KAAA,CAAM,MAAA,CAAO,KAAK,CAAA;AACpC,EAAA,IAAI,CAACA,IAAAA,EAAK;AACR,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,2CAAA,EAA8C,KAAK,CAAA,CAAA,CAAG,CAAA;AAAA,EACxE;AACA,EAAA,MAAMA,KAAI,KAAA,EAAM;AAClB;AAEA,eAAsB,OAAA,GAAyB;AAC7C,EAAA,MAAM,QAAQ,GAAA,CAAI;AAAA,IAChB,SAAS,KAAA,EAAM;AAAA,IACf,QAAQ,KAAA,EAAM;AAAA,IACd,eAAe,KAAA;AAAM,GACtB,CAAA;AACD,EAAA,OAAA,GAAU,IAAA;AACV,EAAA,MAAA,GAAS,IAAA;AACT,EAAA,aAAA,GAAgB,IAAA;AAChB,EAAA,WAAA,GAAc,IAAA;AACd,EAAA,OAAA,GAAU,IAAA;AACZ;;;AC1MO,SAAS,GAAA,CACd,IACA,IAAA,EACU;AACV,EAAA,MAAM,OAAO,EAAA,CAAG,IAAA;AAEhB,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,yHAAA;AAAA,KAEF;AAAA,EACF;AAEA,EAAA,eAAA,CAAgB,MAAM,EAAE,CAAA;AAGxB,EAAA,MAAM,WAAW,IAAA,EAAM,QAAA;AACvB,EAAA,MAAM,UAAuC,IAAA,EAAM,OAAA;AAEnD,EAAA,MAAM,KAAA,GAAQ,UAAU,IAAA,KAAuC;AAC7D,IAAA,MAAM,QAAQ,IAAA,EAAM,IAAA,EAAM,EAAE,QAAA,EAAU,SAAS,CAAA;AAAA,EACjD,CAAA;AAEA,EAAA,OAAO,MAAA,CAAO,OAAO,KAAA,EAAO;AAAA;AAAA,IAE1B,OAAA,EAAS,IAAA;AAAA;AAAA,IAGT,GAAA,EAAK,CAAA,GAAI,IAAA,KAAuC,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IAE1D,KAAA,EAAO,CAAC,EAAA,KACN,OAAA,GAAU,IAAA,KAAuC;AAC/C,MAAA,MAAM,OAAA,CAAQ,MAAM,IAAA,EAAM,EAAE,OAAO,EAAA,EAAI,QAAA,EAAU,SAAS,CAAA;AAAA,IAC5D,CAAA;AAAA,IAEF,EAAA,EAAI,CAAC,IAAA,KACH,OAAA,GAAU,IAAA,KAAuC;AAC/C,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,EAAQ,GAAI,KAAK,GAAA,EAAI;AACxC,MAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,MAAM,IAAI,MAAM,CAAA,oCAAA,CAAsC,CAAA;AACrE,MAAA,MAAM,QAAQ,IAAA,EAAM,IAAA,EAAM,EAAE,KAAA,EAAO,QAAA,EAAU,SAAS,CAAA;AAAA,IACxD,CAAA;AAAA,IAEF,IAAA,EAAM,CAAC,OAAA,KACL,OAAA,GAAU,IAAA,KAAuC;AAC/C,MAAA,MAAM,iBAAiB,IAAA,EAAM,OAAA,EAAS,MAAM,EAAE,QAAA,EAAU,SAAS,CAAA;AAAA,IACnE;AAAA,GACH,CAAA;AACH;;;AC3CO,SAAS,SAAS,MAAA,EAA0B;AACjD,EAAA,SAAA,CAAU,MAAM,CAAA;AAClB;AAMA,eAAsBC,YAAAA,GAA6B;AACjD,EAAA,OAAO,WAAA,EAAa;AACtB;AAMA,eAAsB,UAAA,GAA4B;AAChD,EAAA,cAAA,EAAe;AACf,EAAA,OAAO,OAAA,EAAQ;AACjB;AAYO,SAAS,YAAY,QAAA,EAAoD;AAC9E,EAAA,OAAO,SAAS,QAAQ,CAAA;AAC1B;AAKO,SAAS,eAAe,QAAA,EAAuD;AACpF,EAAA,OAAO,YAAY,QAAQ,CAAA;AAC7B;AAQA,eAAsBC,UAAAA,CAAU,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,EAAA,EAAI;AACrD,EAAA,OAAO,SAAA,CAAW,OAAO,KAAK,CAAA;AAChC;AAMA,eAAsBC,UAAS,KAAA,EAA8B;AAC3D,EAAA,OAAO,SAAU,KAAK,CAAA;AACxB;AA0BA,eAAsB,KAAK,IAAA,EAA+B;AACxD,EAAA,OAAO,aAAa,IAAI,CAAA;AAC1B","file":"index.cjs","sourcesContent":["import { Queue, Worker, FlowProducer } from \"bullmq\";\nimport type { ConnectionOptions, FlowJob } from \"bullmq\";\nimport type { JobsConfig, FailedEvent, CompletedEvent, FailedJob, BackoffStrategy, FlowNode } from \"./types.js\";\n\n// --- Global singleton state ---\n\nlet _config: JobsConfig | null = null;\nlet _connection: ConnectionOptions | null = null;\nlet _queue: Queue | null = null;\nlet _worker: Worker | null = null;\nlet _flowProducer: FlowProducer | null = null;\n\nconst _handlers = new Map<string, (...args: unknown[]) => Promise<unknown>>();\nconst _failedListeners: ((e: FailedEvent) => void)[] = [];\nconst _completedListeners: ((e: CompletedEvent) => void)[] = [];\n\n// --- Internal helpers ---\n\nfunction parseRedis(url: string): ConnectionOptions {\n const parsed = new URL(url);\n return {\n host: parsed.hostname || \"127.0.0.1\",\n port: parsed.port ? parseInt(parsed.port, 10) : 6379,\n password: parsed.password || undefined,\n db: parsed.pathname && parsed.pathname !== \"/\" ? parseInt(parsed.pathname.slice(1), 10) : 0,\n };\n}\n\nfunction requireQueue(): Queue {\n if (!_queue) {\n throw new Error(\n `[tasklane] initJobs() has not been called. Call initJobs({ redis }) before dispatching jobs.`\n );\n }\n return _queue;\n}\n\nfunction requireFlowProducer(): FlowProducer {\n if (!_flowProducer) {\n throw new Error(\n `[tasklane] initJobs() has not been called. Call initJobs({ redis }) before dispatching flows.`\n );\n }\n return _flowProducer;\n}\n\nfunction defaultBackoff(override?: BackoffStrategy) {\n return override ?? _config?.backoff ?? { type: \"exponential\" as const, delay: 1000 };\n}\n\nfunction defaultJobOpts(attempts?: number, backoff?: BackoffStrategy) {\n return {\n attempts: attempts ?? _config?.attempts ?? 3,\n backoff: defaultBackoff(backoff),\n removeOnComplete: { count: 1000 },\n removeOnFail: { count: 5000 },\n };\n}\n\nfunction toFlowJob(node: FlowNode): FlowJob {\n return {\n name: (node.job as { jobName: string }).jobName,\n queueName: _config?.queue ?? \"default\",\n data: { args: node.args ?? [] },\n opts: defaultJobOpts(node.attempts, node.backoff),\n children: node.children?.map(toFlowJob),\n };\n}\n\n// --- Public registry API ---\n\nexport function configure(cfg: JobsConfig): void {\n _config = cfg;\n _connection = parseRedis(cfg.redis);\n _queue = new Queue(cfg.queue ?? \"default\", { connection: _connection });\n _flowProducer = new FlowProducer({ connection: _connection });\n}\n\nexport function resetListeners(): void {\n _failedListeners.length = 0;\n _completedListeners.length = 0;\n}\n\nexport function registerHandler(\n name: string,\n fn: (...args: unknown[]) => Promise<unknown>\n): void {\n _handlers.set(name, fn);\n}\n\nexport async function enqueue(\n name: string,\n args: unknown[],\n opts?: { delay?: number; attempts?: number; backoff?: BackoffStrategy }\n): Promise<void> {\n await requireQueue().add(name, { args }, {\n ...defaultJobOpts(opts?.attempts, opts?.backoff),\n delay: opts?.delay,\n });\n}\n\nexport async function enqueueScheduled(\n name: string,\n cronPattern: string,\n args: unknown[],\n opts?: { attempts?: number; backoff?: BackoffStrategy }\n): Promise<void> {\n await requireQueue().upsertJobScheduler(\n `${name}::cron`,\n { pattern: cronPattern },\n {\n name,\n data: { args },\n opts: defaultJobOpts(opts?.attempts, opts?.backoff),\n }\n );\n}\n\nexport async function dispatchFlow(node: FlowNode): Promise<void> {\n await requireFlowProducer().add(toFlowJob(node));\n}\n\nexport async function startWorker(): Promise<void> {\n if (!_config || !_connection) {\n throw new Error(`[tasklane] initJobs() has not been called before startWorker().`);\n }\n if (_worker) return;\n\n _worker = new Worker(\n _config.queue ?? \"default\",\n async (bullJob) => {\n const handler = _handlers.get(bullJob.name);\n if (!handler) {\n throw new Error(\n `[tasklane] No handler registered for job \"${bullJob.name}\". ` +\n `Make sure the file defining this job is imported before startWorker() is called.`\n );\n }\n const args = (bullJob.data as { args: unknown[] }).args ?? [];\n return handler(...args);\n },\n { connection: _connection }\n );\n\n _worker.on(\"error\", (err) => {\n _failedListeners.forEach((fn) =>\n fn({\n jobId: \"worker\",\n name: \"worker-error\",\n args: [],\n error: err,\n attemptsMade: 0,\n isFinalFailure: false,\n })\n );\n });\n\n _worker.on(\"failed\", (bullJob, err) => {\n if (!bullJob) return;\n const isFinalFailure = bullJob.attemptsMade >= (bullJob.opts.attempts ?? 1);\n const event: FailedEvent = {\n jobId: bullJob.id!,\n name: bullJob.name,\n args: (bullJob.data as { args: unknown[] }).args ?? [],\n error: err,\n attemptsMade: bullJob.attemptsMade,\n isFinalFailure,\n };\n _failedListeners.forEach((fn) => fn(event));\n });\n\n _worker.on(\"completed\", (bullJob, result) => {\n const event: CompletedEvent = {\n jobId: bullJob.id!,\n name: bullJob.name,\n args: (bullJob.data as { args: unknown[] }).args ?? [],\n result,\n };\n _completedListeners.forEach((fn) => fn(event));\n });\n\n await _worker.waitUntilReady();\n}\n\nexport function onFailed(listener: (e: FailedEvent) => void): () => void {\n _failedListeners.push(listener);\n return () => {\n const i = _failedListeners.indexOf(listener);\n if (i !== -1) _failedListeners.splice(i, 1);\n };\n}\n\nexport function onCompleted(listener: (e: CompletedEvent) => void): () => void {\n _completedListeners.push(listener);\n return () => {\n const i = _completedListeners.indexOf(listener);\n if (i !== -1) _completedListeners.splice(i, 1);\n };\n}\n\nexport async function getFailed(start = 0, limit = 50): Promise<FailedJob[]> {\n const queue = requireQueue();\n const jobs = await queue.getFailed(start, start + limit - 1);\n return jobs.map((j) => ({\n jobId: j.id!,\n name: j.name,\n args: (j.data as { args: unknown[] }).args ?? [],\n failedReason: j.failedReason ?? \"Unknown error\",\n attemptsMade: j.attemptsMade,\n timestamp: j.timestamp,\n finishedOn: j.finishedOn,\n }));\n}\n\nexport async function retryJob(jobId: string): Promise<void> {\n const queue = requireQueue();\n const job = await queue.getJob(jobId);\n if (!job) {\n throw new Error(`[tasklane] retryJob: no job found with id \"${jobId}\"`);\n }\n await job.retry();\n}\n\nexport async function stopAll(): Promise<void> {\n await Promise.all([\n _worker?.close(),\n _queue?.close(),\n _flowProducer?.close(),\n ]);\n _worker = null;\n _queue = null;\n _flowProducer = null;\n _connection = null;\n _config = null;\n}\n","import { registerHandler, enqueue, enqueueScheduled } from \"./registry.js\";\nimport type { JobFnOptions, BackoffStrategy } from \"./types.js\";\n\n/**\n * Wraps an async function as a background job.\n *\n * The returned function has the same signature as the original — calling it\n * enqueues the job instead of running it inline.\n *\n * The function MUST be named (not an arrow function) so the package can\n * use its name as the job identifier.\n *\n * @example\n * export const sendSms = job(async function sendSms(to: string, message: string) {\n * await smsProvider.send({ to, message });\n * }, { attempts: 5, backoff: { type: \"fixed\", delay: 2000 } });\n *\n * // Enqueues immediately\n * await sendSms(\"Mushud\", \"Your OTP is 1234\");\n *\n * // Enqueue after a delay (ms)\n * await sendSms.delay(60_000)(\"Mushud\", \"Reminder\");\n *\n * // Enqueue once at a specific date\n * await sendSms.at(new Date(\"2026-04-13T09:00:00Z\"))(\"Mushud\", \"Good morning\");\n *\n * // Recurring cron schedule\n * await sendSms.cron(\"0 9 * * *\")(\"Mushud\", \"Daily reminder\");\n *\n * // Run directly without the queue\n * await sendSms.run(\"Mushud\", \"Inline\");\n */\nexport function job<T extends (...args: any[]) => Promise<any>>(\n fn: T,\n opts?: JobFnOptions\n): JobFn<T> {\n const name = fn.name;\n\n if (!name) {\n throw new Error(\n `[tasklane] job() requires a named function. ` +\n `Use: job(async function myJobName(...) { ... }) — not an arrow function.`\n );\n }\n\n registerHandler(name, fn);\n\n // Extracted once — shared by all dispatch paths\n const attempts = opts?.attempts;\n const backoff: BackoffStrategy | undefined = opts?.backoff;\n\n const proxy = async (...args: Parameters<T>): Promise<void> => {\n await enqueue(name, args, { attempts, backoff });\n };\n\n return Object.assign(proxy, {\n /** The registered job name — used by flow() to build the job tree */\n jobName: name,\n\n /** Run the handler directly, bypassing the queue entirely */\n run: (...args: Parameters<T>): ReturnType<T> => fn(...args) as ReturnType<T>,\n\n delay: (ms: number) =>\n async (...args: Parameters<T>): Promise<void> => {\n await enqueue(name, args, { delay: ms, attempts, backoff });\n },\n\n at: (date: Date) =>\n async (...args: Parameters<T>): Promise<void> => {\n const delay = date.getTime() - Date.now();\n if (delay < 0) throw new Error(`[tasklane] .at() date is in the past`);\n await enqueue(name, args, { delay, attempts, backoff });\n },\n\n cron: (pattern: string) =>\n async (...args: Parameters<T>): Promise<void> => {\n await enqueueScheduled(name, pattern, args, { attempts, backoff });\n },\n }) as unknown as JobFn<T>;\n}\n\n/** The type returned by job() — same call signature plus scheduling helpers */\nexport type JobFn<T extends (...args: any[]) => Promise<any>> = {\n (...args: Parameters<T>): Promise<void>;\n /** The registered job name */\n readonly jobName: string;\n /** Run the handler directly, bypassing the queue entirely */\n run(...args: Parameters<T>): ReturnType<T>;\n /** Enqueue after a delay in milliseconds */\n delay(ms: number): (...args: Parameters<T>) => Promise<void>;\n /** Enqueue once at a specific Date */\n at(date: Date): (...args: Parameters<T>) => Promise<void>;\n /** Register a recurring cron schedule */\n cron(pattern: string): (...args: Parameters<T>) => Promise<void>;\n};\n","import {\n configure,\n startWorker as _startWorker,\n stopAll,\n onFailed,\n onCompleted,\n getFailed as _getFailed,\n retryJob as _retryJob,\n dispatchFlow,\n resetListeners,\n} from \"./registry.js\";\nimport type { JobsConfig, FailedEvent, CompletedEvent, FlowNode } from \"./types.js\";\n\nexport { job } from \"./job.js\";\nexport type { JobFn } from \"./job.js\";\nexport type {\n JobsConfig,\n JobFnOptions,\n FailedEvent,\n CompletedEvent,\n FailedJob,\n BackoffStrategy,\n FlowNode,\n} from \"./types.js\";\n\n/**\n * Initialize the jobs runtime. Call this once at app startup before\n * importing or calling any job functions.\n *\n * @example\n * initJobs({\n * redis: process.env.REDIS_URL,\n * attempts: 3,\n * backoff: { type: \"exponential\", delay: 1000 },\n * });\n */\nexport function initJobs(config: JobsConfig): void {\n configure(config);\n}\n\n/**\n * Start the worker process. Call after initJobs() and after importing\n * all files that define jobs.\n */\nexport async function startWorker(): Promise<void> {\n return _startWorker();\n}\n\n/**\n * Gracefully stop the worker, close all queue connections,\n * and clear all event listeners.\n */\nexport async function stopWorker(): Promise<void> {\n resetListeners();\n return stopAll();\n}\n\n/**\n * Listen for failed jobs. Fires on every failed attempt.\n * Returns an unsubscribe function.\n *\n * @example\n * const unsub = onJobFailed((event) => {\n * if (event.isFinalFailure) alerting.send(event.name);\n * });\n * // later: unsub();\n */\nexport function onJobFailed(listener: (event: FailedEvent) => void): () => void {\n return onFailed(listener);\n}\n\n/**\n * Listen for completed jobs. Returns an unsubscribe function.\n */\nexport function onJobCompleted(listener: (event: CompletedEvent) => void): () => void {\n return onCompleted(listener);\n}\n\n/**\n * Get a list of permanently failed jobs (all retries exhausted).\n *\n * @param start - Pagination offset. Default: 0\n * @param limit - Max results. Default: 50\n */\nexport async function getFailed(start = 0, limit = 50) {\n return _getFailed(start, limit);\n}\n\n/**\n * Re-queue a permanently failed job by its ID.\n * The attempt count resets to zero.\n */\nexport async function retryJob(jobId: string): Promise<void> {\n return _retryJob(jobId);\n}\n\n/**\n * Dispatch a job flow — a parent job that runs only after all its\n * children complete. Supports unlimited nesting depth.\n *\n * Children run in parallel. The parent runs last.\n * If any child fails permanently, the parent is not executed.\n *\n * @example\n * await flow({\n * job: processOrder,\n * args: [\"ord_123\"],\n * children: [\n * { job: chargePayment, args: [\"ord_123\"] },\n * { job: reserveInventory, args: [\"ord_123\"] },\n * {\n * job: prepareShipment,\n * args: [\"ord_123\"],\n * children: [\n * { job: validateAddress, args: [\"ord_123\"] },\n * ],\n * },\n * ],\n * });\n */\nexport async function flow(node: FlowNode): Promise<void> {\n return dispatchFlow(node);\n}\n"]}
@@ -0,0 +1,190 @@
1
+ interface BackoffStrategy {
2
+ /** "fixed" waits the same delay every time. "exponential" doubles each attempt. */
3
+ type: "fixed" | "exponential";
4
+ /** Base delay in milliseconds */
5
+ delay: number;
6
+ }
7
+ interface JobsConfig {
8
+ /** Redis connection URL, e.g. "redis://127.0.0.1:6379" */
9
+ redis: string;
10
+ /** Queue name. Default: "default" */
11
+ queue?: string;
12
+ /** Default retry attempts for all jobs. Default: 3 */
13
+ attempts?: number;
14
+ /** Default backoff strategy. Default: exponential, 1000ms */
15
+ backoff?: BackoffStrategy;
16
+ }
17
+ interface JobFnOptions {
18
+ /** Override retry attempts for this specific job */
19
+ attempts?: number;
20
+ /** Override backoff strategy for this specific job */
21
+ backoff?: BackoffStrategy;
22
+ }
23
+ interface FailedEvent {
24
+ jobId: string;
25
+ name: string;
26
+ args: unknown[];
27
+ error: Error;
28
+ attemptsMade: number;
29
+ /** true when all retry attempts are exhausted */
30
+ isFinalFailure: boolean;
31
+ }
32
+ interface CompletedEvent {
33
+ jobId: string;
34
+ name: string;
35
+ args: unknown[];
36
+ result: unknown;
37
+ }
38
+ interface FailedJob {
39
+ /** BullMQ job ID — pass to retryJob() to re-queue */
40
+ jobId: string;
41
+ /** Job function name */
42
+ name: string;
43
+ /** Arguments the job was called with */
44
+ args: unknown[];
45
+ /** The last error message */
46
+ failedReason: string;
47
+ /** Total attempts made before giving up */
48
+ attemptsMade: number;
49
+ /** Unix timestamp (ms) when the job was first created */
50
+ timestamp: number;
51
+ /** Unix timestamp (ms) when the job finally failed */
52
+ finishedOn?: number;
53
+ }
54
+ /** A node in a job flow tree */
55
+ interface FlowNode<T extends (...args: any[]) => Promise<any> = (...args: any[]) => Promise<any>> {
56
+ /** The job to run at this level */
57
+ job: {
58
+ jobName: string;
59
+ };
60
+ /** Arguments to call the job with */
61
+ args?: T extends (...args: infer A) => Promise<any> ? A : unknown[];
62
+ /** Child jobs that must complete before this job runs */
63
+ children?: FlowNode<any>[];
64
+ /** Override attempts for this node only */
65
+ attempts?: number;
66
+ /** Override backoff for this node only */
67
+ backoff?: BackoffStrategy;
68
+ }
69
+
70
+ /**
71
+ * Wraps an async function as a background job.
72
+ *
73
+ * The returned function has the same signature as the original — calling it
74
+ * enqueues the job instead of running it inline.
75
+ *
76
+ * The function MUST be named (not an arrow function) so the package can
77
+ * use its name as the job identifier.
78
+ *
79
+ * @example
80
+ * export const sendSms = job(async function sendSms(to: string, message: string) {
81
+ * await smsProvider.send({ to, message });
82
+ * }, { attempts: 5, backoff: { type: "fixed", delay: 2000 } });
83
+ *
84
+ * // Enqueues immediately
85
+ * await sendSms("Mushud", "Your OTP is 1234");
86
+ *
87
+ * // Enqueue after a delay (ms)
88
+ * await sendSms.delay(60_000)("Mushud", "Reminder");
89
+ *
90
+ * // Enqueue once at a specific date
91
+ * await sendSms.at(new Date("2026-04-13T09:00:00Z"))("Mushud", "Good morning");
92
+ *
93
+ * // Recurring cron schedule
94
+ * await sendSms.cron("0 9 * * *")("Mushud", "Daily reminder");
95
+ *
96
+ * // Run directly without the queue
97
+ * await sendSms.run("Mushud", "Inline");
98
+ */
99
+ declare function job<T extends (...args: any[]) => Promise<any>>(fn: T, opts?: JobFnOptions): JobFn<T>;
100
+ /** The type returned by job() — same call signature plus scheduling helpers */
101
+ type JobFn<T extends (...args: any[]) => Promise<any>> = {
102
+ (...args: Parameters<T>): Promise<void>;
103
+ /** The registered job name */
104
+ readonly jobName: string;
105
+ /** Run the handler directly, bypassing the queue entirely */
106
+ run(...args: Parameters<T>): ReturnType<T>;
107
+ /** Enqueue after a delay in milliseconds */
108
+ delay(ms: number): (...args: Parameters<T>) => Promise<void>;
109
+ /** Enqueue once at a specific Date */
110
+ at(date: Date): (...args: Parameters<T>) => Promise<void>;
111
+ /** Register a recurring cron schedule */
112
+ cron(pattern: string): (...args: Parameters<T>) => Promise<void>;
113
+ };
114
+
115
+ /**
116
+ * Initialize the jobs runtime. Call this once at app startup before
117
+ * importing or calling any job functions.
118
+ *
119
+ * @example
120
+ * initJobs({
121
+ * redis: process.env.REDIS_URL,
122
+ * attempts: 3,
123
+ * backoff: { type: "exponential", delay: 1000 },
124
+ * });
125
+ */
126
+ declare function initJobs(config: JobsConfig): void;
127
+ /**
128
+ * Start the worker process. Call after initJobs() and after importing
129
+ * all files that define jobs.
130
+ */
131
+ declare function startWorker(): Promise<void>;
132
+ /**
133
+ * Gracefully stop the worker, close all queue connections,
134
+ * and clear all event listeners.
135
+ */
136
+ declare function stopWorker(): Promise<void>;
137
+ /**
138
+ * Listen for failed jobs. Fires on every failed attempt.
139
+ * Returns an unsubscribe function.
140
+ *
141
+ * @example
142
+ * const unsub = onJobFailed((event) => {
143
+ * if (event.isFinalFailure) alerting.send(event.name);
144
+ * });
145
+ * // later: unsub();
146
+ */
147
+ declare function onJobFailed(listener: (event: FailedEvent) => void): () => void;
148
+ /**
149
+ * Listen for completed jobs. Returns an unsubscribe function.
150
+ */
151
+ declare function onJobCompleted(listener: (event: CompletedEvent) => void): () => void;
152
+ /**
153
+ * Get a list of permanently failed jobs (all retries exhausted).
154
+ *
155
+ * @param start - Pagination offset. Default: 0
156
+ * @param limit - Max results. Default: 50
157
+ */
158
+ declare function getFailed(start?: number, limit?: number): Promise<FailedJob[]>;
159
+ /**
160
+ * Re-queue a permanently failed job by its ID.
161
+ * The attempt count resets to zero.
162
+ */
163
+ declare function retryJob(jobId: string): Promise<void>;
164
+ /**
165
+ * Dispatch a job flow — a parent job that runs only after all its
166
+ * children complete. Supports unlimited nesting depth.
167
+ *
168
+ * Children run in parallel. The parent runs last.
169
+ * If any child fails permanently, the parent is not executed.
170
+ *
171
+ * @example
172
+ * await flow({
173
+ * job: processOrder,
174
+ * args: ["ord_123"],
175
+ * children: [
176
+ * { job: chargePayment, args: ["ord_123"] },
177
+ * { job: reserveInventory, args: ["ord_123"] },
178
+ * {
179
+ * job: prepareShipment,
180
+ * args: ["ord_123"],
181
+ * children: [
182
+ * { job: validateAddress, args: ["ord_123"] },
183
+ * ],
184
+ * },
185
+ * ],
186
+ * });
187
+ */
188
+ declare function flow(node: FlowNode): Promise<void>;
189
+
190
+ export { type BackoffStrategy, type CompletedEvent, type FailedEvent, type FailedJob, type FlowNode, type JobFn, type JobFnOptions, type JobsConfig, flow, getFailed, initJobs, job, onJobCompleted, onJobFailed, retryJob, startWorker, stopWorker };
@@ -0,0 +1,190 @@
1
+ interface BackoffStrategy {
2
+ /** "fixed" waits the same delay every time. "exponential" doubles each attempt. */
3
+ type: "fixed" | "exponential";
4
+ /** Base delay in milliseconds */
5
+ delay: number;
6
+ }
7
+ interface JobsConfig {
8
+ /** Redis connection URL, e.g. "redis://127.0.0.1:6379" */
9
+ redis: string;
10
+ /** Queue name. Default: "default" */
11
+ queue?: string;
12
+ /** Default retry attempts for all jobs. Default: 3 */
13
+ attempts?: number;
14
+ /** Default backoff strategy. Default: exponential, 1000ms */
15
+ backoff?: BackoffStrategy;
16
+ }
17
+ interface JobFnOptions {
18
+ /** Override retry attempts for this specific job */
19
+ attempts?: number;
20
+ /** Override backoff strategy for this specific job */
21
+ backoff?: BackoffStrategy;
22
+ }
23
+ interface FailedEvent {
24
+ jobId: string;
25
+ name: string;
26
+ args: unknown[];
27
+ error: Error;
28
+ attemptsMade: number;
29
+ /** true when all retry attempts are exhausted */
30
+ isFinalFailure: boolean;
31
+ }
32
+ interface CompletedEvent {
33
+ jobId: string;
34
+ name: string;
35
+ args: unknown[];
36
+ result: unknown;
37
+ }
38
+ interface FailedJob {
39
+ /** BullMQ job ID — pass to retryJob() to re-queue */
40
+ jobId: string;
41
+ /** Job function name */
42
+ name: string;
43
+ /** Arguments the job was called with */
44
+ args: unknown[];
45
+ /** The last error message */
46
+ failedReason: string;
47
+ /** Total attempts made before giving up */
48
+ attemptsMade: number;
49
+ /** Unix timestamp (ms) when the job was first created */
50
+ timestamp: number;
51
+ /** Unix timestamp (ms) when the job finally failed */
52
+ finishedOn?: number;
53
+ }
54
+ /** A node in a job flow tree */
55
+ interface FlowNode<T extends (...args: any[]) => Promise<any> = (...args: any[]) => Promise<any>> {
56
+ /** The job to run at this level */
57
+ job: {
58
+ jobName: string;
59
+ };
60
+ /** Arguments to call the job with */
61
+ args?: T extends (...args: infer A) => Promise<any> ? A : unknown[];
62
+ /** Child jobs that must complete before this job runs */
63
+ children?: FlowNode<any>[];
64
+ /** Override attempts for this node only */
65
+ attempts?: number;
66
+ /** Override backoff for this node only */
67
+ backoff?: BackoffStrategy;
68
+ }
69
+
70
+ /**
71
+ * Wraps an async function as a background job.
72
+ *
73
+ * The returned function has the same signature as the original — calling it
74
+ * enqueues the job instead of running it inline.
75
+ *
76
+ * The function MUST be named (not an arrow function) so the package can
77
+ * use its name as the job identifier.
78
+ *
79
+ * @example
80
+ * export const sendSms = job(async function sendSms(to: string, message: string) {
81
+ * await smsProvider.send({ to, message });
82
+ * }, { attempts: 5, backoff: { type: "fixed", delay: 2000 } });
83
+ *
84
+ * // Enqueues immediately
85
+ * await sendSms("Mushud", "Your OTP is 1234");
86
+ *
87
+ * // Enqueue after a delay (ms)
88
+ * await sendSms.delay(60_000)("Mushud", "Reminder");
89
+ *
90
+ * // Enqueue once at a specific date
91
+ * await sendSms.at(new Date("2026-04-13T09:00:00Z"))("Mushud", "Good morning");
92
+ *
93
+ * // Recurring cron schedule
94
+ * await sendSms.cron("0 9 * * *")("Mushud", "Daily reminder");
95
+ *
96
+ * // Run directly without the queue
97
+ * await sendSms.run("Mushud", "Inline");
98
+ */
99
+ declare function job<T extends (...args: any[]) => Promise<any>>(fn: T, opts?: JobFnOptions): JobFn<T>;
100
+ /** The type returned by job() — same call signature plus scheduling helpers */
101
+ type JobFn<T extends (...args: any[]) => Promise<any>> = {
102
+ (...args: Parameters<T>): Promise<void>;
103
+ /** The registered job name */
104
+ readonly jobName: string;
105
+ /** Run the handler directly, bypassing the queue entirely */
106
+ run(...args: Parameters<T>): ReturnType<T>;
107
+ /** Enqueue after a delay in milliseconds */
108
+ delay(ms: number): (...args: Parameters<T>) => Promise<void>;
109
+ /** Enqueue once at a specific Date */
110
+ at(date: Date): (...args: Parameters<T>) => Promise<void>;
111
+ /** Register a recurring cron schedule */
112
+ cron(pattern: string): (...args: Parameters<T>) => Promise<void>;
113
+ };
114
+
115
+ /**
116
+ * Initialize the jobs runtime. Call this once at app startup before
117
+ * importing or calling any job functions.
118
+ *
119
+ * @example
120
+ * initJobs({
121
+ * redis: process.env.REDIS_URL,
122
+ * attempts: 3,
123
+ * backoff: { type: "exponential", delay: 1000 },
124
+ * });
125
+ */
126
+ declare function initJobs(config: JobsConfig): void;
127
+ /**
128
+ * Start the worker process. Call after initJobs() and after importing
129
+ * all files that define jobs.
130
+ */
131
+ declare function startWorker(): Promise<void>;
132
+ /**
133
+ * Gracefully stop the worker, close all queue connections,
134
+ * and clear all event listeners.
135
+ */
136
+ declare function stopWorker(): Promise<void>;
137
+ /**
138
+ * Listen for failed jobs. Fires on every failed attempt.
139
+ * Returns an unsubscribe function.
140
+ *
141
+ * @example
142
+ * const unsub = onJobFailed((event) => {
143
+ * if (event.isFinalFailure) alerting.send(event.name);
144
+ * });
145
+ * // later: unsub();
146
+ */
147
+ declare function onJobFailed(listener: (event: FailedEvent) => void): () => void;
148
+ /**
149
+ * Listen for completed jobs. Returns an unsubscribe function.
150
+ */
151
+ declare function onJobCompleted(listener: (event: CompletedEvent) => void): () => void;
152
+ /**
153
+ * Get a list of permanently failed jobs (all retries exhausted).
154
+ *
155
+ * @param start - Pagination offset. Default: 0
156
+ * @param limit - Max results. Default: 50
157
+ */
158
+ declare function getFailed(start?: number, limit?: number): Promise<FailedJob[]>;
159
+ /**
160
+ * Re-queue a permanently failed job by its ID.
161
+ * The attempt count resets to zero.
162
+ */
163
+ declare function retryJob(jobId: string): Promise<void>;
164
+ /**
165
+ * Dispatch a job flow — a parent job that runs only after all its
166
+ * children complete. Supports unlimited nesting depth.
167
+ *
168
+ * Children run in parallel. The parent runs last.
169
+ * If any child fails permanently, the parent is not executed.
170
+ *
171
+ * @example
172
+ * await flow({
173
+ * job: processOrder,
174
+ * args: ["ord_123"],
175
+ * children: [
176
+ * { job: chargePayment, args: ["ord_123"] },
177
+ * { job: reserveInventory, args: ["ord_123"] },
178
+ * {
179
+ * job: prepareShipment,
180
+ * args: ["ord_123"],
181
+ * children: [
182
+ * { job: validateAddress, args: ["ord_123"] },
183
+ * ],
184
+ * },
185
+ * ],
186
+ * });
187
+ */
188
+ declare function flow(node: FlowNode): Promise<void>;
189
+
190
+ export { type BackoffStrategy, type CompletedEvent, type FailedEvent, type FailedJob, type FlowNode, type JobFn, type JobFnOptions, type JobsConfig, flow, getFailed, initJobs, job, onJobCompleted, onJobFailed, retryJob, startWorker, stopWorker };