@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/index.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { IgniterError } from '@igniter-js/
|
|
1
|
+
import { IgniterError } from '@igniter-js/common';
|
|
2
2
|
import { createBullMQAdapter } from '@igniter-js/adapter-bullmq';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
5
10
|
var IGNITER_JOBS_ERROR_CODES = {
|
|
6
11
|
JOBS_ADAPTER_REQUIRED: "JOBS_ADAPTER_REQUIRED",
|
|
7
12
|
JOBS_SERVICE_REQUIRED: "JOBS_SERVICE_REQUIRED",
|
|
@@ -34,7 +39,7 @@ var IgniterJobsError = class extends IgniterError {
|
|
|
34
39
|
}
|
|
35
40
|
};
|
|
36
41
|
|
|
37
|
-
// src/builders/
|
|
42
|
+
// src/builders/worker.builder.ts
|
|
38
43
|
var IgniterWorkerBuilder = class _IgniterWorkerBuilder {
|
|
39
44
|
constructor(params) {
|
|
40
45
|
this.adapter = params.adapter;
|
|
@@ -220,27 +225,6 @@ var IgniterJobsScopeUtils = class {
|
|
|
220
225
|
*/
|
|
221
226
|
IgniterJobsScopeUtils.SCOPE_METADATA_KEY = "__igniter_jobs_scope";
|
|
222
227
|
|
|
223
|
-
// src/utils/telemetry.ts
|
|
224
|
-
var IgniterJobsTelemetryUtils = class {
|
|
225
|
-
/**
|
|
226
|
-
* Emits a telemetry event if telemetry is configured.
|
|
227
|
-
* This is a fire-and-forget operation - telemetry errors are silently ignored
|
|
228
|
-
* to avoid affecting job processing.
|
|
229
|
-
*
|
|
230
|
-
* @param telemetry - The telemetry instance (optional).
|
|
231
|
-
* @param eventName - The name of the event to emit.
|
|
232
|
-
* @param attributes - Attributes to attach to the event.
|
|
233
|
-
* @param level - The log level for the event (default: 'info').
|
|
234
|
-
*/
|
|
235
|
-
static emitTelemetry(telemetry, eventName, attributes, level = "info") {
|
|
236
|
-
if (!telemetry) return;
|
|
237
|
-
try {
|
|
238
|
-
telemetry.emit(eventName, { attributes, level });
|
|
239
|
-
} catch {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
228
|
// src/utils/validation.ts
|
|
245
229
|
var IgniterJobsValidationUtils = class {
|
|
246
230
|
/**
|
|
@@ -315,428 +299,637 @@ var IgniterJobsValidationUtils = class {
|
|
|
315
299
|
}
|
|
316
300
|
};
|
|
317
301
|
|
|
318
|
-
// src/core/
|
|
319
|
-
var
|
|
302
|
+
// src/core/manager.ts
|
|
303
|
+
var registeredAdapters = /* @__PURE__ */ new WeakSet();
|
|
304
|
+
var IgniterJobsManager = class _IgniterJobsManager {
|
|
305
|
+
/**
|
|
306
|
+
* @internal
|
|
307
|
+
* Constructor is internal. Use `IgniterJobs.create()` instead.
|
|
308
|
+
*/
|
|
309
|
+
constructor(config, scopeEntry) {
|
|
310
|
+
this.config = config;
|
|
311
|
+
this.adapter = config.adapter;
|
|
312
|
+
this.telemetry = config.telemetry;
|
|
313
|
+
this.scopeEntry = scopeEntry;
|
|
314
|
+
}
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Public Methods
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
320
318
|
/**
|
|
321
|
-
*
|
|
319
|
+
* Creates a scoped jobs instance.
|
|
320
|
+
*
|
|
321
|
+
* Scopes provide multi-tenant isolation by adding scope metadata to all jobs.
|
|
322
|
+
* Jobs dispatched from a scoped instance will include scope information in their metadata.
|
|
323
|
+
*
|
|
324
|
+
* @param type - The scope type (e.g., 'organization', 'workspace')
|
|
325
|
+
* @param id - The scope identifier (e.g., 'org_123')
|
|
326
|
+
* @param tags - Optional additional tags for the scope
|
|
327
|
+
* @returns A new scoped IgniterJobsManager instance
|
|
322
328
|
*
|
|
323
329
|
* @example
|
|
324
330
|
* ```typescript
|
|
325
|
-
* const
|
|
326
|
-
*
|
|
327
|
-
* .withService('my-api')
|
|
328
|
-
* .withEnvironment('test')
|
|
329
|
-
* .withContext(async () => ({ db }))
|
|
330
|
-
* .addQueue(emailQueue)
|
|
331
|
-
* .build()
|
|
331
|
+
* const orgJobs = jobs.scope('organization', 'org_123')
|
|
332
|
+
* await orgJobs.email.sendWelcome.dispatch({ input: { to: 'user@example.com' } })
|
|
332
333
|
* ```
|
|
333
334
|
*/
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
scope(type, id, tags) {
|
|
336
|
+
const entry = { type, id: String(id), tags };
|
|
337
|
+
return new _IgniterJobsManager(this.config, entry);
|
|
336
338
|
}
|
|
337
339
|
/**
|
|
338
|
-
*
|
|
340
|
+
* Subscribes to all job events from this jobs instance.
|
|
341
|
+
*
|
|
342
|
+
* @param handler - The event handler function
|
|
343
|
+
* @returns A function to unsubscribe
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```typescript
|
|
347
|
+
* const unsubscribe = await jobs.subscribe((event) => {
|
|
348
|
+
* console.log('Job event:', event.type, event.data)
|
|
349
|
+
* })
|
|
350
|
+
* // Later: await unsubscribe()
|
|
351
|
+
* ```
|
|
339
352
|
*/
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
return
|
|
353
|
+
async subscribe(handler) {
|
|
354
|
+
const channel = this.buildEventsChannel();
|
|
355
|
+
return this.adapter.subscribeEvent(channel, async (payload) => {
|
|
356
|
+
await handler(payload);
|
|
357
|
+
});
|
|
343
358
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Searches for jobs, queues, or workers based on filters.
|
|
361
|
+
*
|
|
362
|
+
* @param target - The target to search ('jobs', 'queues', or 'workers')
|
|
363
|
+
* @param filter - The filter criteria
|
|
364
|
+
* @returns The search results
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const failedJobs = await jobs.search('jobs', { status: 'failed', queue: 'email' })
|
|
369
|
+
* const queues = await jobs.search('queues', {})
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
async search(target, filter) {
|
|
373
|
+
switch (target) {
|
|
374
|
+
case "jobs":
|
|
375
|
+
return this.adapter.searchJobs(filter);
|
|
376
|
+
case "queues":
|
|
377
|
+
return this.adapter.searchQueues(filter);
|
|
378
|
+
case "workers":
|
|
379
|
+
return this.adapter.searchWorkers(filter);
|
|
380
|
+
default:
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Gracefully shuts down the jobs instance.
|
|
386
|
+
*
|
|
387
|
+
* This will close all connections and stop all workers.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* await jobs.shutdown()
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
async shutdown() {
|
|
395
|
+
await this.adapter.shutdown();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Creates a new worker builder for processing jobs.
|
|
399
|
+
*
|
|
400
|
+
* @returns A new IgniterWorkerBuilder instance
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```typescript
|
|
404
|
+
* const worker = await jobs.worker
|
|
405
|
+
* .create()
|
|
406
|
+
* .addQueue('email')
|
|
407
|
+
* .withConcurrency(5)
|
|
408
|
+
* .start()
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
get worker() {
|
|
412
|
+
return {
|
|
390
413
|
create: () => new IgniterWorkerBuilder({
|
|
391
|
-
adapter:
|
|
392
|
-
allowedQueues: Object.keys(
|
|
414
|
+
adapter: this.adapter,
|
|
415
|
+
allowedQueues: Object.keys(this.config.queues)
|
|
393
416
|
})
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
if (internal.config.scopeDefinition) {
|
|
397
|
-
runtime.scope = (type, id, tags) => {
|
|
398
|
-
const scope = { type, id, tags };
|
|
399
|
-
return createRuntime(internal, scope);
|
|
400
417
|
};
|
|
401
418
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Converts the manager to a runtime proxy with typed queue accessors.
|
|
421
|
+
*
|
|
422
|
+
* @internal
|
|
423
|
+
* @returns The runtime proxy
|
|
424
|
+
*/
|
|
425
|
+
toRuntime() {
|
|
426
|
+
this.ensureRegistered();
|
|
427
|
+
const self = this;
|
|
428
|
+
const queueNames = Object.keys(this.config.queues);
|
|
429
|
+
const handler = {
|
|
430
|
+
get(target, prop) {
|
|
431
|
+
if (prop === "config") return target.config;
|
|
432
|
+
if (prop === "subscribe") return target.subscribe.bind(target);
|
|
433
|
+
if (prop === "search") return target.search.bind(target);
|
|
434
|
+
if (prop === "shutdown") return target.shutdown.bind(target);
|
|
435
|
+
if (prop === "worker") return target.worker;
|
|
436
|
+
if (prop === "scope" && target.config.scopeDefinition) {
|
|
437
|
+
return (type, id, tags) => target.scope(type, id, tags).toRuntime();
|
|
438
|
+
}
|
|
439
|
+
if (queueNames.includes(prop)) {
|
|
440
|
+
return self.createQueueAccessor(prop);
|
|
441
|
+
}
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
return new Proxy(this, handler);
|
|
411
446
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
// Private Methods - Registration
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
/**
|
|
451
|
+
* Ensures all jobs and crons are registered with the adapter.
|
|
452
|
+
* Uses a shared WeakSet to prevent duplicate registration across scoped instances.
|
|
453
|
+
* @internal
|
|
454
|
+
*/
|
|
455
|
+
ensureRegistered() {
|
|
456
|
+
if (registeredAdapters.has(this.adapter)) return;
|
|
457
|
+
for (const [queueName, queue] of Object.entries(
|
|
458
|
+
this.config.queues
|
|
420
459
|
)) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
config,
|
|
426
|
-
adapter,
|
|
460
|
+
for (const [jobName, def] of Object.entries(
|
|
461
|
+
queue.jobs
|
|
462
|
+
)) {
|
|
463
|
+
this.adapter.registerJob(
|
|
427
464
|
queueName,
|
|
428
465
|
jobName,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
adapter.registerCron(
|
|
437
|
-
queueName,
|
|
438
|
-
cronName,
|
|
439
|
-
wrapCronDefinition({
|
|
440
|
-
config,
|
|
441
|
-
adapter,
|
|
466
|
+
this.wrapJobDefinition(queueName, jobName, def)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
for (const [cronName, cron] of Object.entries(
|
|
470
|
+
queue.crons
|
|
471
|
+
)) {
|
|
472
|
+
this.adapter.registerCron(
|
|
442
473
|
queueName,
|
|
443
474
|
cronName,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
475
|
+
this.wrapCronDefinition(queueName, cronName, cron)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
447
478
|
}
|
|
479
|
+
registeredAdapters.add(this.adapter);
|
|
448
480
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Private Methods - Accessors
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
/**
|
|
485
|
+
* Creates a queue accessor with job accessors and management methods.
|
|
486
|
+
* @internal
|
|
487
|
+
*/
|
|
488
|
+
createQueueAccessor(queueName) {
|
|
489
|
+
const queueConfig = this.config.queues[queueName];
|
|
490
|
+
const self = this;
|
|
491
|
+
const queueAccessor = {
|
|
492
|
+
async list(filter) {
|
|
493
|
+
return self.adapter.queues.getJobs(queueName, filter);
|
|
494
|
+
},
|
|
495
|
+
get() {
|
|
496
|
+
return {
|
|
497
|
+
retrieve: () => self.adapter.getQueueInfo(queueName),
|
|
498
|
+
pause: () => self.adapter.pauseQueue(queueName),
|
|
499
|
+
resume: () => self.adapter.resumeQueue(queueName),
|
|
500
|
+
drain: () => self.adapter.drainQueue(queueName),
|
|
501
|
+
clean: (options) => self.adapter.cleanQueue(queueName, options),
|
|
502
|
+
obliterate: (options) => self.adapter.obliterateQueue(queueName, options),
|
|
503
|
+
retryAll: () => self.adapter.retryAllInQueue(queueName)
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
async subscribe(handler) {
|
|
507
|
+
const channel = self.buildEventsChannel();
|
|
508
|
+
return self.adapter.subscribeEvent(channel, async (event) => {
|
|
509
|
+
const typed = event;
|
|
510
|
+
if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:`)) {
|
|
511
|
+
await handler(typed);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
jobs: {}
|
|
516
|
+
};
|
|
517
|
+
for (const jobName of Object.keys(queueConfig.jobs)) {
|
|
518
|
+
const jobAccessor = this.createJobAccessor(queueName, jobName);
|
|
519
|
+
queueAccessor.jobs[jobName] = jobAccessor;
|
|
520
|
+
queueAccessor[jobName] = jobAccessor;
|
|
521
|
+
}
|
|
522
|
+
return queueAccessor;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Creates a job accessor with dispatch, schedule, and management methods.
|
|
526
|
+
* @internal
|
|
527
|
+
*/
|
|
528
|
+
createJobAccessor(queueName, jobName) {
|
|
529
|
+
const self = this;
|
|
457
530
|
return {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
531
|
+
async dispatch(params) {
|
|
532
|
+
return self.dispatchJob(queueName, jobName, params);
|
|
533
|
+
},
|
|
534
|
+
async schedule(params) {
|
|
535
|
+
return self.scheduleJob(queueName, jobName, params);
|
|
536
|
+
},
|
|
537
|
+
get(id) {
|
|
538
|
+
return {
|
|
539
|
+
retrieve: () => self.adapter.getJob(id, queueName),
|
|
540
|
+
retry: () => self.adapter.retryJob(id, queueName),
|
|
541
|
+
remove: () => self.adapter.removeJob(id, queueName),
|
|
542
|
+
promote: () => self.adapter.promoteJob(id, queueName),
|
|
543
|
+
move: (state, reason) => {
|
|
544
|
+
if (state !== "failed") return Promise.resolve();
|
|
545
|
+
return self.adapter.moveJobToFailed(id, reason, queueName);
|
|
546
|
+
},
|
|
547
|
+
state: () => self.adapter.getJobState(id, queueName),
|
|
548
|
+
progress: () => self.adapter.getJobProgress(id, queueName),
|
|
549
|
+
logs: () => self.adapter.getJobLogs(id, queueName)
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
many(ids) {
|
|
553
|
+
return {
|
|
554
|
+
retry: () => self.adapter.retryManyJobs(ids, queueName),
|
|
555
|
+
remove: () => self.adapter.removeManyJobs(ids, queueName)
|
|
556
|
+
};
|
|
557
|
+
},
|
|
558
|
+
pause: () => self.adapter.pauseJobType(queueName, jobName),
|
|
559
|
+
resume: () => self.adapter.resumeJobType(queueName, jobName),
|
|
560
|
+
async subscribe(handler) {
|
|
561
|
+
const channel = self.buildEventsChannel();
|
|
562
|
+
return self.adapter.subscribeEvent(channel, async (event) => {
|
|
563
|
+
const typed = event;
|
|
564
|
+
if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:${jobName}:`)) {
|
|
565
|
+
await handler(typed);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
462
569
|
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
570
|
+
}
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
// Private Methods - Job Operations
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
/**
|
|
575
|
+
* Dispatches a job for immediate or delayed processing.
|
|
576
|
+
* @internal
|
|
577
|
+
*/
|
|
578
|
+
async dispatchJob(queueName, jobName, params) {
|
|
579
|
+
const definition = this.getJobDefinition(queueName, jobName);
|
|
580
|
+
if (definition?.input) {
|
|
581
|
+
await IgniterJobsValidationUtils.validateInput(
|
|
582
|
+
definition.input,
|
|
583
|
+
params.input
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const scope = this.resolveScope(params.scope);
|
|
587
|
+
const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
|
|
588
|
+
params.metadata,
|
|
589
|
+
scope
|
|
467
590
|
);
|
|
468
|
-
await
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
591
|
+
const jobId = await this.adapter.dispatch({
|
|
592
|
+
queue: queueName,
|
|
593
|
+
jobName,
|
|
594
|
+
...params,
|
|
472
595
|
scope,
|
|
473
|
-
|
|
474
|
-
type: IgniterJobsEventsUtils.buildJobEventType(
|
|
475
|
-
queueName,
|
|
476
|
-
cronName,
|
|
477
|
-
event
|
|
478
|
-
),
|
|
479
|
-
data,
|
|
480
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
481
|
-
scope
|
|
482
|
-
}
|
|
596
|
+
metadata
|
|
483
597
|
});
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
512
|
-
});
|
|
513
|
-
throw error;
|
|
514
|
-
}
|
|
598
|
+
await this.publishJobEvent(
|
|
599
|
+
queueName,
|
|
600
|
+
jobName,
|
|
601
|
+
"enqueued",
|
|
602
|
+
{ jobId, queue: queueName, jobName },
|
|
603
|
+
scope
|
|
604
|
+
);
|
|
605
|
+
this.emitTelemetry("igniter.jobs.job.enqueued", {
|
|
606
|
+
"ctx.job.id": jobId,
|
|
607
|
+
"ctx.job.name": jobName,
|
|
608
|
+
"ctx.job.queue": queueName,
|
|
609
|
+
"ctx.job.priority": params.priority ?? null,
|
|
610
|
+
"ctx.job.delay": params.delay ?? null
|
|
611
|
+
});
|
|
612
|
+
return jobId;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Schedules a job for future processing.
|
|
616
|
+
* @internal
|
|
617
|
+
*/
|
|
618
|
+
async scheduleJob(queueName, jobName, params) {
|
|
619
|
+
const definition = this.getJobDefinition(queueName, jobName);
|
|
620
|
+
if (definition?.input) {
|
|
621
|
+
await IgniterJobsValidationUtils.validateInput(
|
|
622
|
+
definition.input,
|
|
623
|
+
params.input
|
|
624
|
+
);
|
|
515
625
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const { config, queueName, jobName, definition } = params;
|
|
520
|
-
const buildExecutionContext = async (ctx) => {
|
|
521
|
-
const realContext = await config.contextFactory();
|
|
522
|
-
const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(ctx.job.metadata);
|
|
523
|
-
return {
|
|
524
|
-
...ctx,
|
|
525
|
-
context: realContext,
|
|
526
|
-
job: { ...ctx.job, name: jobName, queue: queueName },
|
|
626
|
+
const scope = this.resolveScope(params.scope);
|
|
627
|
+
const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
|
|
628
|
+
params.metadata,
|
|
527
629
|
scope
|
|
528
|
-
};
|
|
529
|
-
};
|
|
530
|
-
const publishLifecycle = async (event, ctx, data) => {
|
|
531
|
-
const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(
|
|
532
|
-
ctx.job?.metadata
|
|
533
630
|
);
|
|
534
|
-
await
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
631
|
+
const jobId = await this.adapter.schedule({
|
|
632
|
+
queue: queueName,
|
|
633
|
+
jobName,
|
|
634
|
+
...params,
|
|
538
635
|
scope,
|
|
539
|
-
|
|
540
|
-
|
|
636
|
+
metadata
|
|
637
|
+
});
|
|
638
|
+
await this.publishJobEvent(
|
|
639
|
+
queueName,
|
|
640
|
+
jobName,
|
|
641
|
+
"scheduled",
|
|
642
|
+
{ jobId, queue: queueName, jobName },
|
|
643
|
+
scope
|
|
644
|
+
);
|
|
645
|
+
this.emitTelemetry("igniter.jobs.job.scheduled", {
|
|
646
|
+
"ctx.job.id": jobId,
|
|
647
|
+
"ctx.job.name": jobName,
|
|
648
|
+
"ctx.job.queue": queueName,
|
|
649
|
+
"ctx.job.scheduledAt": params.runAt?.toISOString?.() ?? null,
|
|
650
|
+
"ctx.job.cron": params.cron ?? null
|
|
651
|
+
});
|
|
652
|
+
return jobId;
|
|
653
|
+
}
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
// Private Methods - Handler Wrapping
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
/**
|
|
658
|
+
* Wraps a job definition with context building, validation, and telemetry.
|
|
659
|
+
* @internal
|
|
660
|
+
*/
|
|
661
|
+
wrapJobDefinition(queueName, jobName, definition) {
|
|
662
|
+
const self = this;
|
|
663
|
+
return {
|
|
664
|
+
...definition,
|
|
665
|
+
handler: async (ctx) => {
|
|
666
|
+
const enhanced = await self.buildExecutionContext(
|
|
667
|
+
ctx,
|
|
668
|
+
queueName,
|
|
669
|
+
jobName
|
|
670
|
+
);
|
|
671
|
+
if (definition.input) {
|
|
672
|
+
const validated = await IgniterJobsValidationUtils.validateInput(
|
|
673
|
+
definition.input,
|
|
674
|
+
enhanced.input
|
|
675
|
+
);
|
|
676
|
+
enhanced.input = validated;
|
|
677
|
+
}
|
|
678
|
+
return definition.handler(enhanced);
|
|
679
|
+
},
|
|
680
|
+
onStart: async (ctx) => {
|
|
681
|
+
const enhanced = await self.buildExecutionContext(
|
|
682
|
+
ctx,
|
|
683
|
+
queueName,
|
|
684
|
+
jobName
|
|
685
|
+
);
|
|
686
|
+
await self.publishJobEvent(
|
|
541
687
|
queueName,
|
|
542
688
|
jobName,
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
...definition,
|
|
553
|
-
handler: async (ctx) => {
|
|
554
|
-
const enhanced = await buildExecutionContext(ctx);
|
|
555
|
-
if (definition.input) {
|
|
556
|
-
const validated = await IgniterJobsValidationUtils.validateInput(
|
|
557
|
-
definition.input,
|
|
558
|
-
enhanced.input
|
|
689
|
+
"started",
|
|
690
|
+
{
|
|
691
|
+
jobId: enhanced.job.id,
|
|
692
|
+
jobName,
|
|
693
|
+
queue: queueName,
|
|
694
|
+
attemptsMade: enhanced.job.attemptsMade,
|
|
695
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
696
|
+
},
|
|
697
|
+
enhanced.scope
|
|
559
698
|
);
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
return definition.handler(enhanced);
|
|
563
|
-
},
|
|
564
|
-
onStart: async (ctx) => {
|
|
565
|
-
const enhanced = await buildExecutionContext(ctx);
|
|
566
|
-
await publishLifecycle("started", enhanced, {
|
|
567
|
-
jobId: enhanced.job.id,
|
|
568
|
-
jobName,
|
|
569
|
-
queue: queueName,
|
|
570
|
-
attemptsMade: enhanced.job.attemptsMade,
|
|
571
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
572
|
-
});
|
|
573
|
-
IgniterJobsTelemetryUtils.emitTelemetry(
|
|
574
|
-
config.telemetry,
|
|
575
|
-
"igniter.jobs.job.started",
|
|
576
|
-
{
|
|
699
|
+
self.emitTelemetry("igniter.jobs.job.started", {
|
|
577
700
|
"ctx.job.id": enhanced.job.id,
|
|
578
701
|
"ctx.job.name": jobName,
|
|
579
702
|
"ctx.job.queue": queueName,
|
|
580
703
|
"ctx.job.attempt": enhanced.job.attemptsMade,
|
|
581
704
|
"ctx.job.maxAttempts": definition.attempts ?? 3
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
705
|
+
});
|
|
706
|
+
await definition.onStart?.(enhanced);
|
|
707
|
+
},
|
|
708
|
+
onSuccess: async (ctx) => {
|
|
709
|
+
const enhanced = await self.buildExecutionContext(
|
|
710
|
+
ctx,
|
|
711
|
+
queueName,
|
|
712
|
+
jobName
|
|
713
|
+
);
|
|
714
|
+
const duration = ctx.duration ?? ctx.executionTime ?? 0;
|
|
715
|
+
await self.publishJobEvent(
|
|
716
|
+
queueName,
|
|
594
717
|
jobName,
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
718
|
+
"completed",
|
|
719
|
+
{
|
|
720
|
+
jobId: enhanced.job.id,
|
|
721
|
+
jobName,
|
|
722
|
+
queue: queueName,
|
|
723
|
+
result: ctx.result,
|
|
724
|
+
duration,
|
|
725
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
726
|
+
},
|
|
727
|
+
enhanced.scope
|
|
728
|
+
);
|
|
729
|
+
self.emitTelemetry("igniter.jobs.job.completed", {
|
|
605
730
|
"ctx.job.id": enhanced.job.id,
|
|
606
731
|
"ctx.job.name": jobName,
|
|
607
732
|
"ctx.job.queue": queueName,
|
|
608
733
|
"ctx.job.duration": typeof duration === "number" ? duration : 0
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
734
|
+
});
|
|
735
|
+
await definition.onSuccess?.(enhanced);
|
|
736
|
+
},
|
|
737
|
+
onFailure: async (ctx) => {
|
|
738
|
+
const enhanced = await self.buildExecutionContext(
|
|
739
|
+
ctx,
|
|
740
|
+
queueName,
|
|
741
|
+
jobName
|
|
742
|
+
);
|
|
743
|
+
const duration = ctx.duration ?? ctx.executionTime ?? 0;
|
|
744
|
+
const isFinalAttempt = Boolean(ctx.isFinalAttempt);
|
|
745
|
+
const errorMessage = ctx.error?.message ?? String(ctx.error);
|
|
746
|
+
const errorCode = ctx.error?.code;
|
|
747
|
+
const maxAttempts = definition.attempts ?? 3;
|
|
748
|
+
await self.publishJobEvent(
|
|
749
|
+
queueName,
|
|
625
750
|
jobName,
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
"
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
751
|
+
"failed",
|
|
752
|
+
{
|
|
753
|
+
jobId: enhanced.job.id,
|
|
754
|
+
jobName,
|
|
755
|
+
queue: queueName,
|
|
756
|
+
error: { message: errorMessage },
|
|
757
|
+
attemptsMade: enhanced.job.attemptsMade,
|
|
758
|
+
isFinalAttempt,
|
|
759
|
+
duration,
|
|
760
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
761
|
+
},
|
|
762
|
+
enhanced.scope
|
|
763
|
+
);
|
|
764
|
+
self.emitTelemetry(
|
|
765
|
+
"igniter.jobs.job.failed",
|
|
766
|
+
{
|
|
767
|
+
"ctx.job.id": enhanced.job.id,
|
|
768
|
+
"ctx.job.name": jobName,
|
|
769
|
+
"ctx.job.queue": queueName,
|
|
770
|
+
"ctx.job.error.message": errorMessage,
|
|
771
|
+
"ctx.job.error.code": errorCode ?? null,
|
|
772
|
+
"ctx.job.attempt": enhanced.job.attemptsMade,
|
|
773
|
+
"ctx.job.maxAttempts": maxAttempts,
|
|
774
|
+
"ctx.job.isFinalAttempt": isFinalAttempt
|
|
775
|
+
},
|
|
776
|
+
"error"
|
|
777
|
+
);
|
|
778
|
+
await definition.onFailure?.(enhanced);
|
|
779
|
+
},
|
|
780
|
+
onProgress: definition.onProgress ? async (ctx) => {
|
|
781
|
+
const enhanced = await self.buildExecutionContext(
|
|
782
|
+
ctx,
|
|
783
|
+
queueName,
|
|
784
|
+
jobName
|
|
785
|
+
);
|
|
786
|
+
const progress = ctx.progress ?? 0;
|
|
787
|
+
const message = ctx.message;
|
|
788
|
+
await self.publishJobEvent(
|
|
789
|
+
queueName,
|
|
790
|
+
jobName,
|
|
791
|
+
"progress",
|
|
792
|
+
{
|
|
793
|
+
jobId: enhanced.job.id,
|
|
794
|
+
jobName,
|
|
795
|
+
queue: queueName,
|
|
796
|
+
progress,
|
|
797
|
+
message,
|
|
798
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
799
|
+
},
|
|
800
|
+
enhanced.scope
|
|
801
|
+
);
|
|
802
|
+
self.emitTelemetry("igniter.jobs.job.progress", {
|
|
667
803
|
"ctx.job.id": enhanced.job.id,
|
|
668
804
|
"ctx.job.name": jobName,
|
|
669
805
|
"ctx.job.queue": queueName,
|
|
670
806
|
"ctx.job.progress": typeof progress === "number" ? progress : 0,
|
|
671
807
|
"ctx.job.progress.message": message ?? null
|
|
808
|
+
});
|
|
809
|
+
await definition.onProgress?.(enhanced);
|
|
810
|
+
} : void 0
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Wraps a cron definition with context building and lifecycle events.
|
|
815
|
+
* @internal
|
|
816
|
+
*/
|
|
817
|
+
wrapCronDefinition(queueName, cronName, definition) {
|
|
818
|
+
const self = this;
|
|
819
|
+
return {
|
|
820
|
+
...definition,
|
|
821
|
+
handler: async (ctx) => {
|
|
822
|
+
const enhanced = await self.buildCronExecutionContext(
|
|
823
|
+
ctx,
|
|
824
|
+
queueName,
|
|
825
|
+
cronName
|
|
826
|
+
);
|
|
827
|
+
await self.publishJobEvent(
|
|
828
|
+
queueName,
|
|
829
|
+
cronName,
|
|
830
|
+
"started",
|
|
831
|
+
{
|
|
832
|
+
jobId: enhanced.job?.id,
|
|
833
|
+
jobName: cronName,
|
|
834
|
+
queue: queueName,
|
|
835
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
836
|
+
},
|
|
837
|
+
enhanced.scope
|
|
838
|
+
);
|
|
839
|
+
try {
|
|
840
|
+
const result = await definition.handler(enhanced);
|
|
841
|
+
await self.publishJobEvent(
|
|
842
|
+
queueName,
|
|
843
|
+
cronName,
|
|
844
|
+
"completed",
|
|
845
|
+
{
|
|
846
|
+
jobId: enhanced.job?.id,
|
|
847
|
+
jobName: cronName,
|
|
848
|
+
queue: queueName,
|
|
849
|
+
result,
|
|
850
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
851
|
+
},
|
|
852
|
+
enhanced.scope
|
|
853
|
+
);
|
|
854
|
+
return result;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
await self.publishJobEvent(
|
|
857
|
+
queueName,
|
|
858
|
+
cronName,
|
|
859
|
+
"failed",
|
|
860
|
+
{
|
|
861
|
+
jobId: enhanced.job?.id,
|
|
862
|
+
jobName: cronName,
|
|
863
|
+
queue: queueName,
|
|
864
|
+
error: { message: error?.message ?? String(error) },
|
|
865
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
866
|
+
},
|
|
867
|
+
enhanced.scope
|
|
868
|
+
);
|
|
869
|
+
throw error;
|
|
672
870
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
} : void 0
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
function createQueueAccessor(params) {
|
|
679
|
-
const { internal, boundScope, queueName, queueConfig } = params;
|
|
680
|
-
const queueAccessor = {
|
|
681
|
-
async list(filter) {
|
|
682
|
-
return internal.adapter.queues.getJobs(queueName, filter);
|
|
683
|
-
},
|
|
684
|
-
get() {
|
|
685
|
-
return {
|
|
686
|
-
retrieve: () => internal.adapter.getQueueInfo(queueName),
|
|
687
|
-
pause: () => internal.adapter.pauseQueue(queueName),
|
|
688
|
-
resume: () => internal.adapter.resumeQueue(queueName),
|
|
689
|
-
drain: () => internal.adapter.drainQueue(queueName),
|
|
690
|
-
clean: (options) => internal.adapter.cleanQueue(queueName, options),
|
|
691
|
-
obliterate: (options) => internal.adapter.obliterateQueue(queueName, options),
|
|
692
|
-
retryAll: () => internal.adapter.retryAllInQueue(queueName)
|
|
693
|
-
};
|
|
694
|
-
},
|
|
695
|
-
async subscribe(handler) {
|
|
696
|
-
const channel = boundScope ? IgniterJobsPrefix.buildEventsChannel({
|
|
697
|
-
service: internal.config.service,
|
|
698
|
-
environment: internal.config.environment,
|
|
699
|
-
scope: { type: boundScope.type, id: boundScope.id }
|
|
700
|
-
}) : IgniterJobsPrefix.buildEventsChannel({
|
|
701
|
-
service: internal.config.service,
|
|
702
|
-
environment: internal.config.environment
|
|
703
|
-
});
|
|
704
|
-
return internal.adapter.subscribeEvent(channel, async (event) => {
|
|
705
|
-
const typed = event;
|
|
706
|
-
if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:`)) {
|
|
707
|
-
await handler(typed);
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
},
|
|
711
|
-
jobs: {}
|
|
712
|
-
};
|
|
713
|
-
for (const jobName of Object.keys(queueConfig.jobs)) {
|
|
714
|
-
queueAccessor.jobs[jobName] = createJobAccessor({
|
|
715
|
-
internal,
|
|
716
|
-
boundScope,
|
|
717
|
-
queueName,
|
|
718
|
-
jobName
|
|
719
|
-
});
|
|
720
|
-
queueAccessor[jobName] = queueAccessor.jobs[jobName];
|
|
871
|
+
}
|
|
872
|
+
};
|
|
721
873
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
874
|
+
// ---------------------------------------------------------------------------
|
|
875
|
+
// Private Methods - Helpers
|
|
876
|
+
// ---------------------------------------------------------------------------
|
|
877
|
+
/**
|
|
878
|
+
* Builds the execution context for a job.
|
|
879
|
+
* @internal
|
|
880
|
+
*/
|
|
881
|
+
async buildExecutionContext(ctx, queueName, jobName) {
|
|
882
|
+
const realContext = await this.config.contextFactory();
|
|
883
|
+
const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(ctx.job.metadata);
|
|
884
|
+
return {
|
|
885
|
+
...ctx,
|
|
886
|
+
context: realContext,
|
|
887
|
+
job: { ...ctx.job, name: jobName, queue: queueName },
|
|
888
|
+
scope
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Builds the execution context for a cron job.
|
|
893
|
+
* @internal
|
|
894
|
+
*/
|
|
895
|
+
async buildCronExecutionContext(ctx, queueName, cronName) {
|
|
896
|
+
const realContext = await this.config.contextFactory();
|
|
897
|
+
const scope = ctx.scope ?? IgniterJobsScopeUtils.extractScopeFromMetadata(
|
|
898
|
+
ctx.job?.metadata
|
|
899
|
+
);
|
|
900
|
+
return {
|
|
901
|
+
...ctx,
|
|
902
|
+
context: realContext,
|
|
903
|
+
job: { ...ctx.job, name: cronName, queue: queueName },
|
|
904
|
+
scope
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Gets a job definition by queue and job name.
|
|
909
|
+
* @internal
|
|
910
|
+
*/
|
|
911
|
+
getJobDefinition(queueName, jobName) {
|
|
912
|
+
const queue = this.config.queues[queueName];
|
|
913
|
+
return queue?.jobs?.[jobName];
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Resolves the effective scope for a job operation.
|
|
917
|
+
* @internal
|
|
918
|
+
*/
|
|
919
|
+
resolveScope(paramsScope) {
|
|
920
|
+
if (!this.config.scopeDefinition) return void 0;
|
|
728
921
|
const required = Object.values(
|
|
729
|
-
|
|
922
|
+
this.config.scopeDefinition
|
|
730
923
|
)[0]?.required ?? false;
|
|
731
|
-
const effective =
|
|
924
|
+
const effective = this.scopeEntry ?? paramsScope;
|
|
732
925
|
if (required && !effective) {
|
|
733
926
|
throw new IgniterJobsError({
|
|
734
927
|
code: "JOBS_CONFIGURATION_INVALID",
|
|
735
928
|
message: "Scope is required for this jobs instance."
|
|
736
929
|
});
|
|
737
930
|
}
|
|
738
|
-
if (
|
|
739
|
-
if (
|
|
931
|
+
if (this.scopeEntry && paramsScope) {
|
|
932
|
+
if (this.scopeEntry.type !== paramsScope.type || this.scopeEntry.id !== paramsScope.id) {
|
|
740
933
|
throw new IgniterJobsError({
|
|
741
934
|
code: "JOBS_CONFIGURATION_INVALID",
|
|
742
935
|
message: "Cannot override scope on a scoped jobs instance."
|
|
@@ -744,308 +937,57 @@ function createJobAccessor(params) {
|
|
|
744
937
|
}
|
|
745
938
|
}
|
|
746
939
|
return effective;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Builds the events channel string for subscriptions.
|
|
943
|
+
* @internal
|
|
944
|
+
*/
|
|
945
|
+
buildEventsChannel() {
|
|
946
|
+
return this.scopeEntry ? IgniterJobsPrefix.buildEventsChannel({
|
|
947
|
+
service: this.config.service,
|
|
948
|
+
environment: this.config.environment,
|
|
949
|
+
scope: { type: this.scopeEntry.type, id: this.scopeEntry.id }
|
|
950
|
+
}) : IgniterJobsPrefix.buildEventsChannel({
|
|
951
|
+
service: this.config.service,
|
|
952
|
+
environment: this.config.environment
|
|
755
953
|
});
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
const scope = resolveScope(params2.scope);
|
|
771
|
-
const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
|
|
772
|
-
params2.metadata,
|
|
773
|
-
scope
|
|
774
|
-
);
|
|
775
|
-
const jobId = await internal.adapter.dispatch({
|
|
776
|
-
queue: queueName,
|
|
777
|
-
jobName,
|
|
778
|
-
...params2,
|
|
779
|
-
scope,
|
|
780
|
-
metadata
|
|
781
|
-
});
|
|
782
|
-
await publish({
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Publishes a job lifecycle event.
|
|
957
|
+
* @internal
|
|
958
|
+
*/
|
|
959
|
+
async publishJobEvent(queueName, jobName, eventType, data, scope) {
|
|
960
|
+
await IgniterJobsEventsUtils.publishJobsEvent({
|
|
961
|
+
adapter: this.adapter,
|
|
962
|
+
service: this.config.service,
|
|
963
|
+
environment: this.config.environment,
|
|
964
|
+
scope,
|
|
965
|
+
event: {
|
|
783
966
|
type: IgniterJobsEventsUtils.buildJobEventType(
|
|
784
967
|
queueName,
|
|
785
968
|
jobName,
|
|
786
|
-
|
|
969
|
+
eventType
|
|
787
970
|
),
|
|
788
|
-
data
|
|
971
|
+
data,
|
|
789
972
|
timestamp: /* @__PURE__ */ new Date(),
|
|
790
973
|
scope
|
|
791
|
-
});
|
|
792
|
-
IgniterJobsTelemetryUtils.emitTelemetry(
|
|
793
|
-
internal.config.telemetry,
|
|
794
|
-
"igniter.jobs.job.enqueued",
|
|
795
|
-
{
|
|
796
|
-
"ctx.job.id": jobId,
|
|
797
|
-
"ctx.job.name": jobName,
|
|
798
|
-
"ctx.job.queue": queueName,
|
|
799
|
-
"ctx.job.priority": params2.priority ?? null,
|
|
800
|
-
"ctx.job.delay": params2.delay ?? null
|
|
801
|
-
}
|
|
802
|
-
);
|
|
803
|
-
return jobId;
|
|
804
|
-
},
|
|
805
|
-
async schedule(params2) {
|
|
806
|
-
const definition = getDefinition();
|
|
807
|
-
if (definition?.input) {
|
|
808
|
-
await IgniterJobsValidationUtils.validateInput(
|
|
809
|
-
definition.input,
|
|
810
|
-
params2.input
|
|
811
|
-
);
|
|
812
974
|
}
|
|
813
|
-
const scope = resolveScope(params2.scope);
|
|
814
|
-
const metadata = IgniterJobsScopeUtils.mergeMetadataWithScope(
|
|
815
|
-
params2.metadata,
|
|
816
|
-
scope
|
|
817
|
-
);
|
|
818
|
-
const jobId = await internal.adapter.schedule({
|
|
819
|
-
queue: queueName,
|
|
820
|
-
jobName,
|
|
821
|
-
...params2,
|
|
822
|
-
scope,
|
|
823
|
-
metadata
|
|
824
|
-
});
|
|
825
|
-
await publish({
|
|
826
|
-
type: IgniterJobsEventsUtils.buildJobEventType(
|
|
827
|
-
queueName,
|
|
828
|
-
jobName,
|
|
829
|
-
"scheduled"
|
|
830
|
-
),
|
|
831
|
-
data: { jobId, queue: queueName, jobName },
|
|
832
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
833
|
-
scope
|
|
834
|
-
});
|
|
835
|
-
IgniterJobsTelemetryUtils.emitTelemetry(
|
|
836
|
-
internal.config.telemetry,
|
|
837
|
-
"igniter.jobs.job.scheduled",
|
|
838
|
-
{
|
|
839
|
-
"ctx.job.id": jobId,
|
|
840
|
-
"ctx.job.name": jobName,
|
|
841
|
-
"ctx.job.queue": queueName,
|
|
842
|
-
"ctx.job.scheduledAt": params2.runAt?.toISOString?.() ?? null,
|
|
843
|
-
"ctx.job.cron": params2.cron ?? null
|
|
844
|
-
}
|
|
845
|
-
);
|
|
846
|
-
return jobId;
|
|
847
|
-
},
|
|
848
|
-
get(id) {
|
|
849
|
-
return {
|
|
850
|
-
retrieve: () => internal.adapter.getJob(id, queueName),
|
|
851
|
-
retry: () => internal.adapter.retryJob(id, queueName),
|
|
852
|
-
remove: () => internal.adapter.removeJob(id, queueName),
|
|
853
|
-
promote: () => internal.adapter.promoteJob(id, queueName),
|
|
854
|
-
move: (state, reason) => {
|
|
855
|
-
if (state !== "failed") return Promise.resolve();
|
|
856
|
-
return internal.adapter.moveJobToFailed(id, reason, queueName);
|
|
857
|
-
},
|
|
858
|
-
state: () => internal.adapter.getJobState(id, queueName),
|
|
859
|
-
progress: () => internal.adapter.getJobProgress(id, queueName),
|
|
860
|
-
logs: () => internal.adapter.getJobLogs(id, queueName)
|
|
861
|
-
};
|
|
862
|
-
},
|
|
863
|
-
many(ids) {
|
|
864
|
-
return {
|
|
865
|
-
retry: () => internal.adapter.retryManyJobs(ids, queueName),
|
|
866
|
-
remove: () => internal.adapter.removeManyJobs(ids, queueName)
|
|
867
|
-
};
|
|
868
|
-
},
|
|
869
|
-
pause: () => internal.adapter.pauseJobType(queueName, jobName),
|
|
870
|
-
resume: () => internal.adapter.resumeJobType(queueName, jobName),
|
|
871
|
-
async subscribe(handler) {
|
|
872
|
-
const channel = boundScope ? IgniterJobsPrefix.buildEventsChannel({
|
|
873
|
-
service: internal.config.service,
|
|
874
|
-
environment: internal.config.environment,
|
|
875
|
-
scope: { type: boundScope.type, id: boundScope.id }
|
|
876
|
-
}) : IgniterJobsPrefix.buildEventsChannel({
|
|
877
|
-
service: internal.config.service,
|
|
878
|
-
environment: internal.config.environment
|
|
879
|
-
});
|
|
880
|
-
return internal.adapter.subscribeEvent(channel, async (event) => {
|
|
881
|
-
const typed = event;
|
|
882
|
-
if (typeof typed?.type === "string" && typed.type.startsWith(`${queueName}:${jobName}:`)) {
|
|
883
|
-
await handler(typed);
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// src/builders/igniter-jobs.builder.ts
|
|
891
|
-
var IgniterJobsBuilder = class _IgniterJobsBuilder {
|
|
892
|
-
constructor(state) {
|
|
893
|
-
this.state = {
|
|
894
|
-
queues: state?.queues ?? {},
|
|
895
|
-
...state
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Creates the initial builder with no configuration.
|
|
900
|
-
*/
|
|
901
|
-
static create() {
|
|
902
|
-
return new _IgniterJobsBuilder({ queues: {} });
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Returns a new builder with updated state while preserving generics.
|
|
906
|
-
*/
|
|
907
|
-
clone(patch) {
|
|
908
|
-
return new _IgniterJobsBuilder({
|
|
909
|
-
...this.state,
|
|
910
|
-
...patch,
|
|
911
|
-
queues: patch.queues ?? this.state.queues,
|
|
912
|
-
scope: patch.scope ?? this.state.scope
|
|
913
975
|
});
|
|
914
976
|
}
|
|
915
977
|
/**
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
* @param adapter - Backend adapter implementation (BullMQ, memory, etc.).
|
|
919
|
-
*/
|
|
920
|
-
withAdapter(adapter) {
|
|
921
|
-
return this.clone({ adapter });
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Sets the service identifier for telemetry and metrics.
|
|
925
|
-
*
|
|
926
|
-
* @param service - Service name (e.g., "my-api").
|
|
927
|
-
*/
|
|
928
|
-
withService(service) {
|
|
929
|
-
return this.clone({ service });
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* Sets the environment name (e.g., development, staging, production).
|
|
933
|
-
*/
|
|
934
|
-
withEnvironment(environment) {
|
|
935
|
-
return this.clone({ environment });
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Provides a context factory used when executing jobs.
|
|
939
|
-
*/
|
|
940
|
-
withContext(factory) {
|
|
941
|
-
return this.clone({ contextFactory: factory });
|
|
942
|
-
}
|
|
943
|
-
/**
|
|
944
|
-
* Adds a scope definition (single scope supported).
|
|
945
|
-
*/
|
|
946
|
-
addScope(name, options) {
|
|
947
|
-
if (this.state.scope) {
|
|
948
|
-
throw new IgniterJobsError({
|
|
949
|
-
code: "JOBS_SCOPE_ALREADY_DEFINED",
|
|
950
|
-
message: "Only one scope can be defined for IgniterJobs."
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
return this.clone({ scope: { name, options } });
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Registers a queue definition on the builder.
|
|
957
|
-
*/
|
|
958
|
-
addQueue(queue) {
|
|
959
|
-
if (this.state.queues[queue.name]) {
|
|
960
|
-
throw new IgniterJobsError({
|
|
961
|
-
code: "JOBS_QUEUE_DUPLICATE",
|
|
962
|
-
message: `Queue "${queue.name}" is already registered.`
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
const nextQueues = {
|
|
966
|
-
...this.state.queues,
|
|
967
|
-
[queue.name]: queue
|
|
968
|
-
};
|
|
969
|
-
return this.clone({ queues: nextQueues });
|
|
970
|
-
}
|
|
971
|
-
/**
|
|
972
|
-
* Applies default job options to all queues.
|
|
973
|
-
*/
|
|
974
|
-
withQueueDefaults(defaults) {
|
|
975
|
-
return this.clone({ queueDefaults: defaults });
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* Applies default worker options.
|
|
979
|
-
*/
|
|
980
|
-
withWorkerDefaults(defaults) {
|
|
981
|
-
return this.clone({ workerDefaults: defaults });
|
|
982
|
-
}
|
|
983
|
-
/**
|
|
984
|
-
* Configures automatic worker startup.
|
|
985
|
-
*/
|
|
986
|
-
withAutoStartWorker(config) {
|
|
987
|
-
return this.clone({ autoStartWorker: config });
|
|
988
|
-
}
|
|
989
|
-
/**
|
|
990
|
-
* Attaches telemetry support.
|
|
991
|
-
*/
|
|
992
|
-
withTelemetry(telemetry) {
|
|
993
|
-
return this.clone({ telemetry });
|
|
994
|
-
}
|
|
995
|
-
/**
|
|
996
|
-
* Attaches a custom logger.
|
|
997
|
-
*/
|
|
998
|
-
withLogger(logger) {
|
|
999
|
-
return this.clone({ logger });
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Finalizes the configuration and returns the runtime instance.
|
|
978
|
+
* Emits a telemetry event if telemetry is configured.
|
|
979
|
+
* @internal
|
|
1003
980
|
*/
|
|
1004
|
-
|
|
1005
|
-
if (!this.
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
1011
|
-
if (!this.state.service) {
|
|
1012
|
-
throw new IgniterJobsError({
|
|
1013
|
-
code: "JOBS_SERVICE_REQUIRED",
|
|
1014
|
-
message: "Service name is required. Call withService() before build()."
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
if (!this.state.environment) {
|
|
1018
|
-
throw new IgniterJobsError({
|
|
1019
|
-
code: "JOBS_CONFIGURATION_INVALID",
|
|
1020
|
-
message: "Environment is required. Call withEnvironment() before build()."
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
if (!this.state.contextFactory) {
|
|
1024
|
-
throw new IgniterJobsError({
|
|
1025
|
-
code: "JOBS_CONTEXT_REQUIRED",
|
|
1026
|
-
message: "Context factory is required. Call withContext() before build()."
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
const config = {
|
|
1030
|
-
adapter: this.state.adapter,
|
|
1031
|
-
service: this.state.service,
|
|
1032
|
-
environment: this.state.environment,
|
|
1033
|
-
contextFactory: this.state.contextFactory,
|
|
1034
|
-
queues: this.state.queues,
|
|
1035
|
-
scopeDefinition: this.state.scope ? {
|
|
1036
|
-
[this.state.scope.name]: this.state.scope.options ?? {}
|
|
1037
|
-
} : void 0,
|
|
1038
|
-
queueDefaults: this.state.queueDefaults,
|
|
1039
|
-
workerDefaults: this.state.workerDefaults,
|
|
1040
|
-
autoStartWorker: this.state.autoStartWorker,
|
|
1041
|
-
logger: this.state.logger,
|
|
1042
|
-
telemetry: this.state.telemetry
|
|
1043
|
-
};
|
|
1044
|
-
return IgniterJobs.fromConfig(config);
|
|
981
|
+
emitTelemetry(eventName, attributes, level = "info") {
|
|
982
|
+
if (!this.telemetry) return;
|
|
983
|
+
this.telemetry.emit(eventName, {
|
|
984
|
+
attributes,
|
|
985
|
+
level
|
|
986
|
+
});
|
|
1045
987
|
}
|
|
1046
988
|
};
|
|
1047
989
|
|
|
1048
|
-
// src/builders/
|
|
990
|
+
// src/builders/queue.builder.ts
|
|
1049
991
|
var IgniterQueueBuilder = class _IgniterQueueBuilder {
|
|
1050
992
|
constructor(state) {
|
|
1051
993
|
this.state = state;
|
|
@@ -1066,21 +1008,6 @@ var IgniterQueueBuilder = class _IgniterQueueBuilder {
|
|
|
1066
1008
|
crons: {}
|
|
1067
1009
|
});
|
|
1068
1010
|
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Re-types this builder with the application context type.
|
|
1071
|
-
*
|
|
1072
|
-
* This is a type-level helper; it does not mutate runtime state.
|
|
1073
|
-
*
|
|
1074
|
-
* @example
|
|
1075
|
-
* ```typescript
|
|
1076
|
-
* const queue = IgniterQueue.create('email')
|
|
1077
|
-
* .withContext<AppContext>()
|
|
1078
|
-
* .addJob('send', { handler: async ({ context }) => context.mailer.send() })
|
|
1079
|
-
* ```
|
|
1080
|
-
*/
|
|
1081
|
-
withContext() {
|
|
1082
|
-
return this;
|
|
1083
|
-
}
|
|
1084
1011
|
clone(patch) {
|
|
1085
1012
|
return new _IgniterQueueBuilder({
|
|
1086
1013
|
...this.state,
|
|
@@ -1195,7 +1122,7 @@ var IgniterQueueBuilder = class _IgniterQueueBuilder {
|
|
|
1195
1122
|
}
|
|
1196
1123
|
};
|
|
1197
1124
|
|
|
1198
|
-
// src/core/
|
|
1125
|
+
// src/core/queue.ts
|
|
1199
1126
|
var IgniterQueue = class {
|
|
1200
1127
|
/**
|
|
1201
1128
|
* Creates a new queue builder for the given name.
|
|
@@ -1212,54 +1139,253 @@ var IgniterQueue = class {
|
|
|
1212
1139
|
return IgniterQueueBuilder.create(name);
|
|
1213
1140
|
}
|
|
1214
1141
|
};
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
this.coreAdapter = null;
|
|
1223
|
-
this.coreExecutor = null;
|
|
1224
|
-
this.executorDirty = true;
|
|
1225
|
-
this.jobsByQueue = /* @__PURE__ */ new Map();
|
|
1226
|
-
this.cronsByQueue = /* @__PURE__ */ new Map();
|
|
1227
|
-
this.redis = options.redis;
|
|
1228
|
-
this.publisher = this.redis;
|
|
1229
|
-
this.subscriber = this.redis.duplicate();
|
|
1230
|
-
this.client = { redis: this.redis };
|
|
1231
|
-
this.subscriber.on("message", (channel, message) => {
|
|
1232
|
-
const set = this.subscribers.get(channel);
|
|
1233
|
-
if (!set || set.size === 0) return;
|
|
1234
|
-
let payload = message;
|
|
1235
|
-
try {
|
|
1236
|
-
payload = JSON.parse(message);
|
|
1237
|
-
} catch {
|
|
1238
|
-
}
|
|
1239
|
-
for (const handler of set) handler(payload);
|
|
1240
|
-
});
|
|
1241
|
-
this.queues = {
|
|
1242
|
-
list: async () => this.listQueues(),
|
|
1243
|
-
get: async (name) => this.getQueueInfo(name),
|
|
1244
|
-
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
1245
|
-
getJobs: async (name, filter) => {
|
|
1246
|
-
const full = this.toCoreQueueName(name);
|
|
1247
|
-
return this.core().queues.getJobs(full, filter);
|
|
1248
|
-
},
|
|
1249
|
-
pause: async (name) => this.pauseQueue(name),
|
|
1250
|
-
resume: async (name) => this.resumeQueue(name),
|
|
1251
|
-
isPaused: async (name) => {
|
|
1252
|
-
const full = this.toCoreQueueName(name);
|
|
1253
|
-
return this.core().queues.isPaused(full);
|
|
1254
|
-
},
|
|
1255
|
-
drain: async (name) => this.drainQueue(name),
|
|
1256
|
-
clean: async (name, options2) => this.cleanQueue(name, options2),
|
|
1257
|
-
obliterate: async (name, options2) => this.obliterateQueue(name, options2)
|
|
1142
|
+
|
|
1143
|
+
// src/builders/main.builder.ts
|
|
1144
|
+
var IgniterJobsBuilder = class _IgniterJobsBuilder {
|
|
1145
|
+
constructor(state) {
|
|
1146
|
+
this.state = {
|
|
1147
|
+
queues: state?.queues ?? {},
|
|
1148
|
+
...state
|
|
1258
1149
|
};
|
|
1259
1150
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1151
|
+
/**
|
|
1152
|
+
* Creates the initial builder with no configuration.
|
|
1153
|
+
*
|
|
1154
|
+
* Context type is inferred from `withContext()` - no explicit generic needed.
|
|
1155
|
+
*
|
|
1156
|
+
* @example
|
|
1157
|
+
* ```typescript
|
|
1158
|
+
* // Context is inferred from the factory return type
|
|
1159
|
+
* const jobs = IgniterJobs.create()
|
|
1160
|
+
* .withContext(async () => ({ db: prisma, cache: redis }))
|
|
1161
|
+
* // TContext is now { db: PrismaClient, cache: Redis }
|
|
1162
|
+
* .build()
|
|
1163
|
+
* ```
|
|
1164
|
+
*/
|
|
1165
|
+
static create() {
|
|
1166
|
+
return new _IgniterJobsBuilder({
|
|
1167
|
+
queues: {}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Returns a new builder with updated state while preserving generics.
|
|
1172
|
+
*/
|
|
1173
|
+
clone(patch) {
|
|
1174
|
+
return new _IgniterJobsBuilder({
|
|
1175
|
+
...this.state,
|
|
1176
|
+
...patch,
|
|
1177
|
+
queues: patch.queues ?? this.state.queues,
|
|
1178
|
+
scope: patch.scope ?? this.state.scope
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Attaches the jobs adapter.
|
|
1183
|
+
*
|
|
1184
|
+
* @param adapter - Backend adapter implementation (BullMQ, memory, etc.).
|
|
1185
|
+
*/
|
|
1186
|
+
withAdapter(adapter) {
|
|
1187
|
+
return this.clone({ adapter });
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Sets the service identifier for telemetry and metrics.
|
|
1191
|
+
*
|
|
1192
|
+
* @param service - Service name (e.g., "my-api").
|
|
1193
|
+
*/
|
|
1194
|
+
withService(service) {
|
|
1195
|
+
return this.clone({ service });
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Sets the environment name (e.g., development, staging, production).
|
|
1199
|
+
*/
|
|
1200
|
+
withEnvironment(environment) {
|
|
1201
|
+
return this.clone({ environment });
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Provides a context factory used when executing jobs.
|
|
1205
|
+
*
|
|
1206
|
+
* The context type is inferred from the factory return type, eliminating
|
|
1207
|
+
* the need for explicit generics on `IgniterJobs.create()`.
|
|
1208
|
+
*
|
|
1209
|
+
* **Note:** This must be called before adding any queues, as the context
|
|
1210
|
+
* type affects queue type compatibility.
|
|
1211
|
+
*
|
|
1212
|
+
* @param factory - Function that returns the context (sync or async)
|
|
1213
|
+
* @returns A new builder with the inferred context type
|
|
1214
|
+
*
|
|
1215
|
+
* @example
|
|
1216
|
+
* ```typescript
|
|
1217
|
+
* // Context type is inferred as { db: PrismaClient, cache: Redis }
|
|
1218
|
+
* const jobs = IgniterJobs.create()
|
|
1219
|
+
* .withContext(async () => ({
|
|
1220
|
+
* db: new PrismaClient(),
|
|
1221
|
+
* cache: new Redis(),
|
|
1222
|
+
* }))
|
|
1223
|
+
* .addQueue(emailQueue)
|
|
1224
|
+
* .build()
|
|
1225
|
+
* ```
|
|
1226
|
+
*/
|
|
1227
|
+
withContext(factory) {
|
|
1228
|
+
return new _IgniterJobsBuilder({
|
|
1229
|
+
...this.state,
|
|
1230
|
+
contextFactory: factory,
|
|
1231
|
+
queues: {}
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Adds a scope definition (single scope supported).
|
|
1236
|
+
*/
|
|
1237
|
+
addScope(name, options) {
|
|
1238
|
+
if (this.state.scope) {
|
|
1239
|
+
throw new IgniterJobsError({
|
|
1240
|
+
code: "JOBS_SCOPE_ALREADY_DEFINED",
|
|
1241
|
+
message: "Only one scope can be defined for IgniterJobs."
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
return this.clone({ scope: { name, options } });
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Registers a queue definition on the builder.
|
|
1248
|
+
*/
|
|
1249
|
+
addQueue(queue) {
|
|
1250
|
+
if (this.state.queues[queue.name]) {
|
|
1251
|
+
throw new IgniterJobsError({
|
|
1252
|
+
code: "JOBS_QUEUE_DUPLICATE",
|
|
1253
|
+
message: `Queue "${queue.name}" is already registered.`
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
const nextQueues = {
|
|
1257
|
+
...this.state.queues,
|
|
1258
|
+
[queue.name]: queue
|
|
1259
|
+
};
|
|
1260
|
+
return this.clone({ queues: nextQueues });
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Applies default job options to all queues.
|
|
1264
|
+
*/
|
|
1265
|
+
withQueueDefaults(defaults) {
|
|
1266
|
+
return this.clone({ queueDefaults: defaults });
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Applies default worker options.
|
|
1270
|
+
*/
|
|
1271
|
+
withWorkerDefaults(defaults) {
|
|
1272
|
+
return this.clone({ workerDefaults: defaults });
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Configures automatic worker startup.
|
|
1276
|
+
*/
|
|
1277
|
+
withAutoStartWorker(config) {
|
|
1278
|
+
return this.clone({ autoStartWorker: config });
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Attaches telemetry support.
|
|
1282
|
+
*/
|
|
1283
|
+
withTelemetry(telemetry) {
|
|
1284
|
+
return this.clone({ telemetry });
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Attaches a custom logger.
|
|
1288
|
+
*/
|
|
1289
|
+
withLogger(logger) {
|
|
1290
|
+
return this.clone({ logger });
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Finalizes the configuration and returns the runtime instance.
|
|
1294
|
+
*/
|
|
1295
|
+
build() {
|
|
1296
|
+
if (!this.state.adapter) {
|
|
1297
|
+
throw new IgniterJobsError({
|
|
1298
|
+
code: "JOBS_ADAPTER_REQUIRED",
|
|
1299
|
+
message: "Jobs adapter is required. Call withAdapter() before build()."
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
if (!this.state.service) {
|
|
1303
|
+
throw new IgniterJobsError({
|
|
1304
|
+
code: "JOBS_SERVICE_REQUIRED",
|
|
1305
|
+
message: "Service name is required. Call withService() before build()."
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
if (!this.state.environment) {
|
|
1309
|
+
throw new IgniterJobsError({
|
|
1310
|
+
code: "JOBS_CONFIGURATION_INVALID",
|
|
1311
|
+
message: "Environment is required. Call withEnvironment() before build()."
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
if (!this.state.contextFactory) {
|
|
1315
|
+
throw new IgniterJobsError({
|
|
1316
|
+
code: "JOBS_CONTEXT_REQUIRED",
|
|
1317
|
+
message: "Context factory is required. Call withContext() before build()."
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
const config = {
|
|
1321
|
+
adapter: this.state.adapter,
|
|
1322
|
+
service: this.state.service,
|
|
1323
|
+
environment: this.state.environment,
|
|
1324
|
+
contextFactory: this.state.contextFactory,
|
|
1325
|
+
queues: this.state.queues,
|
|
1326
|
+
scopeDefinition: this.state.scope ? {
|
|
1327
|
+
[this.state.scope.name]: this.state.scope.options ?? {}
|
|
1328
|
+
} : void 0,
|
|
1329
|
+
queueDefaults: this.state.queueDefaults,
|
|
1330
|
+
workerDefaults: this.state.workerDefaults,
|
|
1331
|
+
autoStartWorker: this.state.autoStartWorker,
|
|
1332
|
+
logger: this.state.logger,
|
|
1333
|
+
telemetry: this.state.telemetry
|
|
1334
|
+
};
|
|
1335
|
+
return new IgniterJobsManager(config).toRuntime();
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var IgniterJobs = {
|
|
1339
|
+
create: IgniterJobsBuilder.create
|
|
1340
|
+
};
|
|
1341
|
+
function toDateArray(values) {
|
|
1342
|
+
if (!values) return void 0;
|
|
1343
|
+
return values.map((v) => v instanceof Date ? v : new Date(v));
|
|
1344
|
+
}
|
|
1345
|
+
var IgniterJobsBullMQAdapter = class _IgniterJobsBullMQAdapter {
|
|
1346
|
+
constructor(options) {
|
|
1347
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
1348
|
+
this.coreAdapter = null;
|
|
1349
|
+
this.coreExecutor = null;
|
|
1350
|
+
this.executorDirty = true;
|
|
1351
|
+
this.jobsByQueue = /* @__PURE__ */ new Map();
|
|
1352
|
+
this.cronsByQueue = /* @__PURE__ */ new Map();
|
|
1353
|
+
this.redis = options.redis;
|
|
1354
|
+
this.publisher = this.redis;
|
|
1355
|
+
this.subscriber = this.redis.duplicate();
|
|
1356
|
+
this.client = { redis: this.redis };
|
|
1357
|
+
this.subscriber.on("message", (channel, message) => {
|
|
1358
|
+
const set = this.subscribers.get(channel);
|
|
1359
|
+
if (!set || set.size === 0) return;
|
|
1360
|
+
let payload = message;
|
|
1361
|
+
try {
|
|
1362
|
+
payload = JSON.parse(message);
|
|
1363
|
+
} catch {
|
|
1364
|
+
}
|
|
1365
|
+
for (const handler of set) handler(payload);
|
|
1366
|
+
});
|
|
1367
|
+
this.queues = {
|
|
1368
|
+
list: async () => this.listQueues(),
|
|
1369
|
+
get: async (name) => this.getQueueInfo(name),
|
|
1370
|
+
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
1371
|
+
getJobs: async (name, filter) => {
|
|
1372
|
+
const full = this.toCoreQueueName(name);
|
|
1373
|
+
return this.core().queues.getJobs(full, filter);
|
|
1374
|
+
},
|
|
1375
|
+
pause: async (name) => this.pauseQueue(name),
|
|
1376
|
+
resume: async (name) => this.resumeQueue(name),
|
|
1377
|
+
isPaused: async (name) => {
|
|
1378
|
+
const full = this.toCoreQueueName(name);
|
|
1379
|
+
return this.core().queues.isPaused(full);
|
|
1380
|
+
},
|
|
1381
|
+
drain: async (name) => this.drainQueue(name),
|
|
1382
|
+
clean: async (name, options2) => this.cleanQueue(name, options2),
|
|
1383
|
+
obliterate: async (name, options2) => this.obliterateQueue(name, options2)
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
static create(options) {
|
|
1387
|
+
return new _IgniterJobsBullMQAdapter(options);
|
|
1388
|
+
}
|
|
1263
1389
|
registerJob(queueName, jobName, definition) {
|
|
1264
1390
|
const map = this.jobsByQueue.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
1265
1391
|
if (map.has(jobName)) {
|
|
@@ -1752,38 +1878,761 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1752
1878
|
async dispatch(params) {
|
|
1753
1879
|
const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
|
|
1754
1880
|
const maxAttempts = params.attempts ?? 1;
|
|
1755
|
-
const metadata = params.metadata ?? {};
|
|
1756
|
-
const job = {
|
|
1757
|
-
id: jobId,
|
|
1758
|
-
name: params.jobName,
|
|
1759
|
-
queue: params.queue,
|
|
1760
|
-
input: params.input,
|
|
1761
|
-
status: this.pausedQueues.has(params.queue) ? "paused" : params.delay && params.delay > 0 ? "delayed" : "waiting",
|
|
1762
|
-
progress: 0,
|
|
1763
|
-
attemptsMade: 0,
|
|
1881
|
+
const metadata = params.metadata ?? {};
|
|
1882
|
+
const job = {
|
|
1883
|
+
id: jobId,
|
|
1884
|
+
name: params.jobName,
|
|
1885
|
+
queue: params.queue,
|
|
1886
|
+
input: params.input,
|
|
1887
|
+
status: this.pausedQueues.has(params.queue) ? "paused" : params.delay && params.delay > 0 ? "delayed" : "waiting",
|
|
1888
|
+
progress: 0,
|
|
1889
|
+
attemptsMade: 0,
|
|
1890
|
+
maxAttempts,
|
|
1891
|
+
priority: params.priority ?? 0,
|
|
1892
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1893
|
+
metadata,
|
|
1894
|
+
scope: params.scope,
|
|
1895
|
+
logs: []
|
|
1896
|
+
};
|
|
1897
|
+
this.jobsById.set(jobId, job);
|
|
1898
|
+
const queueList = this.jobsByQueue.get(params.queue) ?? [];
|
|
1899
|
+
queueList.push(jobId);
|
|
1900
|
+
this.jobsByQueue.set(params.queue, queueList);
|
|
1901
|
+
if (params.delay && params.delay > 0) {
|
|
1902
|
+
setTimeout(() => {
|
|
1903
|
+
const stored = this.jobsById.get(jobId);
|
|
1904
|
+
if (!stored) return;
|
|
1905
|
+
if (!this.pausedQueues.has(params.queue)) stored.status = "waiting";
|
|
1906
|
+
void this.kickWorkers(params.queue);
|
|
1907
|
+
}, params.delay);
|
|
1908
|
+
return jobId;
|
|
1909
|
+
}
|
|
1910
|
+
void this.kickWorkers(params.queue);
|
|
1911
|
+
return jobId;
|
|
1912
|
+
}
|
|
1913
|
+
async schedule(params) {
|
|
1914
|
+
if (params.at) {
|
|
1915
|
+
const delay = params.at.getTime() - Date.now();
|
|
1916
|
+
if (delay <= 0) {
|
|
1917
|
+
throw new IgniterJobsError({
|
|
1918
|
+
code: "JOBS_INVALID_SCHEDULE",
|
|
1919
|
+
message: "Scheduled time must be in the future."
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
return this.dispatch({ ...params, delay });
|
|
1923
|
+
}
|
|
1924
|
+
if (params.cron || params.every) {
|
|
1925
|
+
return this.dispatch({ ...params, delay: params.delay ?? 0 });
|
|
1926
|
+
}
|
|
1927
|
+
return this.dispatch(params);
|
|
1928
|
+
}
|
|
1929
|
+
async getJob(jobId, queue) {
|
|
1930
|
+
const job = this.jobsById.get(jobId);
|
|
1931
|
+
if (!job) return null;
|
|
1932
|
+
if (queue && job.queue !== queue) return null;
|
|
1933
|
+
return this.toSearchResult(job);
|
|
1934
|
+
}
|
|
1935
|
+
async getJobState(jobId, queue) {
|
|
1936
|
+
const job = this.jobsById.get(jobId);
|
|
1937
|
+
if (!job) return null;
|
|
1938
|
+
if (queue && job.queue !== queue) return null;
|
|
1939
|
+
return job.status;
|
|
1940
|
+
}
|
|
1941
|
+
async getJobLogs(jobId, queue) {
|
|
1942
|
+
const job = this.jobsById.get(jobId);
|
|
1943
|
+
if (!job) return [];
|
|
1944
|
+
if (queue && job.queue !== queue) return [];
|
|
1945
|
+
return job.logs;
|
|
1946
|
+
}
|
|
1947
|
+
async getJobProgress(jobId, queue) {
|
|
1948
|
+
const job = this.jobsById.get(jobId);
|
|
1949
|
+
if (!job) return 0;
|
|
1950
|
+
if (queue && job.queue !== queue) return 0;
|
|
1951
|
+
return job.progress;
|
|
1952
|
+
}
|
|
1953
|
+
async retryJob(jobId, queue) {
|
|
1954
|
+
const job = this.jobsById.get(jobId);
|
|
1955
|
+
if (!job) {
|
|
1956
|
+
throw new IgniterJobsError({
|
|
1957
|
+
code: "JOBS_NOT_FOUND",
|
|
1958
|
+
message: `Job "${jobId}" not found.`
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
if (queue && job.queue !== queue) {
|
|
1962
|
+
throw new IgniterJobsError({
|
|
1963
|
+
code: "JOBS_NOT_FOUND",
|
|
1964
|
+
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
job.status = "waiting";
|
|
1968
|
+
job.error = void 0;
|
|
1969
|
+
job.completedAt = void 0;
|
|
1970
|
+
job.progress = 0;
|
|
1971
|
+
void this.kickWorkers(job.queue);
|
|
1972
|
+
}
|
|
1973
|
+
async removeJob(jobId, queue) {
|
|
1974
|
+
const job = this.jobsById.get(jobId);
|
|
1975
|
+
if (!job) return;
|
|
1976
|
+
if (queue && job.queue !== queue) return;
|
|
1977
|
+
this.jobsById.delete(jobId);
|
|
1978
|
+
const list = this.jobsByQueue.get(job.queue);
|
|
1979
|
+
if (list)
|
|
1980
|
+
this.jobsByQueue.set(
|
|
1981
|
+
job.queue,
|
|
1982
|
+
list.filter((id) => id !== jobId)
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
async promoteJob(jobId, queue) {
|
|
1986
|
+
const job = this.jobsById.get(jobId);
|
|
1987
|
+
if (!job) {
|
|
1988
|
+
throw new IgniterJobsError({
|
|
1989
|
+
code: "JOBS_NOT_FOUND",
|
|
1990
|
+
message: `Job "${jobId}" not found.`
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
if (queue && job.queue !== queue) {
|
|
1994
|
+
throw new IgniterJobsError({
|
|
1995
|
+
code: "JOBS_NOT_FOUND",
|
|
1996
|
+
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
if (job.status === "delayed" || job.status === "paused") {
|
|
2000
|
+
job.status = this.pausedQueues.has(job.queue) ? "paused" : "waiting";
|
|
2001
|
+
void this.kickWorkers(job.queue);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async moveJobToFailed(jobId, reason, queue) {
|
|
2005
|
+
const job = this.jobsById.get(jobId);
|
|
2006
|
+
if (!job) {
|
|
2007
|
+
throw new IgniterJobsError({
|
|
2008
|
+
code: "JOBS_NOT_FOUND",
|
|
2009
|
+
message: `Job "${jobId}" not found.`
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
if (queue && job.queue !== queue) {
|
|
2013
|
+
throw new IgniterJobsError({
|
|
2014
|
+
code: "JOBS_NOT_FOUND",
|
|
2015
|
+
message: `Job "${jobId}" not found in queue "${queue}".`
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
job.status = "failed";
|
|
2019
|
+
job.error = reason;
|
|
2020
|
+
job.completedAt = /* @__PURE__ */ new Date();
|
|
2021
|
+
}
|
|
2022
|
+
async retryManyJobs(jobIds, queue) {
|
|
2023
|
+
await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
|
|
2024
|
+
}
|
|
2025
|
+
async removeManyJobs(jobIds, queue) {
|
|
2026
|
+
await Promise.all(jobIds.map((id) => this.removeJob(id, queue)));
|
|
2027
|
+
}
|
|
2028
|
+
async getQueueInfo(queue) {
|
|
2029
|
+
const counts = await this.getQueueJobCounts(queue);
|
|
2030
|
+
return {
|
|
2031
|
+
name: queue,
|
|
2032
|
+
isPaused: this.pausedQueues.has(queue),
|
|
2033
|
+
jobCounts: counts
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
async getQueueJobCounts(queue) {
|
|
2037
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2038
|
+
const counts = {
|
|
2039
|
+
waiting: 0,
|
|
2040
|
+
active: 0,
|
|
2041
|
+
completed: 0,
|
|
2042
|
+
failed: 0,
|
|
2043
|
+
delayed: 0,
|
|
2044
|
+
paused: 0
|
|
2045
|
+
};
|
|
2046
|
+
for (const id of jobIds) {
|
|
2047
|
+
const job = this.jobsById.get(id);
|
|
2048
|
+
if (!job) continue;
|
|
2049
|
+
if (job.status in counts) {
|
|
2050
|
+
counts[job.status]++;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return counts;
|
|
2054
|
+
}
|
|
2055
|
+
async listQueues() {
|
|
2056
|
+
const queues = Array.from(
|
|
2057
|
+
/* @__PURE__ */ new Set([
|
|
2058
|
+
...this.jobsByQueue.keys(),
|
|
2059
|
+
...this.registeredJobs.keys(),
|
|
2060
|
+
...this.registeredCrons.keys()
|
|
2061
|
+
])
|
|
2062
|
+
);
|
|
2063
|
+
const result = [];
|
|
2064
|
+
for (const q of queues) {
|
|
2065
|
+
result.push(await this.getQueueInfo(q));
|
|
2066
|
+
}
|
|
2067
|
+
return result;
|
|
2068
|
+
}
|
|
2069
|
+
async pauseQueue(queue) {
|
|
2070
|
+
this.pausedQueues.add(queue);
|
|
2071
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2072
|
+
for (const id of jobIds) {
|
|
2073
|
+
const job = this.jobsById.get(id);
|
|
2074
|
+
if (!job) continue;
|
|
2075
|
+
if (job.status === "waiting") job.status = "paused";
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
async resumeQueue(queue) {
|
|
2079
|
+
this.pausedQueues.delete(queue);
|
|
2080
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2081
|
+
for (const id of jobIds) {
|
|
2082
|
+
const job = this.jobsById.get(id);
|
|
2083
|
+
if (!job) continue;
|
|
2084
|
+
if (job.status === "paused") job.status = "waiting";
|
|
2085
|
+
}
|
|
2086
|
+
void this.kickWorkers(queue);
|
|
2087
|
+
}
|
|
2088
|
+
async drainQueue(queue) {
|
|
2089
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2090
|
+
let removed = 0;
|
|
2091
|
+
for (const id of jobIds) {
|
|
2092
|
+
const job = this.jobsById.get(id);
|
|
2093
|
+
if (!job) continue;
|
|
2094
|
+
if (job.status === "waiting" || job.status === "paused") {
|
|
2095
|
+
this.jobsById.delete(id);
|
|
2096
|
+
removed++;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
this.jobsByQueue.set(
|
|
2100
|
+
queue,
|
|
2101
|
+
jobIds.filter((id) => this.jobsById.has(id))
|
|
2102
|
+
);
|
|
2103
|
+
return removed;
|
|
2104
|
+
}
|
|
2105
|
+
async cleanQueue(queue, options) {
|
|
2106
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
2107
|
+
const olderThan = options.olderThan ?? 0;
|
|
2108
|
+
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
2109
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2110
|
+
const now = Date.now();
|
|
2111
|
+
let cleaned = 0;
|
|
2112
|
+
for (const id of [...jobIds]) {
|
|
2113
|
+
if (cleaned >= limit) break;
|
|
2114
|
+
const job = this.jobsById.get(id);
|
|
2115
|
+
if (!job) continue;
|
|
2116
|
+
if (!statuses.includes(job.status)) continue;
|
|
2117
|
+
const ageMs = now - job.createdAt.getTime();
|
|
2118
|
+
if (ageMs < olderThan) continue;
|
|
2119
|
+
this.jobsById.delete(id);
|
|
2120
|
+
cleaned++;
|
|
2121
|
+
}
|
|
2122
|
+
this.jobsByQueue.set(
|
|
2123
|
+
queue,
|
|
2124
|
+
jobIds.filter((id) => this.jobsById.has(id))
|
|
2125
|
+
);
|
|
2126
|
+
return cleaned;
|
|
2127
|
+
}
|
|
2128
|
+
async obliterateQueue(queue, options) {
|
|
2129
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2130
|
+
for (const id of jobIds) this.jobsById.delete(id);
|
|
2131
|
+
this.jobsByQueue.delete(queue);
|
|
2132
|
+
this.registeredJobs.delete(queue);
|
|
2133
|
+
this.registeredCrons.delete(queue);
|
|
2134
|
+
this.pausedQueues.delete(queue);
|
|
2135
|
+
}
|
|
2136
|
+
async retryAllInQueue(queue) {
|
|
2137
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2138
|
+
let retried = 0;
|
|
2139
|
+
for (const id of jobIds) {
|
|
2140
|
+
const job = this.jobsById.get(id);
|
|
2141
|
+
if (!job) continue;
|
|
2142
|
+
if (job.status === "failed") {
|
|
2143
|
+
await this.retryJob(id, queue);
|
|
2144
|
+
retried++;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return retried;
|
|
2148
|
+
}
|
|
2149
|
+
async pauseJobType(queue, jobName) {
|
|
2150
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2151
|
+
for (const id of jobIds) {
|
|
2152
|
+
const job = this.jobsById.get(id);
|
|
2153
|
+
if (!job) continue;
|
|
2154
|
+
if (job.name === jobName && job.status === "waiting")
|
|
2155
|
+
job.status = "paused";
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
async resumeJobType(queue, jobName) {
|
|
2159
|
+
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
2160
|
+
for (const id of jobIds) {
|
|
2161
|
+
const job = this.jobsById.get(id);
|
|
2162
|
+
if (!job) continue;
|
|
2163
|
+
if (job.name === jobName && job.status === "paused")
|
|
2164
|
+
job.status = "waiting";
|
|
2165
|
+
}
|
|
2166
|
+
void this.kickWorkers(queue);
|
|
2167
|
+
}
|
|
2168
|
+
async searchJobs(filter) {
|
|
2169
|
+
const queue = filter?.queue;
|
|
2170
|
+
const statuses = filter?.status;
|
|
2171
|
+
const limit = filter?.limit ?? 100;
|
|
2172
|
+
const offset = filter?.offset ?? 0;
|
|
2173
|
+
const all = Array.from(this.jobsById.values()).filter((j) => queue ? j.queue === queue : true).filter((j) => statuses ? statuses.includes(j.status) : true).sort(
|
|
2174
|
+
(a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
|
|
2175
|
+
);
|
|
2176
|
+
return all.slice(offset, offset + limit).map((j) => this.toSearchResult(j));
|
|
2177
|
+
}
|
|
2178
|
+
async searchQueues(filter) {
|
|
2179
|
+
const name = filter?.name;
|
|
2180
|
+
const isPaused = filter?.isPaused;
|
|
2181
|
+
const all = await this.listQueues();
|
|
2182
|
+
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
2183
|
+
(q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
async searchWorkers(filter) {
|
|
2187
|
+
const queue = filter?.queue;
|
|
2188
|
+
const isRunning = filter?.isRunning;
|
|
2189
|
+
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
2190
|
+
(w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
|
|
2191
|
+
).map((w) => this.toWorkerHandle(w));
|
|
2192
|
+
}
|
|
2193
|
+
async createWorker(config) {
|
|
2194
|
+
const workerId = IgniterJobsIdGenerator.generate("worker");
|
|
2195
|
+
const state = {
|
|
2196
|
+
id: workerId,
|
|
2197
|
+
queues: config.queues ?? [],
|
|
2198
|
+
concurrency: config.concurrency ?? 1,
|
|
2199
|
+
paused: false,
|
|
2200
|
+
closed: false,
|
|
2201
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
2202
|
+
metrics: { processed: 0, failed: 0, totalDuration: 0 },
|
|
2203
|
+
handlers: config.handlers
|
|
2204
|
+
};
|
|
2205
|
+
this.workers.set(workerId, state);
|
|
2206
|
+
for (const q of state.queues) void this.kickWorkers(q);
|
|
2207
|
+
return this.toWorkerHandle(state);
|
|
2208
|
+
}
|
|
2209
|
+
getWorkers() {
|
|
2210
|
+
const out = /* @__PURE__ */ new Map();
|
|
2211
|
+
for (const [id, state] of this.workers)
|
|
2212
|
+
out.set(id, this.toWorkerHandle(state));
|
|
2213
|
+
return out;
|
|
2214
|
+
}
|
|
2215
|
+
async publishEvent(channel, payload) {
|
|
2216
|
+
const handlers = this.subscribers.get(channel);
|
|
2217
|
+
if (!handlers) return;
|
|
2218
|
+
await Promise.all(Array.from(handlers).map(async (h) => h(payload)));
|
|
2219
|
+
}
|
|
2220
|
+
async subscribeEvent(channel, handler) {
|
|
2221
|
+
const set = this.subscribers.get(channel) ?? /* @__PURE__ */ new Set();
|
|
2222
|
+
set.add(handler);
|
|
2223
|
+
this.subscribers.set(channel, set);
|
|
2224
|
+
return async () => {
|
|
2225
|
+
const current = this.subscribers.get(channel);
|
|
2226
|
+
if (!current) return;
|
|
2227
|
+
current.delete(handler);
|
|
2228
|
+
if (current.size === 0) this.subscribers.delete(channel);
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
async shutdown() {
|
|
2232
|
+
this.workers.clear();
|
|
2233
|
+
this.subscribers.clear();
|
|
2234
|
+
}
|
|
2235
|
+
toSearchResult(job) {
|
|
2236
|
+
return {
|
|
2237
|
+
id: job.id,
|
|
2238
|
+
name: job.name,
|
|
2239
|
+
queue: job.queue,
|
|
2240
|
+
status: job.status,
|
|
2241
|
+
input: job.input,
|
|
2242
|
+
result: job.result,
|
|
2243
|
+
error: job.error,
|
|
2244
|
+
progress: job.progress,
|
|
2245
|
+
attemptsMade: job.attemptsMade,
|
|
2246
|
+
priority: job.priority,
|
|
2247
|
+
createdAt: job.createdAt,
|
|
2248
|
+
startedAt: job.startedAt,
|
|
2249
|
+
completedAt: job.completedAt,
|
|
2250
|
+
metadata: job.metadata,
|
|
2251
|
+
scope: job.scope
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
toWorkerHandle(worker) {
|
|
2255
|
+
return {
|
|
2256
|
+
id: worker.id,
|
|
2257
|
+
queues: worker.queues,
|
|
2258
|
+
pause: async () => {
|
|
2259
|
+
worker.paused = true;
|
|
2260
|
+
},
|
|
2261
|
+
resume: async () => {
|
|
2262
|
+
worker.paused = false;
|
|
2263
|
+
for (const q of worker.queues) void this.kickWorkers(q);
|
|
2264
|
+
},
|
|
2265
|
+
close: async () => {
|
|
2266
|
+
worker.closed = true;
|
|
2267
|
+
},
|
|
2268
|
+
isRunning: () => !worker.closed && !worker.paused,
|
|
2269
|
+
isPaused: () => worker.paused,
|
|
2270
|
+
isClosed: () => worker.closed,
|
|
2271
|
+
getMetrics: async () => this.toWorkerMetrics(worker)
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
toWorkerMetrics(worker) {
|
|
2275
|
+
const uptime = Date.now() - worker.startedAt.getTime();
|
|
2276
|
+
const processed = worker.metrics.processed;
|
|
2277
|
+
return {
|
|
2278
|
+
processed,
|
|
2279
|
+
failed: worker.metrics.failed,
|
|
2280
|
+
avgDuration: processed > 0 ? worker.metrics.totalDuration / processed : 0,
|
|
2281
|
+
concurrency: worker.concurrency,
|
|
2282
|
+
uptime
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
async kickWorkers(queue) {
|
|
2286
|
+
if (this.pausedQueues.has(queue)) return;
|
|
2287
|
+
const relevant = Array.from(this.workers.values()).filter(
|
|
2288
|
+
(w) => !w.closed && !w.paused && (w.queues.length === 0 || w.queues.includes(queue))
|
|
2289
|
+
);
|
|
2290
|
+
if (relevant.length === 0) return;
|
|
2291
|
+
for (const w of relevant) {
|
|
2292
|
+
void this.processLoop(w, queue);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
async processLoop(worker, queue) {
|
|
2296
|
+
if (worker.closed || worker.paused) return;
|
|
2297
|
+
const concurrency = Math.max(1, worker.concurrency);
|
|
2298
|
+
const running = worker.__running;
|
|
2299
|
+
const currentRunning = running ?? 0;
|
|
2300
|
+
if (currentRunning >= concurrency) return;
|
|
2301
|
+
worker.__running = currentRunning + 1;
|
|
2302
|
+
try {
|
|
2303
|
+
const next = this.nextJob(queue);
|
|
2304
|
+
if (!next) return;
|
|
2305
|
+
await this.processJob(worker, next);
|
|
2306
|
+
} finally {
|
|
2307
|
+
worker.__running = worker.__running - 1;
|
|
2308
|
+
if (this.nextJob(queue)) void this.processLoop(worker, queue);
|
|
2309
|
+
else if (worker.handlers?.onIdle) await worker.handlers.onIdle();
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
nextJob(queue) {
|
|
2313
|
+
const ids = this.jobsByQueue.get(queue) ?? [];
|
|
2314
|
+
const candidates = ids.map((id) => this.jobsById.get(id)).filter((j) => Boolean(j)).filter((j) => j.status === "waiting").sort(
|
|
2315
|
+
(a, b) => b.priority - a.priority || a.createdAt.getTime() - b.createdAt.getTime()
|
|
2316
|
+
);
|
|
2317
|
+
return candidates[0] ?? null;
|
|
2318
|
+
}
|
|
2319
|
+
async processJob(worker, job) {
|
|
2320
|
+
if (this.pausedQueues.has(job.queue)) {
|
|
2321
|
+
job.status = "paused";
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
job.status = "active";
|
|
2325
|
+
job.startedAt = /* @__PURE__ */ new Date();
|
|
2326
|
+
job.attemptsMade += 1;
|
|
2327
|
+
job.logs.push({
|
|
2328
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2329
|
+
level: "info",
|
|
2330
|
+
message: "Job started"
|
|
2331
|
+
});
|
|
2332
|
+
if (worker.handlers?.onActive)
|
|
2333
|
+
await worker.handlers.onActive({ job: this.toSearchResult(job) });
|
|
2334
|
+
const start = Date.now();
|
|
2335
|
+
try {
|
|
2336
|
+
const definition = this.registeredJobs.get(job.queue)?.get(job.name);
|
|
2337
|
+
if (!definition) {
|
|
2338
|
+
throw new IgniterJobsError({
|
|
2339
|
+
code: "JOBS_NOT_REGISTERED",
|
|
2340
|
+
message: `Job "${job.name}" is not registered for queue "${job.queue}".`
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
if (definition.onStart) {
|
|
2344
|
+
await definition.onStart({
|
|
2345
|
+
input: job.input,
|
|
2346
|
+
context: {},
|
|
2347
|
+
job: {
|
|
2348
|
+
id: job.id,
|
|
2349
|
+
name: job.name,
|
|
2350
|
+
queue: job.queue,
|
|
2351
|
+
attemptsMade: job.attemptsMade,
|
|
2352
|
+
metadata: job.metadata
|
|
2353
|
+
},
|
|
2354
|
+
scope: job.scope,
|
|
2355
|
+
startedAt: job.startedAt
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
const result = await definition.handler({
|
|
2359
|
+
input: job.input,
|
|
2360
|
+
context: {},
|
|
2361
|
+
job: {
|
|
2362
|
+
id: job.id,
|
|
2363
|
+
name: job.name,
|
|
2364
|
+
queue: job.queue,
|
|
2365
|
+
attemptsMade: job.attemptsMade,
|
|
2366
|
+
metadata: job.metadata
|
|
2367
|
+
},
|
|
2368
|
+
scope: job.scope
|
|
2369
|
+
});
|
|
2370
|
+
const duration = Date.now() - start;
|
|
2371
|
+
job.status = "completed";
|
|
2372
|
+
job.completedAt = /* @__PURE__ */ new Date();
|
|
2373
|
+
job.result = result;
|
|
2374
|
+
job.progress = 100;
|
|
2375
|
+
job.logs.push({
|
|
2376
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2377
|
+
level: "info",
|
|
2378
|
+
message: `Job completed in ${duration}ms`
|
|
2379
|
+
});
|
|
2380
|
+
worker.metrics.processed += 1;
|
|
2381
|
+
worker.metrics.totalDuration += duration;
|
|
2382
|
+
if (definition.onSuccess) {
|
|
2383
|
+
await definition.onSuccess({
|
|
2384
|
+
input: job.input,
|
|
2385
|
+
context: {},
|
|
2386
|
+
job: {
|
|
2387
|
+
id: job.id,
|
|
2388
|
+
name: job.name,
|
|
2389
|
+
queue: job.queue,
|
|
2390
|
+
attemptsMade: job.attemptsMade,
|
|
2391
|
+
metadata: job.metadata
|
|
2392
|
+
},
|
|
2393
|
+
scope: job.scope,
|
|
2394
|
+
result,
|
|
2395
|
+
duration
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
if (worker.handlers?.onSuccess)
|
|
2399
|
+
await worker.handlers.onSuccess({
|
|
2400
|
+
job: this.toSearchResult(job),
|
|
2401
|
+
result
|
|
2402
|
+
});
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
job.error = error?.message ?? String(error);
|
|
2405
|
+
job.logs.push({
|
|
2406
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2407
|
+
level: "error",
|
|
2408
|
+
message: job.error ?? "Unknown error"
|
|
2409
|
+
});
|
|
2410
|
+
const isFinalAttempt = job.attemptsMade >= job.maxAttempts;
|
|
2411
|
+
if (isFinalAttempt) {
|
|
2412
|
+
job.status = "failed";
|
|
2413
|
+
job.completedAt = /* @__PURE__ */ new Date();
|
|
2414
|
+
worker.metrics.failed += 1;
|
|
2415
|
+
const definition = this.registeredJobs.get(job.queue)?.get(job.name);
|
|
2416
|
+
if (definition?.onFailure) {
|
|
2417
|
+
await definition.onFailure({
|
|
2418
|
+
input: job.input,
|
|
2419
|
+
context: {},
|
|
2420
|
+
job: {
|
|
2421
|
+
id: job.id,
|
|
2422
|
+
name: job.name,
|
|
2423
|
+
queue: job.queue,
|
|
2424
|
+
attemptsMade: job.attemptsMade,
|
|
2425
|
+
metadata: job.metadata
|
|
2426
|
+
},
|
|
2427
|
+
scope: job.scope,
|
|
2428
|
+
error,
|
|
2429
|
+
isFinalAttempt: true
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
if (worker.handlers?.onFailure)
|
|
2433
|
+
await worker.handlers.onFailure({
|
|
2434
|
+
job: this.toSearchResult(job),
|
|
2435
|
+
error
|
|
2436
|
+
});
|
|
2437
|
+
} else {
|
|
2438
|
+
job.status = "waiting";
|
|
2439
|
+
void this.kickWorkers(job.queue);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
|
|
2445
|
+
// src/adapters/sqlite.adapter.ts
|
|
2446
|
+
var IgniterJobsSQLiteAdapter = class _IgniterJobsSQLiteAdapter {
|
|
2447
|
+
constructor(options) {
|
|
2448
|
+
this.registeredJobs = /* @__PURE__ */ new Map();
|
|
2449
|
+
this.registeredCrons = /* @__PURE__ */ new Map();
|
|
2450
|
+
this.workers = /* @__PURE__ */ new Map();
|
|
2451
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
2452
|
+
this.pausedQueues = /* @__PURE__ */ new Set();
|
|
2453
|
+
this.queues = {
|
|
2454
|
+
list: async () => this.listQueues(),
|
|
2455
|
+
get: async (name) => this.getQueueInfo(name),
|
|
2456
|
+
getJobCounts: async (name) => this.getQueueJobCounts(name),
|
|
2457
|
+
getJobs: async (name, filter) => {
|
|
2458
|
+
const statuses = filter?.status;
|
|
2459
|
+
const limit = filter?.limit ?? 100;
|
|
2460
|
+
const offset = filter?.offset ?? 0;
|
|
2461
|
+
const results = await this.searchJobs({
|
|
2462
|
+
queue: name,
|
|
2463
|
+
status: statuses,
|
|
2464
|
+
limit,
|
|
2465
|
+
offset
|
|
2466
|
+
});
|
|
2467
|
+
return results;
|
|
2468
|
+
},
|
|
2469
|
+
pause: async (name) => this.pauseQueue(name),
|
|
2470
|
+
resume: async (name) => this.resumeQueue(name),
|
|
2471
|
+
isPaused: async (name) => {
|
|
2472
|
+
const info = await this.getQueueInfo(name);
|
|
2473
|
+
return info?.isPaused ?? false;
|
|
2474
|
+
},
|
|
2475
|
+
drain: async (name) => this.drainQueue(name),
|
|
2476
|
+
clean: async (name, options) => this.cleanQueue(name, options),
|
|
2477
|
+
obliterate: async (name, options) => this.obliterateQueue(name, options)
|
|
2478
|
+
};
|
|
2479
|
+
this.options = {
|
|
2480
|
+
path: options.path,
|
|
2481
|
+
pollingInterval: options.pollingInterval ?? 500,
|
|
2482
|
+
enableWAL: options.enableWAL ?? true
|
|
2483
|
+
};
|
|
2484
|
+
this.client = {
|
|
2485
|
+
type: "sqlite",
|
|
2486
|
+
path: this.options.path
|
|
2487
|
+
};
|
|
2488
|
+
const Database = __require("better-sqlite3");
|
|
2489
|
+
this.db = new Database(this.options.path);
|
|
2490
|
+
this.initializeSchema();
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Creates a new SQLite adapter instance.
|
|
2494
|
+
*
|
|
2495
|
+
* @param options - Configuration options
|
|
2496
|
+
* @returns A new adapter instance
|
|
2497
|
+
*
|
|
2498
|
+
* @example
|
|
2499
|
+
* ```ts
|
|
2500
|
+
* // File-based database (persistent)
|
|
2501
|
+
* const adapter = IgniterJobsSQLiteAdapter.create({
|
|
2502
|
+
* path: './data/jobs.sqlite'
|
|
2503
|
+
* });
|
|
2504
|
+
*
|
|
2505
|
+
* // In-memory database (for testing)
|
|
2506
|
+
* const testAdapter = IgniterJobsSQLiteAdapter.create({
|
|
2507
|
+
* path: ':memory:'
|
|
2508
|
+
* });
|
|
2509
|
+
* ```
|
|
2510
|
+
*/
|
|
2511
|
+
static create(options) {
|
|
2512
|
+
return new _IgniterJobsSQLiteAdapter(options);
|
|
2513
|
+
}
|
|
2514
|
+
initializeSchema() {
|
|
2515
|
+
if (this.options.enableWAL) {
|
|
2516
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
2517
|
+
}
|
|
2518
|
+
this.db.exec(`
|
|
2519
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
2520
|
+
id TEXT PRIMARY KEY,
|
|
2521
|
+
name TEXT NOT NULL,
|
|
2522
|
+
queue TEXT NOT NULL,
|
|
2523
|
+
input TEXT NOT NULL,
|
|
2524
|
+
status TEXT NOT NULL DEFAULT 'waiting',
|
|
2525
|
+
progress REAL NOT NULL DEFAULT 0,
|
|
2526
|
+
attempts_made INTEGER NOT NULL DEFAULT 0,
|
|
2527
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
2528
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
2529
|
+
created_at TEXT NOT NULL,
|
|
2530
|
+
started_at TEXT,
|
|
2531
|
+
completed_at TEXT,
|
|
2532
|
+
scheduled_at TEXT,
|
|
2533
|
+
result TEXT,
|
|
2534
|
+
error TEXT,
|
|
2535
|
+
metadata TEXT,
|
|
2536
|
+
scope TEXT
|
|
2537
|
+
);
|
|
2538
|
+
|
|
2539
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_queue_status ON jobs(queue, status);
|
|
2540
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
2541
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_scheduled_at ON jobs(scheduled_at);
|
|
2542
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_priority ON jobs(priority DESC, created_at ASC);
|
|
2543
|
+
`);
|
|
2544
|
+
this.db.exec(`
|
|
2545
|
+
CREATE TABLE IF NOT EXISTS job_logs (
|
|
2546
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2547
|
+
job_id TEXT NOT NULL,
|
|
2548
|
+
timestamp TEXT NOT NULL,
|
|
2549
|
+
level TEXT NOT NULL,
|
|
2550
|
+
message TEXT NOT NULL,
|
|
2551
|
+
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
|
|
2552
|
+
);
|
|
2553
|
+
|
|
2554
|
+
CREATE INDEX IF NOT EXISTS idx_job_logs_job_id ON job_logs(job_id);
|
|
2555
|
+
`);
|
|
2556
|
+
this.db.exec(`
|
|
2557
|
+
CREATE TABLE IF NOT EXISTS paused_queues (
|
|
2558
|
+
name TEXT PRIMARY KEY
|
|
2559
|
+
);
|
|
2560
|
+
`);
|
|
2561
|
+
const pausedRows = this.db.prepare(
|
|
2562
|
+
"SELECT name FROM paused_queues"
|
|
2563
|
+
).all();
|
|
2564
|
+
for (const row of pausedRows) {
|
|
2565
|
+
this.pausedQueues.add(row.name);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
registerJob(queueName, jobName, definition) {
|
|
2569
|
+
const queueJobs = this.registeredJobs.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
2570
|
+
if (queueJobs.has(jobName)) {
|
|
2571
|
+
throw new IgniterJobsError({
|
|
2572
|
+
code: "JOBS_DUPLICATE_JOB",
|
|
2573
|
+
message: `Job "${jobName}" already registered for queue "${queueName}".`
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
queueJobs.set(jobName, definition);
|
|
2577
|
+
this.registeredJobs.set(queueName, queueJobs);
|
|
2578
|
+
}
|
|
2579
|
+
registerCron(queueName, cronName, definition) {
|
|
2580
|
+
const queueCrons = this.registeredCrons.get(queueName) ?? /* @__PURE__ */ new Map();
|
|
2581
|
+
if (queueCrons.has(cronName)) {
|
|
2582
|
+
throw new IgniterJobsError({
|
|
2583
|
+
code: "JOBS_INVALID_CRON",
|
|
2584
|
+
message: `Cron "${cronName}" already registered for queue "${queueName}".`
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
queueCrons.set(cronName, definition);
|
|
2588
|
+
this.registeredCrons.set(queueName, queueCrons);
|
|
2589
|
+
}
|
|
2590
|
+
async dispatch(params) {
|
|
2591
|
+
const jobId = params.jobId ?? IgniterJobsIdGenerator.generate("job");
|
|
2592
|
+
const maxAttempts = params.attempts ?? 1;
|
|
2593
|
+
const now = /* @__PURE__ */ new Date();
|
|
2594
|
+
let status = "waiting";
|
|
2595
|
+
let scheduledAt = null;
|
|
2596
|
+
if (this.pausedQueues.has(params.queue)) {
|
|
2597
|
+
status = "paused";
|
|
2598
|
+
} else if (params.delay && params.delay > 0) {
|
|
2599
|
+
status = "delayed";
|
|
2600
|
+
scheduledAt = new Date(now.getTime() + params.delay);
|
|
2601
|
+
}
|
|
2602
|
+
const stmt = this.db.prepare(`
|
|
2603
|
+
INSERT INTO jobs (
|
|
2604
|
+
id, name, queue, input, status, progress, attempts_made, max_attempts,
|
|
2605
|
+
priority, created_at, scheduled_at, metadata, scope
|
|
2606
|
+
) VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
|
|
2607
|
+
`);
|
|
2608
|
+
stmt.run(
|
|
2609
|
+
jobId,
|
|
2610
|
+
params.jobName,
|
|
2611
|
+
params.queue,
|
|
2612
|
+
JSON.stringify(params.input ?? {}),
|
|
2613
|
+
status,
|
|
1764
2614
|
maxAttempts,
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
this.jobsById.set(jobId, job);
|
|
1772
|
-
const queueList = this.jobsByQueue.get(params.queue) ?? [];
|
|
1773
|
-
queueList.push(jobId);
|
|
1774
|
-
this.jobsByQueue.set(params.queue, queueList);
|
|
2615
|
+
params.priority ?? 0,
|
|
2616
|
+
now.toISOString(),
|
|
2617
|
+
scheduledAt?.toISOString() ?? null,
|
|
2618
|
+
params.metadata ? JSON.stringify(params.metadata) : null,
|
|
2619
|
+
params.scope ? JSON.stringify(params.scope) : null
|
|
2620
|
+
);
|
|
1775
2621
|
if (params.delay && params.delay > 0) {
|
|
1776
2622
|
setTimeout(() => {
|
|
1777
|
-
|
|
1778
|
-
if (!stored) return;
|
|
1779
|
-
if (!this.pausedQueues.has(params.queue)) stored.status = "waiting";
|
|
1780
|
-
void this.kickWorkers(params.queue);
|
|
2623
|
+
this.promoteDelayedJob(jobId, params.queue);
|
|
1781
2624
|
}, params.delay);
|
|
1782
|
-
return jobId;
|
|
1783
2625
|
}
|
|
1784
|
-
void this.kickWorkers(params.queue);
|
|
1785
2626
|
return jobId;
|
|
1786
2627
|
}
|
|
2628
|
+
promoteDelayedJob(jobId, queue) {
|
|
2629
|
+
if (this.pausedQueues.has(queue)) return;
|
|
2630
|
+
const stmt = this.db.prepare(`
|
|
2631
|
+
UPDATE jobs SET status = 'waiting', scheduled_at = NULL
|
|
2632
|
+
WHERE id = ? AND status = 'delayed'
|
|
2633
|
+
`);
|
|
2634
|
+
stmt.run(jobId);
|
|
2635
|
+
}
|
|
1787
2636
|
async schedule(params) {
|
|
1788
2637
|
if (params.at) {
|
|
1789
2638
|
const delay = params.at.getTime() - Date.now();
|
|
@@ -1801,75 +2650,116 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1801
2650
|
return this.dispatch(params);
|
|
1802
2651
|
}
|
|
1803
2652
|
async getJob(jobId, queue) {
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
if (queue
|
|
1807
|
-
|
|
2653
|
+
let sql = "SELECT * FROM jobs WHERE id = ?";
|
|
2654
|
+
const params = [jobId];
|
|
2655
|
+
if (queue) {
|
|
2656
|
+
sql += " AND queue = ?";
|
|
2657
|
+
params.push(queue);
|
|
2658
|
+
}
|
|
2659
|
+
const row = this.db.prepare(sql).get(...params);
|
|
2660
|
+
if (!row) return null;
|
|
2661
|
+
return this.rowToSearchResult(row);
|
|
1808
2662
|
}
|
|
1809
2663
|
async getJobState(jobId, queue) {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
if (queue
|
|
1813
|
-
|
|
2664
|
+
let sql = "SELECT status FROM jobs WHERE id = ?";
|
|
2665
|
+
const params = [jobId];
|
|
2666
|
+
if (queue) {
|
|
2667
|
+
sql += " AND queue = ?";
|
|
2668
|
+
params.push(queue);
|
|
2669
|
+
}
|
|
2670
|
+
const row = this.db.prepare(sql).get(...params);
|
|
2671
|
+
return row?.status ?? null;
|
|
1814
2672
|
}
|
|
1815
2673
|
async getJobLogs(jobId, queue) {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2674
|
+
if (queue) {
|
|
2675
|
+
const job = await this.getJob(jobId, queue);
|
|
2676
|
+
if (!job) return [];
|
|
2677
|
+
}
|
|
2678
|
+
const rows = this.db.prepare("SELECT * FROM job_logs WHERE job_id = ? ORDER BY timestamp ASC").all(jobId);
|
|
2679
|
+
return rows.map((row) => ({
|
|
2680
|
+
timestamp: new Date(row.timestamp),
|
|
2681
|
+
level: row.level,
|
|
2682
|
+
message: row.message
|
|
2683
|
+
}));
|
|
1820
2684
|
}
|
|
1821
2685
|
async getJobProgress(jobId, queue) {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
if (queue
|
|
1825
|
-
|
|
2686
|
+
let sql = "SELECT progress FROM jobs WHERE id = ?";
|
|
2687
|
+
const params = [jobId];
|
|
2688
|
+
if (queue) {
|
|
2689
|
+
sql += " AND queue = ?";
|
|
2690
|
+
params.push(queue);
|
|
2691
|
+
}
|
|
2692
|
+
const row = this.db.prepare(sql).get(...params);
|
|
2693
|
+
return row?.progress ?? 0;
|
|
1826
2694
|
}
|
|
1827
2695
|
async retryJob(jobId, queue) {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2696
|
+
let sql = "SELECT id FROM jobs WHERE id = ?";
|
|
2697
|
+
const checkParams = [jobId];
|
|
2698
|
+
if (queue) {
|
|
2699
|
+
sql += " AND queue = ?";
|
|
2700
|
+
checkParams.push(queue);
|
|
1831
2701
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
2702
|
+
const exists = this.db.prepare(sql).get(...checkParams);
|
|
2703
|
+
if (!exists) {
|
|
2704
|
+
throw new IgniterJobsError({
|
|
2705
|
+
code: "JOBS_NOT_FOUND",
|
|
2706
|
+
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
2707
|
+
});
|
|
1834
2708
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2709
|
+
let updateSql = "UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0 WHERE id = ?";
|
|
2710
|
+
const updateParams = [jobId];
|
|
2711
|
+
if (queue) {
|
|
2712
|
+
updateSql = updateSql.replace("WHERE id = ?", "WHERE id = ? AND queue = ?");
|
|
2713
|
+
updateParams.push(queue);
|
|
2714
|
+
}
|
|
2715
|
+
this.db.prepare(updateSql).run(...updateParams);
|
|
1840
2716
|
}
|
|
1841
2717
|
async removeJob(jobId, queue) {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
if (queue
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2718
|
+
let sql = "DELETE FROM jobs WHERE id = ?";
|
|
2719
|
+
const params = [jobId];
|
|
2720
|
+
if (queue) {
|
|
2721
|
+
sql += " AND queue = ?";
|
|
2722
|
+
params.push(queue);
|
|
2723
|
+
}
|
|
2724
|
+
this.db.prepare(sql).run(...params);
|
|
1848
2725
|
}
|
|
1849
2726
|
async promoteJob(jobId, queue) {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2727
|
+
let sql = "SELECT id, status, queue FROM jobs WHERE id = ?";
|
|
2728
|
+
const checkParams = [jobId];
|
|
2729
|
+
if (queue) {
|
|
2730
|
+
sql += " AND queue = ?";
|
|
2731
|
+
checkParams.push(queue);
|
|
1853
2732
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
2733
|
+
const row = this.db.prepare(sql).get(...checkParams);
|
|
2734
|
+
if (!row) {
|
|
2735
|
+
throw new IgniterJobsError({
|
|
2736
|
+
code: "JOBS_NOT_FOUND",
|
|
2737
|
+
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
2738
|
+
});
|
|
1856
2739
|
}
|
|
1857
|
-
if (
|
|
1858
|
-
|
|
1859
|
-
|
|
2740
|
+
if (row.status === "delayed" || row.status === "paused") {
|
|
2741
|
+
const newStatus = this.pausedQueues.has(row.queue) ? "paused" : "waiting";
|
|
2742
|
+
this.db.prepare("UPDATE jobs SET status = ?, scheduled_at = NULL WHERE id = ?").run(newStatus, jobId);
|
|
1860
2743
|
}
|
|
1861
2744
|
}
|
|
1862
2745
|
async moveJobToFailed(jobId, reason, queue) {
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2746
|
+
let sql = "SELECT id FROM jobs WHERE id = ?";
|
|
2747
|
+
const checkParams = [jobId];
|
|
2748
|
+
if (queue) {
|
|
2749
|
+
sql += " AND queue = ?";
|
|
2750
|
+
checkParams.push(queue);
|
|
1866
2751
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
2752
|
+
const exists = this.db.prepare(sql).get(...checkParams);
|
|
2753
|
+
if (!exists) {
|
|
2754
|
+
throw new IgniterJobsError({
|
|
2755
|
+
code: "JOBS_NOT_FOUND",
|
|
2756
|
+
message: `Job "${jobId}" not found${queue ? ` in queue "${queue}"` : ""}.`
|
|
2757
|
+
});
|
|
1869
2758
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2759
|
+
this.db.prepare(`
|
|
2760
|
+
UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
|
|
2761
|
+
WHERE id = ?
|
|
2762
|
+
`).run(reason, (/* @__PURE__ */ new Date()).toISOString(), jobId);
|
|
1873
2763
|
}
|
|
1874
2764
|
async retryManyJobs(jobIds, queue) {
|
|
1875
2765
|
await Promise.all(jobIds.map((id) => this.retryJob(id, queue)));
|
|
@@ -1886,7 +2776,6 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1886
2776
|
};
|
|
1887
2777
|
}
|
|
1888
2778
|
async getQueueJobCounts(queue) {
|
|
1889
|
-
const jobIds = this.jobsByQueue.get(queue) ?? [];
|
|
1890
2779
|
const counts = {
|
|
1891
2780
|
waiting: 0,
|
|
1892
2781
|
active: 0,
|
|
@@ -1895,132 +2784,131 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
1895
2784
|
delayed: 0,
|
|
1896
2785
|
paused: 0
|
|
1897
2786
|
};
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2787
|
+
const rows = this.db.prepare(
|
|
2788
|
+
"SELECT status, COUNT(*) as count FROM jobs WHERE queue = ? GROUP BY status"
|
|
2789
|
+
).all(queue);
|
|
2790
|
+
for (const row of rows) {
|
|
2791
|
+
if (row.status in counts) {
|
|
2792
|
+
counts[row.status] = row.count;
|
|
1903
2793
|
}
|
|
1904
2794
|
}
|
|
1905
2795
|
return counts;
|
|
1906
2796
|
}
|
|
1907
2797
|
async listQueues() {
|
|
1908
|
-
const
|
|
2798
|
+
const jobQueues = this.db.prepare("SELECT DISTINCT queue FROM jobs").all().map((r) => r.queue);
|
|
2799
|
+
const allQueues = /* @__PURE__ */ new Set([
|
|
2800
|
+
...jobQueues,
|
|
2801
|
+
...this.registeredJobs.keys(),
|
|
2802
|
+
...this.registeredCrons.keys()
|
|
2803
|
+
]);
|
|
1909
2804
|
const result = [];
|
|
1910
|
-
for (const q of
|
|
1911
|
-
|
|
2805
|
+
for (const q of allQueues) {
|
|
2806
|
+
const info = await this.getQueueInfo(q);
|
|
2807
|
+
if (info) result.push(info);
|
|
1912
2808
|
}
|
|
1913
2809
|
return result;
|
|
1914
2810
|
}
|
|
1915
2811
|
async pauseQueue(queue) {
|
|
1916
2812
|
this.pausedQueues.add(queue);
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
if (job.status === "waiting") job.status = "paused";
|
|
1922
|
-
}
|
|
2813
|
+
this.db.prepare("INSERT OR IGNORE INTO paused_queues (name) VALUES (?)").run(queue);
|
|
2814
|
+
this.db.prepare(
|
|
2815
|
+
"UPDATE jobs SET status = 'paused' WHERE queue = ? AND status = 'waiting'"
|
|
2816
|
+
).run(queue);
|
|
1923
2817
|
}
|
|
1924
2818
|
async resumeQueue(queue) {
|
|
1925
2819
|
this.pausedQueues.delete(queue);
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
if (job.status === "paused") job.status = "waiting";
|
|
1931
|
-
}
|
|
1932
|
-
void this.kickWorkers(queue);
|
|
2820
|
+
this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
|
|
2821
|
+
this.db.prepare(
|
|
2822
|
+
"UPDATE jobs SET status = 'waiting' WHERE queue = ? AND status = 'paused'"
|
|
2823
|
+
).run(queue);
|
|
1933
2824
|
}
|
|
1934
2825
|
async drainQueue(queue) {
|
|
1935
|
-
const
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
if (!job) continue;
|
|
1940
|
-
if (job.status === "waiting" || job.status === "paused") {
|
|
1941
|
-
this.jobsById.delete(id);
|
|
1942
|
-
removed++;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
this.jobsByQueue.set(queue, jobIds.filter((id) => this.jobsById.has(id)));
|
|
1946
|
-
return removed;
|
|
2826
|
+
const result = this.db.prepare(
|
|
2827
|
+
"DELETE FROM jobs WHERE queue = ? AND status IN ('waiting', 'paused')"
|
|
2828
|
+
).run(queue);
|
|
2829
|
+
return result.changes;
|
|
1947
2830
|
}
|
|
1948
2831
|
async cleanQueue(queue, options) {
|
|
1949
2832
|
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
1950
2833
|
const olderThan = options.olderThan ?? 0;
|
|
1951
2834
|
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
1952
|
-
const
|
|
1953
|
-
const
|
|
1954
|
-
let
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2835
|
+
const cutoffTime = new Date(Date.now() - olderThan).toISOString();
|
|
2836
|
+
const statusPlaceholders = statuses.map(() => "?").join(", ");
|
|
2837
|
+
let sql = `
|
|
2838
|
+
DELETE FROM jobs WHERE id IN (
|
|
2839
|
+
SELECT id FROM jobs
|
|
2840
|
+
WHERE queue = ? AND status IN (${statusPlaceholders}) AND created_at < ?
|
|
2841
|
+
ORDER BY created_at ASC
|
|
2842
|
+
LIMIT ?
|
|
2843
|
+
)
|
|
2844
|
+
`;
|
|
2845
|
+
const result = this.db.prepare(sql).run(
|
|
2846
|
+
queue,
|
|
2847
|
+
...statuses,
|
|
2848
|
+
cutoffTime,
|
|
2849
|
+
limit === Number.POSITIVE_INFINITY ? -1 : limit
|
|
2850
|
+
);
|
|
2851
|
+
return result.changes;
|
|
1967
2852
|
}
|
|
1968
|
-
async obliterateQueue(queue,
|
|
1969
|
-
|
|
1970
|
-
for (const id of jobIds) this.jobsById.delete(id);
|
|
1971
|
-
this.jobsByQueue.delete(queue);
|
|
2853
|
+
async obliterateQueue(queue, _options) {
|
|
2854
|
+
this.db.prepare("DELETE FROM jobs WHERE queue = ?").run(queue);
|
|
1972
2855
|
this.registeredJobs.delete(queue);
|
|
1973
2856
|
this.registeredCrons.delete(queue);
|
|
1974
2857
|
this.pausedQueues.delete(queue);
|
|
2858
|
+
this.db.prepare("DELETE FROM paused_queues WHERE name = ?").run(queue);
|
|
1975
2859
|
}
|
|
1976
2860
|
async retryAllInQueue(queue) {
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
if (job.status === "failed") {
|
|
1983
|
-
await this.retryJob(id, queue);
|
|
1984
|
-
retried++;
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
return retried;
|
|
2861
|
+
const result = this.db.prepare(`
|
|
2862
|
+
UPDATE jobs SET status = 'waiting', error = NULL, completed_at = NULL, progress = 0
|
|
2863
|
+
WHERE queue = ? AND status = 'failed'
|
|
2864
|
+
`).run(queue);
|
|
2865
|
+
return result.changes;
|
|
1988
2866
|
}
|
|
1989
2867
|
async pauseJobType(queue, jobName) {
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
if (!job) continue;
|
|
1994
|
-
if (job.name === jobName && job.status === "waiting") job.status = "paused";
|
|
1995
|
-
}
|
|
2868
|
+
this.db.prepare(
|
|
2869
|
+
"UPDATE jobs SET status = 'paused' WHERE queue = ? AND name = ? AND status = 'waiting'"
|
|
2870
|
+
).run(queue, jobName);
|
|
1996
2871
|
}
|
|
1997
2872
|
async resumeJobType(queue, jobName) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
if (!job) continue;
|
|
2002
|
-
if (job.name === jobName && job.status === "paused") job.status = "waiting";
|
|
2003
|
-
}
|
|
2004
|
-
void this.kickWorkers(queue);
|
|
2873
|
+
this.db.prepare(
|
|
2874
|
+
"UPDATE jobs SET status = 'waiting' WHERE queue = ? AND name = ? AND status = 'paused'"
|
|
2875
|
+
).run(queue, jobName);
|
|
2005
2876
|
}
|
|
2006
2877
|
async searchJobs(filter) {
|
|
2007
2878
|
const queue = filter?.queue;
|
|
2008
2879
|
const statuses = filter?.status;
|
|
2009
2880
|
const limit = filter?.limit ?? 100;
|
|
2010
2881
|
const offset = filter?.offset ?? 0;
|
|
2011
|
-
|
|
2012
|
-
|
|
2882
|
+
let sql = "SELECT * FROM jobs WHERE 1=1";
|
|
2883
|
+
const params = [];
|
|
2884
|
+
if (queue) {
|
|
2885
|
+
sql += " AND queue = ?";
|
|
2886
|
+
params.push(queue);
|
|
2887
|
+
}
|
|
2888
|
+
if (statuses && statuses.length > 0) {
|
|
2889
|
+
const placeholders = statuses.map(() => "?").join(", ");
|
|
2890
|
+
sql += ` AND status IN (${placeholders})`;
|
|
2891
|
+
params.push(...statuses);
|
|
2892
|
+
}
|
|
2893
|
+
sql += " ORDER BY priority DESC, created_at ASC LIMIT ? OFFSET ?";
|
|
2894
|
+
params.push(limit, offset);
|
|
2895
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
2896
|
+
return rows.map((row) => this.rowToSearchResult(row));
|
|
2013
2897
|
}
|
|
2014
2898
|
async searchQueues(filter) {
|
|
2015
2899
|
const name = filter?.name;
|
|
2016
2900
|
const isPaused = filter?.isPaused;
|
|
2017
2901
|
const all = await this.listQueues();
|
|
2018
|
-
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
2902
|
+
return all.filter((q) => name ? q.name.includes(name) : true).filter(
|
|
2903
|
+
(q) => typeof isPaused === "boolean" ? q.isPaused === isPaused : true
|
|
2904
|
+
);
|
|
2019
2905
|
}
|
|
2020
2906
|
async searchWorkers(filter) {
|
|
2021
2907
|
const queue = filter?.queue;
|
|
2022
2908
|
const isRunning = filter?.isRunning;
|
|
2023
|
-
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
2909
|
+
return Array.from(this.workers.values()).filter((w) => queue ? w.queues.includes(queue) : true).filter(
|
|
2910
|
+
(w) => typeof isRunning === "boolean" ? isRunning ? !w.closed : w.closed : true
|
|
2911
|
+
).map((w) => this.toWorkerHandle(w));
|
|
2024
2912
|
}
|
|
2025
2913
|
async createWorker(config) {
|
|
2026
2914
|
const workerId = IgniterJobsIdGenerator.generate("worker");
|
|
@@ -2032,15 +2920,18 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
2032
2920
|
closed: false,
|
|
2033
2921
|
startedAt: /* @__PURE__ */ new Date(),
|
|
2034
2922
|
metrics: { processed: 0, failed: 0, totalDuration: 0 },
|
|
2035
|
-
handlers: config.handlers
|
|
2923
|
+
handlers: config.handlers,
|
|
2924
|
+
activeJobs: 0
|
|
2036
2925
|
};
|
|
2037
2926
|
this.workers.set(workerId, state);
|
|
2038
|
-
|
|
2927
|
+
this.startPollingLoop(state);
|
|
2039
2928
|
return this.toWorkerHandle(state);
|
|
2040
2929
|
}
|
|
2041
2930
|
getWorkers() {
|
|
2042
2931
|
const out = /* @__PURE__ */ new Map();
|
|
2043
|
-
for (const [id, state] of this.workers)
|
|
2932
|
+
for (const [id, state] of this.workers) {
|
|
2933
|
+
out.set(id, this.toWorkerHandle(state));
|
|
2934
|
+
}
|
|
2044
2935
|
return out;
|
|
2045
2936
|
}
|
|
2046
2937
|
async publishEvent(channel, payload) {
|
|
@@ -2060,26 +2951,36 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
2060
2951
|
};
|
|
2061
2952
|
}
|
|
2062
2953
|
async shutdown() {
|
|
2954
|
+
for (const worker of this.workers.values()) {
|
|
2955
|
+
worker.closed = true;
|
|
2956
|
+
if (worker.pollingTimer) {
|
|
2957
|
+
clearInterval(worker.pollingTimer);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2063
2960
|
this.workers.clear();
|
|
2064
2961
|
this.subscribers.clear();
|
|
2962
|
+
this.db.close();
|
|
2065
2963
|
}
|
|
2066
|
-
|
|
2964
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2965
|
+
// Private helpers
|
|
2966
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2967
|
+
rowToSearchResult(row) {
|
|
2067
2968
|
return {
|
|
2068
|
-
id:
|
|
2069
|
-
name:
|
|
2070
|
-
queue:
|
|
2071
|
-
status:
|
|
2072
|
-
input:
|
|
2073
|
-
result:
|
|
2074
|
-
error:
|
|
2075
|
-
progress:
|
|
2076
|
-
attemptsMade:
|
|
2077
|
-
priority:
|
|
2078
|
-
createdAt:
|
|
2079
|
-
startedAt:
|
|
2080
|
-
completedAt:
|
|
2081
|
-
metadata:
|
|
2082
|
-
scope:
|
|
2969
|
+
id: row.id,
|
|
2970
|
+
name: row.name,
|
|
2971
|
+
queue: row.queue,
|
|
2972
|
+
status: row.status,
|
|
2973
|
+
input: JSON.parse(row.input),
|
|
2974
|
+
result: row.result ? JSON.parse(row.result) : void 0,
|
|
2975
|
+
error: row.error ?? void 0,
|
|
2976
|
+
progress: row.progress,
|
|
2977
|
+
attemptsMade: row.attempts_made,
|
|
2978
|
+
priority: row.priority,
|
|
2979
|
+
createdAt: new Date(row.created_at),
|
|
2980
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
2981
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : void 0,
|
|
2982
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2983
|
+
scope: row.scope ? JSON.parse(row.scope) : void 0
|
|
2083
2984
|
};
|
|
2084
2985
|
}
|
|
2085
2986
|
toWorkerHandle(worker) {
|
|
@@ -2091,10 +2992,12 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
2091
2992
|
},
|
|
2092
2993
|
resume: async () => {
|
|
2093
2994
|
worker.paused = false;
|
|
2094
|
-
for (const q of worker.queues) void this.kickWorkers(q);
|
|
2095
2995
|
},
|
|
2096
2996
|
close: async () => {
|
|
2097
2997
|
worker.closed = true;
|
|
2998
|
+
if (worker.pollingTimer) {
|
|
2999
|
+
clearInterval(worker.pollingTimer);
|
|
3000
|
+
}
|
|
2098
3001
|
},
|
|
2099
3002
|
isRunning: () => !worker.closed && !worker.paused,
|
|
2100
3003
|
isPaused: () => worker.paused,
|
|
@@ -2113,150 +3016,167 @@ var IgniterJobsMemoryAdapter = class _IgniterJobsMemoryAdapter {
|
|
|
2113
3016
|
uptime
|
|
2114
3017
|
};
|
|
2115
3018
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
3019
|
+
startPollingLoop(worker) {
|
|
3020
|
+
const poll = () => {
|
|
3021
|
+
if (worker.closed || worker.paused) return;
|
|
3022
|
+
void this.processNextJobs(worker);
|
|
3023
|
+
};
|
|
3024
|
+
poll();
|
|
3025
|
+
worker.pollingTimer = setInterval(poll, this.options.pollingInterval);
|
|
2123
3026
|
}
|
|
2124
|
-
async
|
|
3027
|
+
async processNextJobs(worker) {
|
|
2125
3028
|
if (worker.closed || worker.paused) return;
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
const
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
if (
|
|
2138
|
-
|
|
3029
|
+
const availableSlots = worker.concurrency - worker.activeJobs;
|
|
3030
|
+
if (availableSlots <= 0) return;
|
|
3031
|
+
const queueFilter = worker.queues.length > 0 ? `queue IN (${worker.queues.map(() => "?").join(", ")})` : "1=1";
|
|
3032
|
+
const params = worker.queues.length > 0 ? [...worker.queues, availableSlots] : [availableSlots];
|
|
3033
|
+
const rows = this.db.prepare(`
|
|
3034
|
+
SELECT * FROM jobs
|
|
3035
|
+
WHERE status = 'waiting' AND ${queueFilter}
|
|
3036
|
+
ORDER BY priority DESC, created_at ASC
|
|
3037
|
+
LIMIT ?
|
|
3038
|
+
`).all(...params);
|
|
3039
|
+
for (const row of rows) {
|
|
3040
|
+
if (worker.closed || worker.paused) break;
|
|
3041
|
+
if (worker.activeJobs >= worker.concurrency) break;
|
|
3042
|
+
const claimed = this.db.prepare(`
|
|
3043
|
+
UPDATE jobs SET status = 'active', started_at = ?
|
|
3044
|
+
WHERE id = ? AND status = 'waiting'
|
|
3045
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), row.id);
|
|
3046
|
+
if (claimed.changes === 0) continue;
|
|
3047
|
+
worker.activeJobs++;
|
|
3048
|
+
void this.processJob(worker, row.id).finally(() => {
|
|
3049
|
+
worker.activeJobs--;
|
|
3050
|
+
if (worker.handlers?.onIdle && worker.activeJobs === 0) {
|
|
3051
|
+
void worker.handlers.onIdle();
|
|
3052
|
+
}
|
|
3053
|
+
});
|
|
2139
3054
|
}
|
|
2140
3055
|
}
|
|
2141
|
-
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
job.status = "paused";
|
|
2149
|
-
return;
|
|
3056
|
+
async processJob(worker, jobId) {
|
|
3057
|
+
const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(jobId);
|
|
3058
|
+
if (!row) return;
|
|
3059
|
+
const job = this.rowToSearchResult(row);
|
|
3060
|
+
this.addJobLog(jobId, "info", "Job started");
|
|
3061
|
+
if (worker.handlers?.onActive) {
|
|
3062
|
+
await worker.handlers.onActive({ job });
|
|
2150
3063
|
}
|
|
2151
|
-
job.status = "active";
|
|
2152
|
-
job.startedAt = /* @__PURE__ */ new Date();
|
|
2153
|
-
job.attemptsMade += 1;
|
|
2154
|
-
job.logs.push({ timestamp: /* @__PURE__ */ new Date(), level: "info", message: "Job started" });
|
|
2155
|
-
if (worker.handlers?.onActive) await worker.handlers.onActive({ job: this.toSearchResult(job) });
|
|
2156
3064
|
const start = Date.now();
|
|
2157
3065
|
try {
|
|
2158
|
-
const definition = this.registeredJobs.get(
|
|
3066
|
+
const definition = this.registeredJobs.get(row.queue)?.get(row.name);
|
|
2159
3067
|
if (!definition) {
|
|
2160
3068
|
throw new IgniterJobsError({
|
|
2161
3069
|
code: "JOBS_NOT_REGISTERED",
|
|
2162
|
-
message: `Job "${
|
|
3070
|
+
message: `Job "${row.name}" is not registered for queue "${row.queue}".`
|
|
2163
3071
|
});
|
|
2164
3072
|
}
|
|
3073
|
+
this.db.prepare("UPDATE jobs SET attempts_made = attempts_made + 1 WHERE id = ?").run(jobId);
|
|
2165
3074
|
if (definition.onStart) {
|
|
2166
3075
|
await definition.onStart({
|
|
2167
3076
|
input: job.input,
|
|
2168
3077
|
context: {},
|
|
2169
|
-
job: {
|
|
3078
|
+
job: {
|
|
3079
|
+
id: job.id,
|
|
3080
|
+
name: job.name,
|
|
3081
|
+
queue: job.queue,
|
|
3082
|
+
attemptsMade: job.attemptsMade + 1,
|
|
3083
|
+
metadata: job.metadata
|
|
3084
|
+
},
|
|
2170
3085
|
scope: job.scope,
|
|
2171
|
-
startedAt:
|
|
3086
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
2172
3087
|
});
|
|
2173
3088
|
}
|
|
2174
3089
|
const result = await definition.handler({
|
|
2175
3090
|
input: job.input,
|
|
2176
3091
|
context: {},
|
|
2177
|
-
job: {
|
|
3092
|
+
job: {
|
|
3093
|
+
id: job.id,
|
|
3094
|
+
name: job.name,
|
|
3095
|
+
queue: job.queue,
|
|
3096
|
+
attemptsMade: job.attemptsMade + 1,
|
|
3097
|
+
metadata: job.metadata
|
|
3098
|
+
},
|
|
2178
3099
|
scope: job.scope
|
|
2179
3100
|
});
|
|
2180
3101
|
const duration = Date.now() - start;
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
worker.metrics.processed
|
|
3102
|
+
this.db.prepare(`
|
|
3103
|
+
UPDATE jobs SET status = 'completed', completed_at = ?, result = ?, progress = 100
|
|
3104
|
+
WHERE id = ?
|
|
3105
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), JSON.stringify(result), jobId);
|
|
3106
|
+
this.addJobLog(jobId, "info", `Job completed in ${duration}ms`);
|
|
3107
|
+
worker.metrics.processed++;
|
|
2187
3108
|
worker.metrics.totalDuration += duration;
|
|
2188
3109
|
if (definition.onSuccess) {
|
|
2189
3110
|
await definition.onSuccess({
|
|
2190
3111
|
input: job.input,
|
|
2191
3112
|
context: {},
|
|
2192
|
-
job: {
|
|
3113
|
+
job: {
|
|
3114
|
+
id: job.id,
|
|
3115
|
+
name: job.name,
|
|
3116
|
+
queue: job.queue,
|
|
3117
|
+
attemptsMade: job.attemptsMade + 1,
|
|
3118
|
+
metadata: job.metadata
|
|
3119
|
+
},
|
|
2193
3120
|
scope: job.scope,
|
|
2194
3121
|
result,
|
|
2195
3122
|
duration
|
|
2196
3123
|
});
|
|
2197
3124
|
}
|
|
2198
|
-
if (worker.handlers?.onSuccess)
|
|
3125
|
+
if (worker.handlers?.onSuccess) {
|
|
3126
|
+
const updatedJob = await this.getJob(jobId);
|
|
3127
|
+
if (updatedJob) {
|
|
3128
|
+
await worker.handlers.onSuccess({ job: updatedJob, result });
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
2199
3131
|
} catch (error) {
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
const
|
|
3132
|
+
const errorMessage = error?.message ?? String(error);
|
|
3133
|
+
this.addJobLog(jobId, "error", errorMessage);
|
|
3134
|
+
const current = this.db.prepare(
|
|
3135
|
+
"SELECT attempts_made, max_attempts FROM jobs WHERE id = ?"
|
|
3136
|
+
).get(jobId);
|
|
3137
|
+
const isFinalAttempt = (current?.attempts_made ?? 0) >= (current?.max_attempts ?? 1);
|
|
2203
3138
|
if (isFinalAttempt) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
3139
|
+
this.db.prepare(`
|
|
3140
|
+
UPDATE jobs SET status = 'failed', error = ?, completed_at = ?
|
|
3141
|
+
WHERE id = ?
|
|
3142
|
+
`).run(errorMessage, (/* @__PURE__ */ new Date()).toISOString(), jobId);
|
|
3143
|
+
worker.metrics.failed++;
|
|
3144
|
+
const definition = this.registeredJobs.get(row.queue)?.get(row.name);
|
|
2208
3145
|
if (definition?.onFailure) {
|
|
2209
3146
|
await definition.onFailure({
|
|
2210
3147
|
input: job.input,
|
|
2211
3148
|
context: {},
|
|
2212
|
-
job: {
|
|
3149
|
+
job: {
|
|
3150
|
+
id: job.id,
|
|
3151
|
+
name: job.name,
|
|
3152
|
+
queue: job.queue,
|
|
3153
|
+
attemptsMade: current?.attempts_made ?? 1,
|
|
3154
|
+
metadata: job.metadata
|
|
3155
|
+
},
|
|
2213
3156
|
scope: job.scope,
|
|
2214
3157
|
error,
|
|
2215
3158
|
isFinalAttempt: true
|
|
2216
3159
|
});
|
|
2217
3160
|
}
|
|
2218
|
-
if (worker.handlers?.onFailure)
|
|
3161
|
+
if (worker.handlers?.onFailure) {
|
|
3162
|
+
const updatedJob = await this.getJob(jobId);
|
|
3163
|
+
if (updatedJob) {
|
|
3164
|
+
await worker.handlers.onFailure({ job: updatedJob, error });
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
2219
3167
|
} else {
|
|
2220
|
-
|
|
2221
|
-
void this.kickWorkers(job.queue);
|
|
3168
|
+
this.db.prepare("UPDATE jobs SET status = 'waiting' WHERE id = ?").run(jobId);
|
|
2222
3169
|
}
|
|
2223
3170
|
}
|
|
2224
3171
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
events: {
|
|
2231
|
-
// Job lifecycle events
|
|
2232
|
-
job: {
|
|
2233
|
-
enqueued: {},
|
|
2234
|
-
started: {},
|
|
2235
|
-
completed: {},
|
|
2236
|
-
failed: {},
|
|
2237
|
-
progress: {},
|
|
2238
|
-
retrying: {},
|
|
2239
|
-
scheduled: {}
|
|
2240
|
-
},
|
|
2241
|
-
// Worker lifecycle events
|
|
2242
|
-
worker: {
|
|
2243
|
-
started: {},
|
|
2244
|
-
stopped: {},
|
|
2245
|
-
idle: {},
|
|
2246
|
-
paused: {},
|
|
2247
|
-
resumed: {}
|
|
2248
|
-
},
|
|
2249
|
-
// Queue management events
|
|
2250
|
-
queue: {
|
|
2251
|
-
paused: {},
|
|
2252
|
-
resumed: {},
|
|
2253
|
-
drained: {},
|
|
2254
|
-
cleaned: {},
|
|
2255
|
-
obliterated: {}
|
|
2256
|
-
}
|
|
3172
|
+
addJobLog(jobId, level, message) {
|
|
3173
|
+
this.db.prepare(`
|
|
3174
|
+
INSERT INTO job_logs (job_id, timestamp, level, message)
|
|
3175
|
+
VALUES (?, ?, ?, ?)
|
|
3176
|
+
`).run(jobId, (/* @__PURE__ */ new Date()).toISOString(), level, message);
|
|
2257
3177
|
}
|
|
2258
3178
|
};
|
|
2259
3179
|
|
|
2260
|
-
export { IGNITER_JOBS_ERROR_CODES, IgniterJobs, IgniterJobsBuilder, IgniterJobsBullMQAdapter, IgniterJobsError, IgniterJobsMemoryAdapter,
|
|
3180
|
+
export { IGNITER_JOBS_ERROR_CODES, IgniterJobs, IgniterJobsBuilder, IgniterJobsBullMQAdapter, IgniterJobsError, IgniterJobsManager, IgniterJobsMemoryAdapter, IgniterJobsSQLiteAdapter, IgniterQueue, IgniterQueueBuilder, IgniterWorkerBuilder };
|
|
2261
3181
|
//# sourceMappingURL=index.mjs.map
|
|
2262
3182
|
//# sourceMappingURL=index.mjs.map
|