@igniter-js/jobs 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +117 -243
  2. package/README.md +311 -153
  3. package/dist/{adapter-CXZxomI9.d.mts → adapter-Cax_40HW.d.mts} +27 -6
  4. package/dist/{adapter-CXZxomI9.d.ts → adapter-Cax_40HW.d.ts} +27 -6
  5. package/dist/adapters/bun.d.mts +93 -0
  6. package/dist/adapters/bun.d.ts +93 -0
  7. package/dist/adapters/bun.js +914 -0
  8. package/dist/adapters/bun.js.map +1 -0
  9. package/dist/adapters/bun.mjs +912 -0
  10. package/dist/adapters/bun.mjs.map +1 -0
  11. package/dist/adapters/{memory.adapter.d.ts → mock.d.mts} +1 -3
  12. package/dist/adapters/{memory.adapter.d.mts → mock.d.ts} +1 -3
  13. package/dist/adapters/{memory.adapter.js → mock.js} +58 -25
  14. package/dist/adapters/mock.js.map +1 -0
  15. package/dist/adapters/{memory.adapter.mjs → mock.mjs} +58 -25
  16. package/dist/adapters/mock.mjs.map +1 -0
  17. package/dist/adapters/{bullmq.adapter.d.ts → node.d.mts} +1 -3
  18. package/dist/adapters/{bullmq.adapter.d.mts → node.d.ts} +1 -3
  19. package/dist/adapters/{bullmq.adapter.js → node.js} +117 -40
  20. package/dist/adapters/node.js.map +1 -0
  21. package/dist/adapters/{bullmq.adapter.mjs → node.mjs} +117 -40
  22. package/dist/adapters/node.mjs.map +1 -0
  23. package/dist/index.d.mts +17 -21
  24. package/dist/index.d.ts +17 -21
  25. package/dist/index.js +20 -1854
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +21 -1852
  28. package/dist/index.mjs.map +1 -1
  29. package/package.json +28 -40
  30. package/CHANGELOG.md +0 -13
  31. package/dist/adapters/bullmq.adapter.js.map +0 -1
  32. package/dist/adapters/bullmq.adapter.mjs.map +0 -1
  33. package/dist/adapters/index.d.mts +0 -143
  34. package/dist/adapters/index.d.ts +0 -143
  35. package/dist/adapters/index.js +0 -1891
  36. package/dist/adapters/index.js.map +0 -1
  37. package/dist/adapters/index.mjs +0 -1887
  38. package/dist/adapters/index.mjs.map +0 -1
  39. package/dist/adapters/memory.adapter.js.map +0 -1
  40. package/dist/adapters/memory.adapter.mjs.map +0 -1
