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