@@ -0,0 +1,914 @@
1
+ 'use strict';
2
+
3
+ var common = require('@igniter-js/common');
4
+
5
+ // src/utils/id-generator.ts
6
+ var IgniterJobsIdGenerator = class {
7
+ /**
8
+ * Generates a unique identifier with a prefix.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const jobId = IgniterJobsIdGenerator.generate('job')
13
+ * ```
14
+ */
15
+ static generate(prefix) {
16
+ const now = Date.now().toString(36);
17
+ const random = Math.random().toString(36).slice(2, 8);
18
+ return `${prefix}_${now}_${random}`;
19
+ }
20
+ };
21
+ var IgniterJobsError = class extends common.IgniterError {
22
+ constructor(options) {
23
+ super(options);
24
+ }
25
+ };
26
+
27
+ // src/adapters/bun-sqlite.adapter.ts
28
+ var IgniterJobsBunSQLiteAdapter = class _IgniterJobsBunSQLiteAdapter {
29
+ constructor(options) {
30
+ this.queues = {
31
+ list: async () => this.listQueues(),
32
+ get: async (name) => this.getQueueInfo(name),
33
+ getJobCounts: async (name) => this.getQueueJobCounts(name),
34
+ getJobs: async (name, filter) => this.searchJobs({
35
+ queue: name,
36
+ status: filter?.status,
37
+ limit: filter?.limit,
38
+ offset: filter?.offset
39
+ }),
40
+ pause: async (name) => this.pauseQueue(name),
41
+ resume: async (name) => this.resumeQueue(name),
42
+ isPaused: async (name) => (await this.getQueueInfo(name))?.isPaused ?? false,
43
+ drain: async (name) => this.drainQueue(name),
44
+ clean: async (name, options) => this.cleanQueue(name, options),
45
+ obliterate: async (name, options) => this.obliterateQueue(name, options)
46
+ };
47
+ this.bunqueueModulePromise = null;
48
+ this.queueRuntimes = /* @__PURE__ */ new Map();
49
+ this.workers = /* @__PURE__ */ new Map();
50
+ this.subscribers = /* @__PURE__ */ new Map();
51
+ this.registeredJobs = /* @__PURE__ */ new Map();
52
+ this.registeredCrons = /* @__PURE__ */ new Map();
53
+ this.options = {
54
+ path: options.path,
55
+ durable: options.durable ?? false,
56
+ heartbeatInterval: options.heartbeatInterval ?? 1e4,
57
+ pollTimeout: options.pollTimeout ?? 0,
58
+ batchSize: options.batchSize ?? 10,
59
+ lockDuration: options.lockDuration ?? 3e4,
60
+ maxStalledCount: options.maxStalledCount ?? 1
61
+ };
62
+ this.client = {
63
+ type: "bun-sqlite",
64
+ path: this.options.path
65
+ };
66
+ }
67
+ static create(options) {
68
+ return new _IgniterJobsBunSQLiteAdapter(options);
69
+ }
70
+ registerJob(queueName, jobName, definition) {
71
+ const jobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
72
+ if (jobs.has(jobName)) {
73
+ throw new IgniterJobsError({
74
+ code: "JOBS_DUPLICATE_JOB",
75
+ message: `Job "${jobName}" already registered for queue "${queueName}".`
76
+ });
77
+ }
78
+ jobs.set(jobName, definition);
79
+ this.registeredJobs.set(queueName, jobs);
80
+ this.markQueueCronsDirty(queueName);
81
+ }
82
+ registerCron(queueName, cronName, definition) {
83
+ const crons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
84
+ if (crons.has(cronName)) {
85
+ throw new IgniterJobsError({
86
+ code: "JOBS_INVALID_CRON",
87
+ message: `Cron "${cronName}" already registered for queue "${queueName}".`
88
+ });
89
+ }
90
+ crons.set(cronName, definition);
91
+ this.registeredCrons.set(queueName, crons);
92
+ this.markQueueCronsDirty(queueName);
93
+ }
94
+ async dispatch(params) {
95
+ const queue = await this.getQueue(params.queue);
96
+ const job = await queue.add(
97
+ params.jobName,
98
+ this.createEnvelope(params.input, params.metadata, params.scope),
99
+ this.toJobOptions(params)
100
+ );
101
+ return job.id;
102
+ }
103
+ async schedule(params) {
104
+ const queue = await this.getQueue(params.queue);
105
+ if (params.at) {
106
+ const delay = params.at.getTime() - Date.now();
107
+ if (delay <= 0) {
108
+ throw new IgniterJobsError({
109
+ code: "JOBS_INVALID_SCHEDULE",
110
+ message: "Scheduled time must be in the future."
111
+ });
112
+ }
113
+ const job2 = await queue.add(
114
+ params.jobName,
115
+ this.createEnvelope(params.input, params.metadata, params.scope),
116
+ this.toJobOptions({ ...params, delay })
117
+ );
118
+ return job2.id;
119
+ }
120
+ const scheduleEnvelope = this.createEnvelope(
121
+ params.input,
122
+ params.metadata,
123
+ params.scope,
124
+ params
125
+ );
126
+ if (params.cron || params.every) {
127
+ const job2 = await queue.add(
128
+ params.jobName,
129
+ scheduleEnvelope,
130
+ this.toJobOptions(params, {
131
+ repeat: {
132
+ pattern: params.cron,
133
+ every: params.every,
134
+ limit: params.maxExecutions,
135
+ tz: params.tz
136
+ }
137
+ })
138
+ );
139
+ return job2.id;
140
+ }
141
+ const job = await queue.add(
142
+ params.jobName,
143
+ scheduleEnvelope,
144
+ this.toJobOptions(params)
145
+ );
146
+ return job.id;
147
+ }
148
+ async getJob(jobId, queue) {
149
+ if (queue) {
150
+ const job = await this.getQueue(queue).then((q) => q.getJob(jobId));
151
+ return job ? this.mapJob(job, queue) : null;
152
+ }
153
+ for (const queueName of await this.getAllQueueNames()) {
154
+ const job = await this.getQueue(queueName).then((q) => q.getJob(jobId));
155
+ if (job) return this.mapJob(job, queueName);
156
+ }
157
+ return null;
158
+ }
159
+ async getJobState(jobId, queue) {
160
+ const job = await this.getJob(jobId, queue);
161
+ return job?.status ?? null;
162
+ }
163
+ async getJobLogs(jobId, queue) {
164
+ const queueName = queue ?? await this.findQueueByJobId(jobId);
165
+ if (!queueName) return [];
166
+ const logs = await this.getQueue(queueName).then(
167
+ (q) => q.getJobLogs(jobId)
168
+ );
169
+ return logs.logs.map((entry) => this.parseLogEntry(entry));
170
+ }
171
+ async getJobProgress(jobId, queue) {
172
+ const job = await this.getJob(jobId, queue);
173
+ return job?.progress ?? 0;
174
+ }
175
+ async retryJob(jobId, queue) {
176
+ const job = await this.requireJob(jobId, queue);
177
+ await job.retry();
178
+ }
179
+ async removeJob(jobId, queue) {
180
+ const job = await this.requireJob(jobId, queue);
181
+ await job.remove();
182
+ }
183
+ async promoteJob(jobId, queue) {
184
+ const job = await this.requireJob(jobId, queue);
185
+ await job.promote();
186
+ }
187
+ async moveJobToFailed(jobId, reason, queue) {
188
+ const queueName = queue ?? await this.findQueueByJobId(jobId);
189
+ if (!queueName) {
190
+ throw new IgniterJobsError({
191
+ code: "JOBS_NOT_FOUND",
192
+ message: `Job "${jobId}" not found.`
193
+ });
194
+ }
195
+ await this.getQueue(queueName).then(
196
+ (q) => q.moveJobToFailed(jobId, new Error(reason))
197
+ );
198
+ }
199
+ async retryManyJobs(jobIds, queue) {
200
+ await Promise.all(jobIds.map((jobId) => this.retryJob(jobId, queue)));
201
+ }
202
+ async removeManyJobs(jobIds, queue) {
203
+ await Promise.all(jobIds.map((jobId) => this.removeJob(jobId, queue)));
204
+ }
205
+ async getQueueInfo(queue) {
206
+ const counts = await this.getQueueJobCounts(queue);
207
+ return {
208
+ name: queue,
209
+ isPaused: await this.getQueue(queue).then((q) => q.isPausedAsync()),
210
+ jobCounts: counts
211
+ };
212
+ }
213
+ async getQueueJobCounts(queue) {
214
+ const counts = await this.getQueue(queue).then(
215
+ (q) => q.getJobCountsAsync()
216
+ );
217
+ return {
218
+ waiting: counts.waiting + counts.prioritized,
219
+ active: counts.active,
220
+ completed: counts.completed,
221
+ failed: counts.failed,
222
+ delayed: counts.delayed,
223
+ paused: counts.paused
224
+ };
225
+ }
226
+ async listQueues() {
227
+ const queueNames = await this.getAllQueueNames();
228
+ return Promise.all(
229
+ queueNames.map((queueName) => this.getQueueInfo(queueName))
230
+ ).then((queues) => queues.filter(Boolean));
231
+ }
232
+ async pauseQueue(queue) {
233
+ const queueClient = await this.getQueue(queue);
234
+ queueClient.pause();
235
+ for (const worker of this.workers.values()) {
236
+ if (worker.workers.has(queue)) {
237
+ worker.paused = true;
238
+ worker.workers.get(queue)?.pause();
239
+ }
240
+ }
241
+ }
242
+ async resumeQueue(queue) {
243
+ const queueClient = await this.getQueue(queue);
244
+ queueClient.resume();
245
+ for (const worker of this.workers.values()) {
246
+ if (worker.workers.has(queue)) {
247
+ worker.paused = false;
248
+ worker.workers.get(queue)?.resume();
249
+ }
250
+ }
251
+ }
252
+ async drainQueue(queue) {
253
+ const queueClient = await this.getQueue(queue);
254
+ const before = await this.getQueueJobCounts(queue);
255
+ queueClient.drain();
256
+ const after = await this.getQueueJobCounts(queue);
257
+ return Math.max(
258
+ 0,
259
+ before.waiting + before.delayed + before.paused - after.waiting - after.delayed - after.paused
260
+ );
261
+ }
262
+ async cleanQueue(queue, options) {
263
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
264
+ let cleaned = 0;
265
+ const queueClient = await this.getQueue(queue);
266
+ for (const status of statuses) {
267
+ const removed = await queueClient.cleanAsync(
268
+ options.olderThan ?? 0,
269
+ options.limit ?? 1e3,
270
+ this.toBunqueueStatus(status)
271
+ );
272
+ cleaned += removed.length;
273
+ }
274
+ return cleaned;
275
+ }
276
+ async obliterateQueue(queue, _options) {
277
+ const queueClient = await this.getQueue(queue);
278
+ queueClient.obliterate();
279
+ this.registeredJobs.delete(queue);
280
+ this.registeredCrons.delete(queue);
281
+ this.queueRuntimes.delete(queue);
282
+ }
283
+ async retryAllInQueue(queue) {
284
+ const queueClient = await this.getQueue(queue);
285
+ const failedJobs = await queueClient.getFailedAsync(0, 1e4);
286
+ await queueClient.retryJobs({
287
+ state: "failed",
288
+ count: failedJobs.length || 1e4
289
+ });
290
+ return failedJobs.length;
291
+ }
292
+ async searchJobs(filter) {
293
+ const queueName = filter.queue;
294
+ const statuses = filter.status;
295
+ const limit = filter.limit ?? 100;
296
+ const offset = filter.offset ?? 0;
297
+ const queueNames = queueName ? [queueName] : await this.getAllQueueNames();
298
+ const mappedStates = statuses?.flatMap(
299
+ (status) => this.toBunqueueStates(status)
300
+ );
301
+ const results = [];
302
+ for (const currentQueue of queueNames) {
303
+ const queueClient = await this.getQueue(currentQueue);
304
+ const jobs = await queueClient.getJobsAsync({
305
+ state: mappedStates,
306
+ start: 0,
307
+ end: offset + limit - 1
308
+ });
309
+ for (const job of jobs) {
310
+ results.push(
311
+ await this.mapJob(job, currentQueue)
312
+ );
313
+ }
314
+ }
315
+ return results.slice(offset, offset + limit);
316
+ }
317
+ async searchQueues(filter) {
318
+ const queues = await this.listQueues();
319
+ const name = filter.name;
320
+ const isPaused = filter.isPaused;
321
+ return queues.filter((queue) => name ? queue.name.includes(name) : true).filter(
322
+ (queue) => typeof isPaused === "boolean" ? queue.isPaused === isPaused : true
323
+ );
324
+ }
325
+ async searchWorkers(filter) {
326
+ const queue = filter.queue;
327
+ const isRunning = filter.isRunning;
328
+ return Array.from(this.workers.values()).filter((worker) => queue ? worker.queues.includes(queue) : true).filter(
329
+ (worker) => typeof isRunning === "boolean" ? isRunning ? !worker.closed && !worker.paused : worker.closed || worker.paused : true
330
+ ).map((worker) => this.toWorkerHandle(worker));
331
+ }
332
+ async createWorker(config) {
333
+ const bunqueue = await this.loadBunqueue();
334
+ const queueNames = config.queues.length ? config.queues : await this.getAllQueueNames();
335
+ const workerId = IgniterJobsIdGenerator.generate("worker");
336
+ const workerState = {
337
+ id: workerId,
338
+ queues: [...queueNames],
339
+ concurrency: config.concurrency ?? 1,
340
+ paused: false,
341
+ closed: false,
342
+ startedAt: /* @__PURE__ */ new Date(),
343
+ activeJobs: /* @__PURE__ */ new Set(),
344
+ metrics: { processed: 0, failed: 0, totalDuration: 0 },
345
+ workers: /* @__PURE__ */ new Map(),
346
+ handlers: config.handlers
347
+ };
348
+ const perQueueConcurrency = this.distributeConcurrency(
349
+ workerState.concurrency,
350
+ queueNames
351
+ );
352
+ for (const queueName of queueNames) {
353
+ await this.getQueue(queueName);
354
+ await this.syncCronSchedulers(queueName);
355
+ const worker = new bunqueue.Worker(
356
+ queueName,
357
+ async (job) => this.processJob(workerState, queueName, job),
358
+ {
359
+ embedded: true,
360
+ dataPath: this.options.path,
361
+ autorun: false,
362
+ concurrency: perQueueConcurrency.get(queueName) ?? 1,
363
+ heartbeatInterval: this.options.heartbeatInterval,
364
+ batchSize: this.options.batchSize,
365
+ pollTimeout: this.options.pollTimeout,
366
+ lockDuration: this.options.lockDuration,
367
+ maxStalledCount: this.options.maxStalledCount,
368
+ limiter: config.limiter ? { max: config.limiter.max, duration: config.limiter.duration } : void 0
369
+ }
370
+ );
371
+ this.attachWorkerEvents(workerState, queueName, worker);
372
+ worker.run();
373
+ workerState.workers.set(queueName, worker);
374
+ }
375
+ this.workers.set(workerId, workerState);
376
+ return this.toWorkerHandle(workerState);
377
+ }
378
+ getWorkers() {
379
+ return new Map(
380
+ Array.from(this.workers.entries()).map(([id, worker]) => [
381
+ id,
382
+ this.toWorkerHandle(worker)
383
+ ])
384
+ );
385
+ }
386
+ async publishEvent(channel, payload) {
387
+ const handlers = this.subscribers.get(channel);
388
+ if (!handlers) return;
389
+ await Promise.all(
390
+ Array.from(handlers).map(async (handler) => handler(payload))
391
+ );
392
+ }
393
+ async subscribeEvent(channel, handler) {
394
+ const handlers = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
395
+ handlers.add(handler);
396
+ this.subscribers.set(channel, handlers);
397
+ return async () => {
398
+ const current = this.subscribers.get(channel);
399
+ if (!current) return;
400
+ current.delete(handler);
401
+ if (current.size === 0) this.subscribers.delete(channel);
402
+ };
403
+ }
404
+ async shutdown() {
405
+ for (const worker of this.workers.values()) {
406
+ await this.closeWorkerState(worker);
407
+ }
408
+ this.workers.clear();
409
+ for (const runtime of this.queueRuntimes.values()) {
410
+ runtime.queue.close();
411
+ }
412
+ this.queueRuntimes.clear();
413
+ this.subscribers.clear();
414
+ if (this.bunqueueModulePromise) {
415
+ const bunqueue = await this.bunqueueModulePromise;
416
+ bunqueue.shutdownManager();
417
+ }
418
+ }
419
+ async loadBunqueue() {
420
+ if (typeof globalThis.Bun === "undefined") {
421
+ throw new IgniterJobsError({
422
+ code: "JOBS_ADAPTER_ERROR",
423
+ message: "The Bun SQLite adapter can only run inside Bun. Use the BullMQ adapter in Node.js runtimes."
424
+ });
425
+ }
426
+ this.bunqueueModulePromise ?? (this.bunqueueModulePromise = import('bunqueue/client'));
427
+ return this.bunqueueModulePromise;
428
+ }
429
+ async getQueue(queueName) {
430
+ const existing = this.queueRuntimes.get(queueName);
431
+ if (existing) return existing.queue;
432
+ const bunqueue = await this.loadBunqueue();
433
+ const queue = new bunqueue.Queue(queueName, {
434
+ embedded: true,
435
+ dataPath: this.options.path,
436
+ defaultJobOptions: {
437
+ durable: this.options.durable
438
+ }
439
+ });
440
+ await queue.waitUntilReady();
441
+ this.queueRuntimes.set(queueName, { queue, cronsSynced: false });
442
+ return queue;
443
+ }
444
+ async getAllQueueNames() {
445
+ return Array.from(
446
+ /* @__PURE__ */ new Set([
447
+ ...this.registeredJobs.keys(),
448
+ ...this.registeredCrons.keys(),
449
+ ...this.queueRuntimes.keys()
450
+ ])
451
+ );
452
+ }
453
+ markQueueCronsDirty(queueName) {
454
+ const runtime = this.queueRuntimes.get(queueName);
455
+ if (runtime) runtime.cronsSynced = false;
456
+ }
457
+ async syncCronSchedulers(queueName) {
458
+ const runtime = this.queueRuntimes.get(queueName);
459
+ if (!runtime || runtime.cronsSynced) return;
460
+ const cronDefinitions = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
461
+ const existing = await runtime.queue.getJobSchedulers(0, 1e4);
462
+ const existingIds = new Set(existing.map((scheduler) => scheduler.id));
463
+ for (const [cronName, definition] of cronDefinitions.entries()) {
464
+ await runtime.queue.upsertJobScheduler(
465
+ cronName,
466
+ {
467
+ pattern: definition.cron,
468
+ limit: definition.maxExecutions,
469
+ timezone: definition.tz
470
+ },
471
+ {
472
+ name: cronName,
473
+ data: this.createEnvelope(
474
+ void 0,
475
+ void 0,
476
+ void 0,
477
+ definition
478
+ ),
479
+ opts: { durable: this.options.durable }
480
+ }
481
+ );
482
+ existingIds.delete(cronName);
483
+ }
484
+ for (const schedulerId of existingIds) {
485
+ await runtime.queue.removeJobScheduler(schedulerId);
486
+ }
487
+ runtime.cronsSynced = true;
488
+ }
489
+ createEnvelope(input, metadata, scope, scheduleSource) {
490
+ return {
491
+ __igniterJobs: true,
492
+ input,
493
+ metadata,
494
+ scope,
495
+ schedule: scheduleSource ? {
496
+ skipWeekends: scheduleSource.skipWeekends,
497
+ onlyBusinessHours: scheduleSource.onlyBusinessHours,
498
+ businessHours: scheduleSource.businessHours,
499
+ onlyWeekdays: scheduleSource.onlyWeekdays,
500
+ skipDates: scheduleSource.skipDates?.map(
501
+ (value) => value instanceof Date ? value.toISOString() : value
502
+ )
503
+ } : void 0
504
+ };
505
+ }
506
+ toJobOptions(params, overrides) {
507
+ return {
508
+ jobId: params.jobId,
509
+ priority: params.priority,
510
+ delay: params.delay,
511
+ attempts: params.attempts,
512
+ removeOnComplete: params.removeOnComplete,
513
+ removeOnFail: params.removeOnFail,
514
+ durable: this.options.durable,
515
+ ...overrides
516
+ };
517
+ }
518
+ async processJob(workerState, queueName, job) {
519
+ const envelope = this.normalizeEnvelope(job.data);
520
+ if (!this.shouldExecuteScheduledJob(envelope.schedule, /* @__PURE__ */ new Date())) {
521
+ await this.writeLog(
522
+ queueName,
523
+ job.id,
524
+ "info",
525
+ "Scheduled run skipped by adapter rules."
526
+ );
527
+ return { skipped: true };
528
+ }
529
+ const registeredJob = this.registeredJobs.get(queueName)?.get(job.name);
530
+ if (registeredJob) {
531
+ return this.processRegisteredDefinition(
532
+ workerState,
533
+ queueName,
534
+ job,
535
+ envelope,
536
+ registeredJob
537
+ );
538
+ }
539
+ const registeredCron = this.registeredCrons.get(queueName)?.get(job.name);
540
+ if (registeredCron) {
541
+ return this.processRegisteredCron(
542
+ queueName,
543
+ job,
544
+ envelope,
545
+ registeredCron
546
+ );
547
+ }
548
+ throw new IgniterJobsError({
549
+ code: "JOBS_NOT_REGISTERED",
550
+ message: `Job "${job.name}" is not registered for queue "${queueName}".`
551
+ });
552
+ }
553
+ async processRegisteredDefinition(workerState, queueName, job, envelope, definition) {
554
+ const baseContext = this.createExecutionContext(
555
+ queueName,
556
+ job,
557
+ envelope,
558
+ definition
559
+ );
560
+ const startedAt = /* @__PURE__ */ new Date();
561
+ await this.writeLog(queueName, job.id, "info", "Job started.");
562
+ await this.invokeHook(
563
+ () => definition.onStart?.({ ...baseContext, startedAt })
564
+ );
565
+ try {
566
+ const result = await definition.handler(baseContext);
567
+ const duration = Date.now() - startedAt.getTime();
568
+ workerState.metrics.processed += 1;
569
+ workerState.metrics.totalDuration += duration;
570
+ await this.writeLog(
571
+ queueName,
572
+ job.id,
573
+ "info",
574
+ `Job completed in ${duration}ms.`
575
+ );
576
+ await this.invokeHook(
577
+ () => definition.onSuccess?.({
578
+ ...baseContext,
579
+ startedAt,
580
+ duration,
581
+ result
582
+ })
583
+ );
584
+ return result;
585
+ } catch (error) {
586
+ const runtimeError = error instanceof Error ? error : new Error(String(error));
587
+ const duration = Date.now() - startedAt.getTime();
588
+ const maxAttempts = definition.attempts ?? job.opts.attempts ?? 1;
589
+ const isFinalAttempt = job.attemptsMade >= maxAttempts;
590
+ workerState.metrics.failed += 1;
591
+ await this.writeLog(queueName, job.id, "error", runtimeError.message);
592
+ await this.invokeHook(
593
+ () => definition.onFailure?.({
594
+ ...baseContext,
595
+ startedAt,
596
+ duration,
597
+ error: runtimeError,
598
+ isFinalAttempt
599
+ })
600
+ );
601
+ throw runtimeError;
602
+ }
603
+ }
604
+ async processRegisteredCron(queueName, job, envelope, definition) {
605
+ await this.writeLog(queueName, job.id, "info", "Cron execution started.");
606
+ try {
607
+ const result = await definition.handler({
608
+ context: {},
609
+ job: {
610
+ id: job.id,
611
+ name: job.name,
612
+ queue: queueName,
613
+ attemptsMade: job.attemptsMade,
614
+ createdAt: new Date(job.timestamp),
615
+ metadata: envelope.metadata
616
+ },
617
+ scope: envelope.scope
618
+ });
619
+ await this.writeLog(
620
+ queueName,
621
+ job.id,
622
+ "info",
623
+ "Cron execution completed."
624
+ );
625
+ return result;
626
+ } catch (error) {
627
+ const runtimeError = error instanceof Error ? error : new Error(String(error));
628
+ await this.writeLog(queueName, job.id, "error", runtimeError.message);
629
+ throw runtimeError;
630
+ }
631
+ }
632
+ createExecutionContext(queueName, job, envelope, definition) {
633
+ const baseContext = {
634
+ input: envelope.input,
635
+ context: {},
636
+ job: {
637
+ id: job.id,
638
+ name: job.name,
639
+ queue: queueName,
640
+ attemptsMade: job.attemptsMade,
641
+ createdAt: new Date(job.timestamp),
642
+ metadata: envelope.metadata,
643
+ updateProgress: async (progress, message) => {
644
+ await job.updateProgress(progress, message);
645
+ await this.writeLog(
646
+ queueName,
647
+ job.id,
648
+ "info",
649
+ `Progress updated to ${progress}%.`
650
+ );
651
+ await this.invokeHook(
652
+ () => definition.onProgress?.({
653
+ ...baseContext,
654
+ progress,
655
+ message
656
+ })
657
+ );
658
+ }
659
+ },
660
+ scope: envelope.scope
661
+ };
662
+ return baseContext;
663
+ }
664
+ attachWorkerEvents(workerState, queueName, worker) {
665
+ worker.on("active", async (job) => {
666
+ workerState.activeJobs.add(job.id);
667
+ await this.invokeHandler(
668
+ () => workerState.handlers?.onActive?.({
669
+ job: this.mapJobSync(job, queueName)
670
+ })
671
+ );
672
+ });
673
+ worker.on("completed", async (job, result) => {
674
+ workerState.activeJobs.delete(job.id);
675
+ await this.invokeHandler(
676
+ () => workerState.handlers?.onSuccess?.({
677
+ job: this.mapJobSync(job, queueName, "completed"),
678
+ result
679
+ })
680
+ );
681
+ });
682
+ worker.on("failed", async (job, error) => {
683
+ workerState.activeJobs.delete(job.id);
684
+ await this.invokeHandler(
685
+ () => workerState.handlers?.onFailure?.({
686
+ job: this.mapJobSync(job, queueName, "failed"),
687
+ error
688
+ })
689
+ );
690
+ });
691
+ worker.on("drained", async () => {
692
+ if (workerState.activeJobs.size === 0) {
693
+ await this.invokeHandler(() => workerState.handlers?.onIdle?.());
694
+ }
695
+ });
696
+ worker.on("error", async (error) => {
697
+ await this.writeQueueLog(
698
+ queueName,
699
+ "error",
700
+ `Worker error: ${error.message}`
701
+ );
702
+ });
703
+ }
704
+ toWorkerHandle(worker) {
705
+ return {
706
+ id: worker.id,
707
+ queues: [...worker.queues],
708
+ pause: async () => {
709
+ worker.paused = true;
710
+ for (const runtime of worker.workers.values()) runtime.pause();
711
+ },
712
+ resume: async () => {
713
+ worker.paused = false;
714
+ for (const runtime of worker.workers.values()) runtime.resume();
715
+ },
716
+ close: async () => {
717
+ await this.closeWorkerState(worker);
718
+ },
719
+ isRunning: () => !worker.closed && !worker.paused,
720
+ isPaused: () => worker.paused,
721
+ isClosed: () => worker.closed,
722
+ getMetrics: async () => this.buildWorkerMetrics(worker)
723
+ };
724
+ }
725
+ buildWorkerMetrics(worker) {
726
+ return {
727
+ processed: worker.metrics.processed,
728
+ failed: worker.metrics.failed,
729
+ avgDuration: worker.metrics.processed > 0 ? worker.metrics.totalDuration / worker.metrics.processed : 0,
730
+ concurrency: worker.concurrency,
731
+ uptime: Date.now() - worker.startedAt.getTime()
732
+ };
733
+ }
734
+ async closeWorkerState(worker) {
735
+ worker.closed = true;
736
+ await Promise.all(
737
+ Array.from(worker.workers.values()).map((runtime) => runtime.close())
738
+ );
739
+ worker.workers.clear();
740
+ }
741
+ async requireJob(jobId, queue) {
742
+ const queueName = queue ?? await this.findQueueByJobId(jobId);
743
+ if (!queueName) {
744
+ throw new IgniterJobsError({
745
+ code: "JOBS_NOT_FOUND",
746
+ message: `Job "${jobId}" not found.`
747
+ });
748
+ }
749
+ const job = await this.getQueue(queueName).then(
750
+ (queueClient) => queueClient.getJob(jobId)
751
+ );
752
+ if (!job) {
753
+ throw new IgniterJobsError({
754
+ code: "JOBS_NOT_FOUND",
755
+ message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
756
+ });
757
+ }
758
+ return job;
759
+ }
760
+ async findQueueByJobId(jobId) {
761
+ for (const queueName of await this.getAllQueueNames()) {
762
+ const job = await this.getQueue(queueName).then(
763
+ (queueClient) => queueClient.getJob(jobId)
764
+ );
765
+ if (job) return queueName;
766
+ }
767
+ return null;
768
+ }
769
+ async mapJob(job, queueName, forcedState) {
770
+ const queue = await this.getQueue(queueName);
771
+ const rawState = await job.getState();
772
+ return this.mapJobSync(
773
+ job,
774
+ queueName,
775
+ forcedState ?? this.toPublicStatus(rawState, queue.isPaused())
776
+ );
777
+ }
778
+ mapJobSync(job, queueName, status) {
779
+ const envelope = this.normalizeEnvelope(job.data);
780
+ return {
781
+ id: job.id,
782
+ name: job.name,
783
+ queue: queueName,
784
+ status: status ?? this.toPublicStatus("waiting", false),
785
+ input: envelope.input,
786
+ result: job.returnvalue,
787
+ error: job.failedReason ?? void 0,
788
+ progress: typeof job.progress === "number" ? job.progress : 0,
789
+ attemptsMade: job.attemptsMade,
790
+ priority: job.priority ?? 0,
791
+ createdAt: new Date(job.timestamp),
792
+ startedAt: job.processedOn ? new Date(job.processedOn) : void 0,
793
+ completedAt: job.finishedOn ? new Date(job.finishedOn) : void 0,
794
+ metadata: envelope.metadata,
795
+ scope: envelope.scope
796
+ };
797
+ }
798
+ normalizeEnvelope(data) {
799
+ if (data && typeof data === "object" && data.__igniterJobs === true) {
800
+ return data;
801
+ }
802
+ return {
803
+ __igniterJobs: true,
804
+ input: data
805
+ };
806
+ }
807
+ toPublicStatus(rawState, isQueuePaused) {
808
+ if ((rawState === "waiting" || rawState === "prioritized") && isQueuePaused) {
809
+ return "paused";
810
+ }
811
+ switch (rawState) {
812
+ case "prioritized":
813
+ case "waiting-children":
814
+ return "waiting";
815
+ case "waiting":
816
+ case "active":
817
+ case "completed":
818
+ case "failed":
819
+ case "delayed":
820
+ case "paused":
821
+ return rawState;
822
+ default:
823
+ return "waiting";
824
+ }
825
+ }
826
+ toBunqueueStatus(status) {
827
+ switch (status) {
828
+ case "waiting":
829
+ return "waiting";
830
+ default:
831
+ return status;
832
+ }
833
+ }
834
+ toBunqueueStates(status) {
835
+ switch (status) {
836
+ case "waiting":
837
+ return ["waiting", "prioritized"];
838
+ case "paused":
839
+ return ["paused"];
840
+ default:
841
+ return [status];
842
+ }
843
+ }
844
+ distributeConcurrency(total, queues) {
845
+ const result = /* @__PURE__ */ new Map();
846
+ if (queues.length === 0) return result;
847
+ const base = Math.max(1, Math.floor(total / queues.length));
848
+ let remainder = Math.max(0, total - base * queues.length);
849
+ for (const queue of queues) {
850
+ const value = base + (remainder > 0 ? 1 : 0);
851
+ result.set(queue, value);
852
+ remainder = Math.max(0, remainder - 1);
853
+ }
854
+ return result;
855
+ }
856
+ shouldExecuteScheduledJob(schedule, now) {
857
+ if (!schedule) return true;
858
+ const day = now.getDay();
859
+ const isoDate = now.toISOString().slice(0, 10);
860
+ if (schedule.skipWeekends && (day === 0 || day === 6)) return false;
861
+ if (schedule.onlyWeekdays?.length && !schedule.onlyWeekdays.includes(day))
862
+ return false;
863
+ if (schedule.skipDates?.some((value) => value.slice(0, 10) === isoDate))
864
+ return false;
865
+ if (schedule.onlyBusinessHours && schedule.businessHours) {
866
+ const hour = now.getHours();
867
+ return hour >= schedule.businessHours.start && hour < schedule.businessHours.end;
868
+ }
869
+ return true;
870
+ }
871
+ parseLogEntry(entry) {
872
+ try {
873
+ const parsed = JSON.parse(entry);
874
+ if (parsed && typeof parsed.message === "string" && typeof parsed.timestamp === "string" && (parsed.level === "info" || parsed.level === "warn" || parsed.level === "error")) {
875
+ return {
876
+ timestamp: new Date(parsed.timestamp),
877
+ level: parsed.level,
878
+ message: parsed.message
879
+ };
880
+ }
881
+ } catch {
882
+ }
883
+ return {
884
+ timestamp: /* @__PURE__ */ new Date(),
885
+ level: "info",
886
+ message: entry
887
+ };
888
+ }
889
+ async writeLog(queueName, jobId, level, message) {
890
+ const queue = await this.getQueue(queueName);
891
+ await queue.addJobLog(
892
+ jobId,
893
+ JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), level, message })
894
+ );
895
+ }
896
+ async writeQueueLog(queueName, level, message) {
897
+ }
898
+ async invokeHook(fn) {
899
+ try {
900
+ await fn();
901
+ } catch {
902
+ }
903
+ }
904
+ async invokeHandler(fn) {
905
+ try {
906
+ await fn();
907
+ } catch {
908
+ }
909
+ }
910
+ };
911
+
912
+ exports.IgniterJobsBunSQLiteAdapter = IgniterJobsBunSQLiteAdapter;
913
+ //# sourceMappingURL=bun.js.map
914
+ //# sourceMappingURL=bun.js.map