@quave/agenda 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +25 -0
- package/README.md +380 -0
- package/dist/Job.d.ts +190 -0
- package/dist/Job.js +689 -0
- package/dist/Job.js.map +1 -0
- package/dist/JobProcessingQueue.d.ts +47 -0
- package/dist/JobProcessingQueue.js +107 -0
- package/dist/JobProcessingQueue.js.map +1 -0
- package/dist/JobProcessor.d.ts +72 -0
- package/dist/JobProcessor.js +512 -0
- package/dist/JobProcessor.js.map +1 -0
- package/dist/backends/index.d.ts +18 -0
- package/dist/backends/index.js +20 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/cjs/Job.d.ts +190 -0
- package/dist/cjs/Job.js +696 -0
- package/dist/cjs/Job.js.map +1 -0
- package/dist/cjs/JobProcessingQueue.d.ts +47 -0
- package/dist/cjs/JobProcessingQueue.js +111 -0
- package/dist/cjs/JobProcessingQueue.js.map +1 -0
- package/dist/cjs/JobProcessor.d.ts +72 -0
- package/dist/cjs/JobProcessor.js +519 -0
- package/dist/cjs/JobProcessor.js.map +1 -0
- package/dist/cjs/backends/index.d.ts +17 -0
- package/dist/cjs/backends/index.js +20 -0
- package/dist/cjs/backends/index.js.map +1 -0
- package/dist/cjs/decorators/Define.d.ts +33 -0
- package/dist/cjs/decorators/Define.js +47 -0
- package/dist/cjs/decorators/Define.js.map +1 -0
- package/dist/cjs/decorators/Every.d.ts +43 -0
- package/dist/cjs/decorators/Every.js +58 -0
- package/dist/cjs/decorators/Every.js.map +1 -0
- package/dist/cjs/decorators/JobsController.d.ts +43 -0
- package/dist/cjs/decorators/JobsController.js +40 -0
- package/dist/cjs/decorators/JobsController.js.map +1 -0
- package/dist/cjs/decorators/index.d.ts +41 -0
- package/dist/cjs/decorators/index.js +57 -0
- package/dist/cjs/decorators/index.js.map +1 -0
- package/dist/cjs/decorators/metadata.d.ts +50 -0
- package/dist/cjs/decorators/metadata.js +41 -0
- package/dist/cjs/decorators/metadata.js.map +1 -0
- package/dist/cjs/decorators/register.d.ts +52 -0
- package/dist/cjs/decorators/register.js +145 -0
- package/dist/cjs/decorators/register.js.map +1 -0
- package/dist/cjs/index.d.ts +498 -0
- package/dist/cjs/index.js +776 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/notifications/BaseNotificationChannel.d.ts +47 -0
- package/dist/cjs/notifications/BaseNotificationChannel.js +112 -0
- package/dist/cjs/notifications/BaseNotificationChannel.js.map +1 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.d.ts +13 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.js +37 -0
- package/dist/cjs/notifications/InMemoryNotificationChannel.js.map +1 -0
- package/dist/cjs/notifications/index.d.ts +2 -0
- package/dist/cjs/notifications/index.js +8 -0
- package/dist/cjs/notifications/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types/AgendaBackend.d.ts +61 -0
- package/dist/cjs/types/AgendaBackend.js +3 -0
- package/dist/cjs/types/AgendaBackend.js.map +1 -0
- package/dist/cjs/types/AgendaConfig.d.ts +10 -0
- package/dist/cjs/types/AgendaConfig.js +3 -0
- package/dist/cjs/types/AgendaConfig.js.map +1 -0
- package/dist/cjs/types/AgendaStatus.d.ts +32 -0
- package/dist/cjs/types/AgendaStatus.js +3 -0
- package/dist/cjs/types/AgendaStatus.js.map +1 -0
- package/dist/cjs/types/DbOptions.d.ts +4 -0
- package/dist/cjs/types/DbOptions.js +3 -0
- package/dist/cjs/types/DbOptions.js.map +1 -0
- package/dist/cjs/types/DrainOptions.d.ts +37 -0
- package/dist/cjs/types/DrainOptions.js +3 -0
- package/dist/cjs/types/DrainOptions.js.map +1 -0
- package/dist/cjs/types/JobDefinition.d.ts +49 -0
- package/dist/cjs/types/JobDefinition.js +3 -0
- package/dist/cjs/types/JobDefinition.js.map +1 -0
- package/dist/cjs/types/JobLogger.d.ts +111 -0
- package/dist/cjs/types/JobLogger.js +3 -0
- package/dist/cjs/types/JobLogger.js.map +1 -0
- package/dist/cjs/types/JobParameters.d.ts +106 -0
- package/dist/cjs/types/JobParameters.js +21 -0
- package/dist/cjs/types/JobParameters.js.map +1 -0
- package/dist/cjs/types/JobQuery.d.ts +79 -0
- package/dist/cjs/types/JobQuery.js +36 -0
- package/dist/cjs/types/JobQuery.js.map +1 -0
- package/dist/cjs/types/JobRepository.d.ts +101 -0
- package/dist/cjs/types/JobRepository.js +3 -0
- package/dist/cjs/types/JobRepository.js.map +1 -0
- package/dist/cjs/types/NotificationChannel.d.ts +120 -0
- package/dist/cjs/types/NotificationChannel.js +3 -0
- package/dist/cjs/types/NotificationChannel.js.map +1 -0
- package/dist/cjs/utils/backoff.d.ts +152 -0
- package/dist/cjs/utils/backoff.js +175 -0
- package/dist/cjs/utils/backoff.js.map +1 -0
- package/dist/cjs/utils/dateConstraints.d.ts +48 -0
- package/dist/cjs/utils/dateConstraints.js +127 -0
- package/dist/cjs/utils/dateConstraints.js.map +1 -0
- package/dist/cjs/utils/isValidDate.d.ts +1 -0
- package/dist/cjs/utils/isValidDate.js +8 -0
- package/dist/cjs/utils/isValidDate.js.map +1 -0
- package/dist/cjs/utils/nextRunAt.d.ts +10 -0
- package/dist/cjs/utils/nextRunAt.js +119 -0
- package/dist/cjs/utils/nextRunAt.js.map +1 -0
- package/dist/cjs/utils/priority.d.ts +13 -0
- package/dist/cjs/utils/priority.js +23 -0
- package/dist/cjs/utils/priority.js.map +1 -0
- package/dist/cjs/utils/processEvery.d.ts +1 -0
- package/dist/cjs/utils/processEvery.js +13 -0
- package/dist/cjs/utils/processEvery.js.map +1 -0
- package/dist/cjs/utils/stack.d.ts +1 -0
- package/dist/cjs/utils/stack.js +21 -0
- package/dist/cjs/utils/stack.js.map +1 -0
- package/dist/cjs/version.d.ts +9 -0
- package/dist/cjs/version.js +13 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/decorators/Define.d.ts +33 -0
- package/dist/decorators/Define.js +44 -0
- package/dist/decorators/Define.js.map +1 -0
- package/dist/decorators/Every.d.ts +43 -0
- package/dist/decorators/Every.js +55 -0
- package/dist/decorators/Every.js.map +1 -0
- package/dist/decorators/JobsController.d.ts +43 -0
- package/dist/decorators/JobsController.js +37 -0
- package/dist/decorators/JobsController.js.map +1 -0
- package/dist/decorators/index.d.ts +41 -0
- package/dist/decorators/index.js +46 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/metadata.d.ts +50 -0
- package/dist/decorators/metadata.js +35 -0
- package/dist/decorators/metadata.js.map +1 -0
- package/dist/decorators/register.d.ts +52 -0
- package/dist/decorators/register.js +139 -0
- package/dist/decorators/register.js.map +1 -0
- package/dist/index.d.ts +498 -0
- package/dist/index.js +744 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/BaseNotificationChannel.d.ts +47 -0
- package/dist/notifications/BaseNotificationChannel.js +108 -0
- package/dist/notifications/BaseNotificationChannel.js.map +1 -0
- package/dist/notifications/InMemoryNotificationChannel.d.ts +13 -0
- package/dist/notifications/InMemoryNotificationChannel.js +33 -0
- package/dist/notifications/InMemoryNotificationChannel.js.map +1 -0
- package/dist/notifications/index.d.ts +2 -0
- package/dist/notifications/index.js +3 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/types/AgendaBackend.d.ts +61 -0
- package/dist/types/AgendaBackend.js +2 -0
- package/dist/types/AgendaBackend.js.map +1 -0
- package/dist/types/AgendaConfig.d.ts +10 -0
- package/dist/types/AgendaConfig.js +2 -0
- package/dist/types/AgendaConfig.js.map +1 -0
- package/dist/types/AgendaStatus.d.ts +32 -0
- package/dist/types/AgendaStatus.js +2 -0
- package/dist/types/AgendaStatus.js.map +1 -0
- package/dist/types/DbOptions.d.ts +4 -0
- package/dist/types/DbOptions.js +2 -0
- package/dist/types/DbOptions.js.map +1 -0
- package/dist/types/DrainOptions.d.ts +37 -0
- package/dist/types/DrainOptions.js +2 -0
- package/dist/types/DrainOptions.js.map +1 -0
- package/dist/types/JobDefinition.d.ts +49 -0
- package/dist/types/JobDefinition.js +2 -0
- package/dist/types/JobDefinition.js.map +1 -0
- package/dist/types/JobLogger.d.ts +111 -0
- package/dist/types/JobLogger.js +2 -0
- package/dist/types/JobLogger.js.map +1 -0
- package/dist/types/JobParameters.d.ts +106 -0
- package/dist/types/JobParameters.js +17 -0
- package/dist/types/JobParameters.js.map +1 -0
- package/dist/types/JobQuery.d.ts +79 -0
- package/dist/types/JobQuery.js +33 -0
- package/dist/types/JobQuery.js.map +1 -0
- package/dist/types/JobRepository.d.ts +101 -0
- package/dist/types/JobRepository.js +2 -0
- package/dist/types/JobRepository.js.map +1 -0
- package/dist/types/NotificationChannel.d.ts +120 -0
- package/dist/types/NotificationChannel.js +2 -0
- package/dist/types/NotificationChannel.js.map +1 -0
- package/dist/utils/backoff.d.ts +152 -0
- package/dist/utils/backoff.js +167 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/dateConstraints.d.ts +48 -0
- package/dist/utils/dateConstraints.js +117 -0
- package/dist/utils/dateConstraints.js.map +1 -0
- package/dist/utils/isValidDate.d.ts +1 -0
- package/dist/utils/isValidDate.js +5 -0
- package/dist/utils/isValidDate.js.map +1 -0
- package/dist/utils/nextRunAt.d.ts +10 -0
- package/dist/utils/nextRunAt.js +110 -0
- package/dist/utils/nextRunAt.js.map +1 -0
- package/dist/utils/priority.d.ts +13 -0
- package/dist/utils/priority.js +20 -0
- package/dist/utils/priority.js.map +1 -0
- package/dist/utils/processEvery.d.ts +1 -0
- package/dist/utils/processEvery.js +7 -0
- package/dist/utils/processEvery.js.map +1 -0
- package/dist/utils/stack.d.ts +1 -0
- package/dist/utils/stack.js +18 -0
- package/dist/utils/stack.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +79 -0
- package/test/fixtures/forkHelper.js +40 -0
- package/test/fixtures/someJobDefinition.ts +12 -0
- package/test/shared/agenda-test-suite.ts +1876 -0
- package/test/shared/backoff-test-suite.ts +408 -0
- package/test/shared/debounce-test-suite.ts +377 -0
- package/test/shared/full-test-suite.ts +161 -0
- package/test/shared/index.ts +50 -0
- package/test/shared/joblogger-test-suite.ts +305 -0
- package/test/shared/jobprocessor-test-suite.ts +266 -0
- package/test/shared/notification-channel-test-suite.ts +471 -0
- package/test/shared/removeoncomplete-test-suite.ts +228 -0
- package/test/shared/repository-test-suite.ts +708 -0
- package/test/shared/retry-test-suite.ts +174 -0
- package/test/shared/test-utils.ts +100 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { Job } from './Job.js';
|
|
4
|
+
import { parsePriority } from './utils/priority.js';
|
|
5
|
+
import { JobProcessor } from './JobProcessor.js';
|
|
6
|
+
import { calculateProcessEvery } from './utils/processEvery.js';
|
|
7
|
+
import { getCallerFilePath } from './utils/stack.js';
|
|
8
|
+
const log = debug('agenda');
|
|
9
|
+
const DefaultOptions = {
|
|
10
|
+
processEvery: 5000,
|
|
11
|
+
defaultConcurrency: 5,
|
|
12
|
+
maxConcurrency: 20,
|
|
13
|
+
defaultLockLimit: 0,
|
|
14
|
+
lockLimit: 0,
|
|
15
|
+
defaultLockLifetime: 10 * 60 * 1000,
|
|
16
|
+
forkHelper: { path: 'dist/childWorker.js' },
|
|
17
|
+
removeOnComplete: false
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* @class
|
|
21
|
+
*/
|
|
22
|
+
export class Agenda extends EventEmitter {
|
|
23
|
+
// Implementation (eslint-disable needed because overloads provide the public type safety)
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
on(event, listener) {
|
|
26
|
+
if (this.forkedWorker && event !== 'ready' && event !== 'error') {
|
|
27
|
+
const warning = new Error(`calling on(${event}) during a forkedWorker has no effect!`);
|
|
28
|
+
log('WARNING: %s %s', warning.message, warning.stack);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
return super.on(event, listener);
|
|
32
|
+
}
|
|
33
|
+
isActiveJobProcessor() {
|
|
34
|
+
return !!this.jobProcessor;
|
|
35
|
+
}
|
|
36
|
+
async runForkedJob(jobId) {
|
|
37
|
+
const jobData = await this.db.getJobById(jobId);
|
|
38
|
+
if (!jobData) {
|
|
39
|
+
throw new Error('db entry not found');
|
|
40
|
+
}
|
|
41
|
+
const job = new Job(this, jobData);
|
|
42
|
+
await job.runJob();
|
|
43
|
+
}
|
|
44
|
+
async getRunningStats(fullDetails = false) {
|
|
45
|
+
if (!this.jobProcessor) {
|
|
46
|
+
throw new Error('agenda not running!');
|
|
47
|
+
}
|
|
48
|
+
const status = await this.jobProcessor.getStatus(fullDetails);
|
|
49
|
+
// Add backend info (stored in Agenda, not JobProcessor)
|
|
50
|
+
status.backend = {
|
|
51
|
+
name: this.backend.name,
|
|
52
|
+
hasNotificationChannel: this.hasNotificationChannel()
|
|
53
|
+
};
|
|
54
|
+
return status;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @param config - Agenda configuration with backend
|
|
58
|
+
* @param cb - Optional callback after Agenda is ready
|
|
59
|
+
*/
|
|
60
|
+
constructor(config, cb) {
|
|
61
|
+
super();
|
|
62
|
+
this.definitions = {};
|
|
63
|
+
// Prevent unhandled 'error' events from crashing the process.
|
|
64
|
+
// Users can still attach their own 'error' listener to handle errors.
|
|
65
|
+
this.on('error', () => { });
|
|
66
|
+
this.attrs = {
|
|
67
|
+
name: config.name || '',
|
|
68
|
+
processEvery: calculateProcessEvery(config.processEvery) || DefaultOptions.processEvery,
|
|
69
|
+
defaultConcurrency: config.defaultConcurrency || DefaultOptions.defaultConcurrency,
|
|
70
|
+
maxConcurrency: config.maxConcurrency || DefaultOptions.maxConcurrency,
|
|
71
|
+
defaultLockLimit: config.defaultLockLimit || DefaultOptions.defaultLockLimit,
|
|
72
|
+
lockLimit: config.lockLimit || DefaultOptions.lockLimit,
|
|
73
|
+
defaultLockLifetime: config.defaultLockLifetime || DefaultOptions.defaultLockLifetime,
|
|
74
|
+
removeOnComplete: config.removeOnComplete ?? DefaultOptions.removeOnComplete
|
|
75
|
+
};
|
|
76
|
+
this.forkedWorker = config.forkedWorker;
|
|
77
|
+
this.forkHelper = config.forkHelper;
|
|
78
|
+
// Initialize persistent job logger: disabled by default
|
|
79
|
+
if (config.logging === true) {
|
|
80
|
+
// Use backend's built-in logger, all jobs logged
|
|
81
|
+
this.jobLogger = config.backend.logger;
|
|
82
|
+
this.loggingDefault = true;
|
|
83
|
+
}
|
|
84
|
+
else if (config.logging && typeof config.logging === 'object' && 'log' in config.logging) {
|
|
85
|
+
// Passed a JobLogger instance directly — all jobs logged
|
|
86
|
+
this.jobLogger = config.logging;
|
|
87
|
+
this.loggingDefault = true;
|
|
88
|
+
}
|
|
89
|
+
else if (config.logging && typeof config.logging === 'object') {
|
|
90
|
+
// Options object: { logger?, default? }
|
|
91
|
+
const opts = config.logging;
|
|
92
|
+
this.jobLogger = opts.logger ?? config.backend.logger;
|
|
93
|
+
this.loggingDefault = opts.default ?? true;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Logging disabled (false / omitted)
|
|
97
|
+
this.loggingDefault = true;
|
|
98
|
+
}
|
|
99
|
+
// Store backend and get repository
|
|
100
|
+
this.backend = config.backend;
|
|
101
|
+
this.db = config.backend.repository;
|
|
102
|
+
// Notification channel priority: explicit config > backend's channel
|
|
103
|
+
this.notificationChannel = config.notificationChannel ?? config.backend.notificationChannel;
|
|
104
|
+
// Ready promise settles when the backend connection attempt finishes
|
|
105
|
+
this.ready = new Promise((resolve, reject) => {
|
|
106
|
+
this.once('ready', resolve);
|
|
107
|
+
// Connect the backend. A failed connection rejects `ready` (and
|
|
108
|
+
// emits 'error' when someone listens) instead of leaving an
|
|
109
|
+
// unhandled rejection and a forever-pending ready promise.
|
|
110
|
+
this.backend.connect().then(() => this.emit('ready'), (error) => {
|
|
111
|
+
log('backend connection failed', error);
|
|
112
|
+
if (this.listenerCount('error') > 0) {
|
|
113
|
+
this.emit('error', error);
|
|
114
|
+
}
|
|
115
|
+
reject(error);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
if (cb) {
|
|
119
|
+
// cb is only invoked on success; the ready rejection is already
|
|
120
|
+
// surfaced above, so avoid a second unhandled rejection here.
|
|
121
|
+
this.ready.then(() => cb(), () => { });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Cancels any jobs matching the passed options, and removes them from the database.
|
|
126
|
+
* @param options Options for which jobs to cancel
|
|
127
|
+
*/
|
|
128
|
+
async cancel(options) {
|
|
129
|
+
log('attempting to cancel all Agenda jobs', options);
|
|
130
|
+
await this.ready;
|
|
131
|
+
try {
|
|
132
|
+
const amountOfRemovedJobs = await this.db.removeJobs(options);
|
|
133
|
+
log('%s jobs cancelled', amountOfRemovedJobs);
|
|
134
|
+
return amountOfRemovedJobs;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
log('error trying to delete jobs from database');
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Disables any jobs matching the passed options, preventing them from being run.
|
|
143
|
+
* @param options Options for which jobs to disable
|
|
144
|
+
* @returns Number of jobs disabled
|
|
145
|
+
*/
|
|
146
|
+
async disable(options) {
|
|
147
|
+
log('attempting to disable Agenda jobs', options);
|
|
148
|
+
await this.ready;
|
|
149
|
+
try {
|
|
150
|
+
const numDisabled = await this.db.disableJobs(options);
|
|
151
|
+
log('%s jobs disabled', numDisabled);
|
|
152
|
+
return numDisabled;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
log('error trying to disable jobs');
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Enables any jobs matching the passed options, allowing them to be run.
|
|
161
|
+
* @param options Options for which jobs to enable
|
|
162
|
+
* @returns Number of jobs enabled
|
|
163
|
+
*/
|
|
164
|
+
async enable(options) {
|
|
165
|
+
log('attempting to enable Agenda jobs', options);
|
|
166
|
+
await this.ready;
|
|
167
|
+
try {
|
|
168
|
+
const numEnabled = await this.db.enableJobs(options);
|
|
169
|
+
log('%s jobs enabled', numEnabled);
|
|
170
|
+
return numEnabled;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
log('error trying to enable jobs');
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Set name of queue
|
|
179
|
+
* @param name
|
|
180
|
+
*/
|
|
181
|
+
name(name) {
|
|
182
|
+
log('Agenda.name(%s)', name);
|
|
183
|
+
this.attrs.name = name;
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Set the time how often the job processor checks for new jobs to process
|
|
188
|
+
* @param time
|
|
189
|
+
*/
|
|
190
|
+
processEvery(time) {
|
|
191
|
+
if (this.jobProcessor) {
|
|
192
|
+
throw new Error('job processor is already running, you need to set processEvery before calling start');
|
|
193
|
+
}
|
|
194
|
+
log('Agenda.processEvery(%d)', time);
|
|
195
|
+
this.attrs.processEvery = calculateProcessEvery(time);
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Set the concurrency for jobs (globally), type does not matter
|
|
200
|
+
* @param num
|
|
201
|
+
*/
|
|
202
|
+
maxConcurrency(num) {
|
|
203
|
+
log('Agenda.maxConcurrency(%d)', num);
|
|
204
|
+
this.attrs.maxConcurrency = num;
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Set the default concurrency for each job
|
|
209
|
+
* @param num number of max concurrency
|
|
210
|
+
*/
|
|
211
|
+
defaultConcurrency(num) {
|
|
212
|
+
log('Agenda.defaultConcurrency(%d)', num);
|
|
213
|
+
this.attrs.defaultConcurrency = num;
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Set the default amount jobs that are allowed to be locked at one time (GLOBAL)
|
|
218
|
+
* @param num
|
|
219
|
+
*/
|
|
220
|
+
lockLimit(num) {
|
|
221
|
+
log('Agenda.lockLimit(%d)', num);
|
|
222
|
+
this.attrs.lockLimit = num;
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Set default lock limit per job type
|
|
227
|
+
* @param num
|
|
228
|
+
*/
|
|
229
|
+
defaultLockLimit(num) {
|
|
230
|
+
log('Agenda.defaultLockLimit(%d)', num);
|
|
231
|
+
this.attrs.defaultLockLimit = num;
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Set the default lock time (in ms)
|
|
236
|
+
* Default is 10 * 60 * 1000 ms (10 minutes)
|
|
237
|
+
* @param ms
|
|
238
|
+
*/
|
|
239
|
+
defaultLockLifetime(ms) {
|
|
240
|
+
log('Agenda.defaultLockLifetime(%d)', ms);
|
|
241
|
+
this.attrs.defaultLockLifetime = ms;
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Set a notification channel for real-time job notifications
|
|
246
|
+
* @param channel - The notification channel implementation
|
|
247
|
+
*/
|
|
248
|
+
notifyVia(channel) {
|
|
249
|
+
if (this.jobProcessor) {
|
|
250
|
+
throw new Error('job processor is already running, you need to set notificationChannel before calling start');
|
|
251
|
+
}
|
|
252
|
+
log('Agenda.notifyVia([NotificationChannel])');
|
|
253
|
+
this.notificationChannel = channel;
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Check if a notification channel is configured
|
|
258
|
+
*/
|
|
259
|
+
hasNotificationChannel() {
|
|
260
|
+
return !!this.notificationChannel;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Check if persistent job logging is enabled
|
|
264
|
+
*/
|
|
265
|
+
hasJobLogger() {
|
|
266
|
+
return !!this.jobLogger;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Log a job lifecycle event to the persistent job logger (fire-and-forget).
|
|
270
|
+
* Does nothing if no job logger is configured or if logging is disabled
|
|
271
|
+
* for the job definition (respects per-definition `logging` and the global `loggingDefault`).
|
|
272
|
+
* @internal
|
|
273
|
+
*/
|
|
274
|
+
logJobEvent(entry) {
|
|
275
|
+
if (!this.jobLogger)
|
|
276
|
+
return;
|
|
277
|
+
// Per-definition logging overrides the global default.
|
|
278
|
+
// If definition.logging is undefined, fall back to loggingDefault.
|
|
279
|
+
const definition = this.definitions[entry.jobName];
|
|
280
|
+
const enabled = definition?.logging ?? this.loggingDefault;
|
|
281
|
+
if (!enabled)
|
|
282
|
+
return;
|
|
283
|
+
this.jobLogger
|
|
284
|
+
.log({
|
|
285
|
+
...entry,
|
|
286
|
+
timestamp: new Date(),
|
|
287
|
+
agendaName: this.attrs.name || undefined
|
|
288
|
+
})
|
|
289
|
+
.catch(err => {
|
|
290
|
+
log('failed to write job log entry: %O', err);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Query persistent job logs.
|
|
295
|
+
* Returns empty result if no job logger is configured.
|
|
296
|
+
*/
|
|
297
|
+
async getLogs(query) {
|
|
298
|
+
if (!this.jobLogger) {
|
|
299
|
+
return { entries: [], total: 0 };
|
|
300
|
+
}
|
|
301
|
+
return this.jobLogger.getLogs(query);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Clear persistent job logs matching the query.
|
|
305
|
+
* Returns 0 if no job logger is configured.
|
|
306
|
+
*/
|
|
307
|
+
async clearLogs(query) {
|
|
308
|
+
if (!this.jobLogger) {
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
return this.jobLogger.clearLogs(query);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Publish a job notification to the notification channel
|
|
315
|
+
* @internal
|
|
316
|
+
*/
|
|
317
|
+
async publishJobNotification(job) {
|
|
318
|
+
if (!this.notificationChannel || this.notificationChannel.state !== 'connected') {
|
|
319
|
+
// Channel not configured or not connected - skip publishing
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const notification = {
|
|
323
|
+
jobId: job.attrs._id,
|
|
324
|
+
jobName: job.attrs.name,
|
|
325
|
+
nextRunAt: job.attrs.nextRunAt,
|
|
326
|
+
priority: job.attrs.priority,
|
|
327
|
+
timestamp: new Date(),
|
|
328
|
+
source: this.attrs.name || undefined
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
await this.notificationChannel.publish(notification);
|
|
332
|
+
log('published job notification for [%s:%s]', job.attrs.name, job.attrs._id);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
log('failed to publish job notification for [%s:%s]', job.attrs.name, job.attrs._id, error);
|
|
336
|
+
this.emit('error', error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Publish a job state notification to the notification channel.
|
|
341
|
+
* This enables bi-directional communication so that all subscribers
|
|
342
|
+
* can receive job lifecycle events (start, success, fail, complete, etc.)
|
|
343
|
+
* @internal
|
|
344
|
+
*/
|
|
345
|
+
publishJobStateNotification(job, type, extra) {
|
|
346
|
+
if (!this.notificationChannel ||
|
|
347
|
+
this.notificationChannel.state !== 'connected' ||
|
|
348
|
+
!this.notificationChannel.publishState) {
|
|
349
|
+
// Channel not configured, not connected, or doesn't support state notifications
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const notification = {
|
|
353
|
+
type,
|
|
354
|
+
jobId: job.attrs._id,
|
|
355
|
+
jobName: job.attrs.name,
|
|
356
|
+
timestamp: new Date(),
|
|
357
|
+
source: this.attrs.name || undefined,
|
|
358
|
+
...extra
|
|
359
|
+
};
|
|
360
|
+
// fire+forget
|
|
361
|
+
this.notificationChannel
|
|
362
|
+
.publishState(notification)
|
|
363
|
+
.then(() => {
|
|
364
|
+
log('published job state notification [%s] for [%s:%s]', type, job.attrs.name, job.attrs._id);
|
|
365
|
+
})
|
|
366
|
+
.catch(error => {
|
|
367
|
+
log('failed to publish job state notification [%s] for [%s:%s]', type, job.attrs.name, job.attrs._id, error);
|
|
368
|
+
this.emit('error', error);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Subscribe to state notifications and re-emit them as regular events.
|
|
373
|
+
* This allows `agenda.on('success:jobName', ...)` to work across processes.
|
|
374
|
+
* @internal
|
|
375
|
+
*/
|
|
376
|
+
subscribeToStateNotifications() {
|
|
377
|
+
if (!this.notificationChannel?.subscribeState) {
|
|
378
|
+
return () => { };
|
|
379
|
+
}
|
|
380
|
+
return this.notificationChannel.subscribeState(notification => {
|
|
381
|
+
// Skip events from our own instance to avoid double-firing
|
|
382
|
+
// (we already emit locally in Job.run())
|
|
383
|
+
// Handle both empty strings and undefined as "same source" when both are falsy
|
|
384
|
+
const notificationSource = notification.source || '';
|
|
385
|
+
const ourSource = this.attrs.name || '';
|
|
386
|
+
if (notificationSource === ourSource) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
log('received remote state notification [%s] for [%s:%s]', notification.type, notification.jobName, notification.jobId);
|
|
390
|
+
// Use nextTick to ensure local events are processed first
|
|
391
|
+
// This prevents race conditions where remote events arrive before local processing completes
|
|
392
|
+
process.nextTick(() => {
|
|
393
|
+
// Re-emit as regular events with remote=true flag
|
|
394
|
+
// For 'fail' events, the first argument is the error
|
|
395
|
+
if (notification.type === 'fail') {
|
|
396
|
+
this.emit('fail', notification.error, notification, true);
|
|
397
|
+
this.emit(`fail:${notification.jobName}`, notification.error, notification, true);
|
|
398
|
+
}
|
|
399
|
+
else if (notification.type === 'retry') {
|
|
400
|
+
// For 'retry' events, pass retry details as second argument
|
|
401
|
+
const retryDetails = {
|
|
402
|
+
attempt: notification.retryAttempt || 1,
|
|
403
|
+
delay: 0, // Not available in notification
|
|
404
|
+
nextRunAt: notification.retryAt || new Date(),
|
|
405
|
+
error: new Error(notification.error || 'Unknown error')
|
|
406
|
+
};
|
|
407
|
+
this.emit('retry', notification, retryDetails, true);
|
|
408
|
+
this.emit(`retry:${notification.jobName}`, notification, retryDetails, true);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
this.emit(notification.type, notification, true);
|
|
412
|
+
this.emit(`${notification.type}:${notification.jobName}`, notification, true);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Query jobs with database-agnostic options.
|
|
419
|
+
* Returns jobs with computed states and supports filtering by state.
|
|
420
|
+
*
|
|
421
|
+
* @param options - Query options (name, state, search, pagination)
|
|
422
|
+
* @returns Jobs with computed states and total count
|
|
423
|
+
*/
|
|
424
|
+
async queryJobs(options) {
|
|
425
|
+
await this.ready;
|
|
426
|
+
return this.db.queryJobs(options);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get overview statistics for jobs grouped by name.
|
|
430
|
+
* Returns counts of jobs in each state for each job name.
|
|
431
|
+
*
|
|
432
|
+
* @returns Array of job overviews with state counts
|
|
433
|
+
*/
|
|
434
|
+
async getJobsOverview() {
|
|
435
|
+
await this.ready;
|
|
436
|
+
return this.db.getJobsOverview();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Removes all orphaned jobs (jobs whose names are not currently defined).
|
|
440
|
+
* @note: Only use after defining your jobs
|
|
441
|
+
*/
|
|
442
|
+
async purge() {
|
|
443
|
+
const definedNames = Object.keys(this.definitions);
|
|
444
|
+
log('Agenda.purge(%o)', definedNames);
|
|
445
|
+
return this.cancel({ notNames: definedNames });
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Removes ALL jobs from the database unconditionally.
|
|
449
|
+
* Use with caution — this cannot be undone.
|
|
450
|
+
* @returns Number of jobs removed
|
|
451
|
+
*/
|
|
452
|
+
async cancelAll() {
|
|
453
|
+
log('Agenda.cancelAll()');
|
|
454
|
+
await this.ready;
|
|
455
|
+
try {
|
|
456
|
+
const removed = await this.db.purgeAllJobs();
|
|
457
|
+
log('%s jobs cancelled', removed);
|
|
458
|
+
return removed;
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
log('error trying to cancel all jobs from database');
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
define(name, processor, options) {
|
|
466
|
+
if (this.definitions[name]) {
|
|
467
|
+
log('overwriting already defined agenda job', name);
|
|
468
|
+
}
|
|
469
|
+
const filePath = getCallerFilePath();
|
|
470
|
+
this.definitions[name] = {
|
|
471
|
+
fn: processor,
|
|
472
|
+
filePath,
|
|
473
|
+
concurrency: options?.concurrency || this.attrs.defaultConcurrency,
|
|
474
|
+
lockLimit: options?.lockLimit || this.attrs.defaultLockLimit,
|
|
475
|
+
priority: parsePriority(options?.priority),
|
|
476
|
+
lockLifetime: options?.lockLifetime || this.attrs.defaultLockLifetime,
|
|
477
|
+
backoff: options?.backoff,
|
|
478
|
+
removeOnComplete: options?.removeOnComplete ?? this.attrs.removeOnComplete,
|
|
479
|
+
logging: options?.logging
|
|
480
|
+
};
|
|
481
|
+
log('job [%s] defined with following options: \n%O', name, this.definitions[name]);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Internal helper method that uses createJob to create jobs for an array of names
|
|
485
|
+
* @param names - Job names to schedule
|
|
486
|
+
* @param createJob - Factory function to create each job
|
|
487
|
+
* @returns Array of created jobs
|
|
488
|
+
*/
|
|
489
|
+
async createJobs(names, createJob) {
|
|
490
|
+
try {
|
|
491
|
+
const jobs = await Promise.all(names.map(name => createJob(name)));
|
|
492
|
+
log('createJobs() -> all jobs created successfully');
|
|
493
|
+
return jobs;
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
log('createJobs() -> error creating one or more of the jobs', error);
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
501
|
+
create(name, data) {
|
|
502
|
+
log('Agenda.create(%s, [Object])', name);
|
|
503
|
+
const priority = this.definitions[name] ? this.definitions[name].priority : 0;
|
|
504
|
+
return new Job(this, { name, data, type: 'normal', priority });
|
|
505
|
+
}
|
|
506
|
+
async every(interval, names, data, options) {
|
|
507
|
+
log('Agenda.every(%s, %O, %O)', interval, names, options);
|
|
508
|
+
/** Internal method to setup job that gets run every interval */
|
|
509
|
+
const createJob = async (name) => {
|
|
510
|
+
const job = this.create(name, data);
|
|
511
|
+
job.attrs.type = 'single';
|
|
512
|
+
// Apply date constraints before repeatEvery (so they're used in nextRunAt computation)
|
|
513
|
+
if (options?.startDate) {
|
|
514
|
+
job.startDate(options.startDate);
|
|
515
|
+
}
|
|
516
|
+
if (options?.endDate) {
|
|
517
|
+
job.endDate(options.endDate);
|
|
518
|
+
}
|
|
519
|
+
if (options?.skipDays) {
|
|
520
|
+
job.skipDays(options.skipDays);
|
|
521
|
+
}
|
|
522
|
+
job.repeatEvery(interval, options);
|
|
523
|
+
if (options?.forkMode) {
|
|
524
|
+
job.forkMode(options.forkMode);
|
|
525
|
+
}
|
|
526
|
+
await job.save();
|
|
527
|
+
return job;
|
|
528
|
+
};
|
|
529
|
+
if (typeof names === 'string') {
|
|
530
|
+
return createJob(names);
|
|
531
|
+
}
|
|
532
|
+
log('Agenda.every(%s, %s, %O)', interval, names, options);
|
|
533
|
+
return this.createJobs(names, createJob);
|
|
534
|
+
}
|
|
535
|
+
async schedule(when, names, data, options) {
|
|
536
|
+
const createJob = async (name) => {
|
|
537
|
+
const job = this.create(name, data);
|
|
538
|
+
// Apply date constraints before scheduling
|
|
539
|
+
if (options?.startDate) {
|
|
540
|
+
job.startDate(options.startDate);
|
|
541
|
+
}
|
|
542
|
+
if (options?.endDate) {
|
|
543
|
+
job.endDate(options.endDate);
|
|
544
|
+
}
|
|
545
|
+
if (options?.skipDays) {
|
|
546
|
+
job.skipDays(options.skipDays);
|
|
547
|
+
}
|
|
548
|
+
await job.schedule(when).save();
|
|
549
|
+
return job;
|
|
550
|
+
};
|
|
551
|
+
if (typeof names === 'string') {
|
|
552
|
+
log('Agenda.schedule(%s, %O, [%O])', when, names);
|
|
553
|
+
return createJob(names);
|
|
554
|
+
}
|
|
555
|
+
log('Agenda.schedule(%s, %O, [%O])', when, names);
|
|
556
|
+
return this.createJobs(names, createJob);
|
|
557
|
+
}
|
|
558
|
+
async now(name, data) {
|
|
559
|
+
log('Agenda.now(%s, [Object])', name);
|
|
560
|
+
try {
|
|
561
|
+
const job = this.create(name, data);
|
|
562
|
+
job.schedule(new Date());
|
|
563
|
+
await job.save();
|
|
564
|
+
return job;
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
log('error trying to create a job for this exact moment');
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Create a debounced job that combines rapid submissions into a single execution.
|
|
573
|
+
* Requires a unique key to identify which jobs should be debounced together.
|
|
574
|
+
*
|
|
575
|
+
* @param name - Job name
|
|
576
|
+
* @param data - Job data
|
|
577
|
+
* @param uniqueKey - Unique constraint to identify jobs (e.g., { 'data.userId': 123 })
|
|
578
|
+
* @param debounceMs - Debounce delay in milliseconds
|
|
579
|
+
* @param options - Optional debounce options (maxWait, strategy)
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* ```ts
|
|
583
|
+
* // Debounce search index updates by entity type
|
|
584
|
+
* await agenda.nowDebounced(
|
|
585
|
+
* 'updateSearchIndex',
|
|
586
|
+
* { entityType: 'products' },
|
|
587
|
+
* { 'data.entityType': 'products' },
|
|
588
|
+
* 2000
|
|
589
|
+
* );
|
|
590
|
+
*
|
|
591
|
+
* // With maxWait to guarantee execution within 30s
|
|
592
|
+
* await agenda.nowDebounced(
|
|
593
|
+
* 'syncUser',
|
|
594
|
+
* { userId: 123 },
|
|
595
|
+
* { 'data.userId': 123 },
|
|
596
|
+
* 5000,
|
|
597
|
+
* { maxWait: 30000 }
|
|
598
|
+
* );
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
async nowDebounced(name, data, uniqueKey, debounceMs, options) {
|
|
602
|
+
log('Agenda.nowDebounced(%s, [Object], %O, %d)', name, uniqueKey, debounceMs);
|
|
603
|
+
try {
|
|
604
|
+
const job = this.create(name, data);
|
|
605
|
+
job.schedule(new Date()).unique(uniqueKey).debounce(debounceMs, options);
|
|
606
|
+
await job.save();
|
|
607
|
+
return job;
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
log('error trying to create a debounced job');
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Starts processing jobs using processJobs() methods, storing an interval ID
|
|
616
|
+
* This method will only resolve if a db has been set up beforehand.
|
|
617
|
+
*/
|
|
618
|
+
async start() {
|
|
619
|
+
log('Agenda.start called, waiting for agenda to be initialized (db connection)', this.attrs.processEvery);
|
|
620
|
+
await this.ready;
|
|
621
|
+
if (this.jobProcessor) {
|
|
622
|
+
log('Agenda.start was already called, ignoring');
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// Connect notification channel if configured
|
|
626
|
+
if (this.notificationChannel) {
|
|
627
|
+
log('Agenda.start connecting notification channel');
|
|
628
|
+
await this.notificationChannel.connect();
|
|
629
|
+
// Subscribe to state notifications for cross-process event propagation
|
|
630
|
+
this.stateSubscriptionUnsubscribe = this.subscribeToStateNotifications();
|
|
631
|
+
}
|
|
632
|
+
this.jobProcessor = new JobProcessor(this, this.attrs.maxConcurrency, this.attrs.lockLimit, this.attrs.processEvery, this.notificationChannel);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Clear the interval that processes the jobs and unlocks all currently locked jobs
|
|
636
|
+
* @param closeConnection - Whether to close the database connection.
|
|
637
|
+
* Defaults to backend.ownsConnection (true if backend created the connection,
|
|
638
|
+
* false if connection was passed in by user).
|
|
639
|
+
*/
|
|
640
|
+
async stop(closeConnection) {
|
|
641
|
+
if (!this.jobProcessor) {
|
|
642
|
+
log('Agenda.stop called, but agenda has never started!');
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
log('Agenda.stop called, clearing interval for processJobs()');
|
|
646
|
+
const lockedJobs = this.jobProcessor.stop();
|
|
647
|
+
log('Agenda._unlockJobs()');
|
|
648
|
+
const jobIds = lockedJobs?.map(job => job.attrs._id) || [];
|
|
649
|
+
if (jobIds.length > 0) {
|
|
650
|
+
log('about to unlock jobs with ids: %O', jobIds);
|
|
651
|
+
await this.db.unlockJobs(jobIds);
|
|
652
|
+
}
|
|
653
|
+
// Unsubscribe from state notifications
|
|
654
|
+
if (this.stateSubscriptionUnsubscribe) {
|
|
655
|
+
this.stateSubscriptionUnsubscribe();
|
|
656
|
+
this.stateSubscriptionUnsubscribe = undefined;
|
|
657
|
+
}
|
|
658
|
+
// Disconnect notification channel if configured (we always disconnect since we connected it in start)
|
|
659
|
+
if (this.notificationChannel) {
|
|
660
|
+
log('Agenda.stop disconnecting notification channel');
|
|
661
|
+
await this.notificationChannel.disconnect();
|
|
662
|
+
}
|
|
663
|
+
// Determine whether to close backend connection
|
|
664
|
+
const shouldClose = closeConnection ?? this.backend.ownsConnection ?? true;
|
|
665
|
+
// Disconnect the backend
|
|
666
|
+
if (shouldClose) {
|
|
667
|
+
log('Agenda.stop disconnecting backend');
|
|
668
|
+
await this.backend.disconnect();
|
|
669
|
+
}
|
|
670
|
+
this.jobProcessor = undefined;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Waits for all currently running jobs to finish before stopping.
|
|
674
|
+
* This allows for a graceful shutdown where jobs complete their work.
|
|
675
|
+
* Unlike stop(), this method waits for running jobs to complete instead of unlocking them.
|
|
676
|
+
*
|
|
677
|
+
* @param options - Drain options or timeout in milliseconds
|
|
678
|
+
* - `timeout`: Maximum time to wait for jobs to complete
|
|
679
|
+
* - `signal`: AbortSignal to cancel the drain operation
|
|
680
|
+
* - `closeConnection`: Whether to close the database connection (default: backend.ownsConnection)
|
|
681
|
+
* @returns DrainResult with completion statistics
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* // Wait indefinitely for all jobs to complete
|
|
685
|
+
* await agenda.drain();
|
|
686
|
+
*
|
|
687
|
+
* @example
|
|
688
|
+
* // Wait up to 30 seconds
|
|
689
|
+
* const result = await agenda.drain(30000);
|
|
690
|
+
* if (result.timedOut) console.log(`${result.running} jobs still running`);
|
|
691
|
+
*
|
|
692
|
+
* @example
|
|
693
|
+
* // Use AbortSignal for external control
|
|
694
|
+
* const controller = new AbortController();
|
|
695
|
+
* setTimeout(() => controller.abort(), 30000);
|
|
696
|
+
* await agenda.drain({ signal: controller.signal });
|
|
697
|
+
*/
|
|
698
|
+
async drain(options) {
|
|
699
|
+
if (!this.jobProcessor) {
|
|
700
|
+
log('Agenda.drain called, but agenda has never started!');
|
|
701
|
+
return { completed: 0, running: 0, timedOut: false, aborted: false };
|
|
702
|
+
}
|
|
703
|
+
// Normalize options
|
|
704
|
+
const opts = typeof options === 'number' ? { timeout: options } : options ?? {};
|
|
705
|
+
log('Agenda.drain called, waiting for jobs to finish');
|
|
706
|
+
const result = await this.jobProcessor.drain(opts);
|
|
707
|
+
// Unsubscribe from state notifications
|
|
708
|
+
if (this.stateSubscriptionUnsubscribe) {
|
|
709
|
+
this.stateSubscriptionUnsubscribe();
|
|
710
|
+
this.stateSubscriptionUnsubscribe = undefined;
|
|
711
|
+
}
|
|
712
|
+
// Disconnect notification channel if configured (we always disconnect since we connected it in start)
|
|
713
|
+
if (this.notificationChannel) {
|
|
714
|
+
log('Agenda.drain disconnecting notification channel');
|
|
715
|
+
await this.notificationChannel.disconnect();
|
|
716
|
+
}
|
|
717
|
+
// Determine whether to close backend connection
|
|
718
|
+
const shouldClose = opts.closeConnection ?? this.backend.ownsConnection ?? true;
|
|
719
|
+
// Disconnect the backend
|
|
720
|
+
if (shouldClose) {
|
|
721
|
+
log('Agenda.drain disconnecting backend');
|
|
722
|
+
await this.backend.disconnect();
|
|
723
|
+
}
|
|
724
|
+
this.jobProcessor = undefined;
|
|
725
|
+
return result;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
export * from './types/AgendaConfig.js';
|
|
729
|
+
export * from './types/JobDefinition.js';
|
|
730
|
+
export * from './types/JobParameters.js';
|
|
731
|
+
export * from './types/DbOptions.js';
|
|
732
|
+
export * from './Job.js';
|
|
733
|
+
export * from './types/JobQuery.js';
|
|
734
|
+
export * from './types/JobRepository.js';
|
|
735
|
+
export * from './types/NotificationChannel.js';
|
|
736
|
+
export * from './types/AgendaBackend.js';
|
|
737
|
+
export * from './types/AgendaStatus.js';
|
|
738
|
+
export * from './types/DrainOptions.js';
|
|
739
|
+
export * from './notifications/index.js';
|
|
740
|
+
export * from './types/JobLogger.js';
|
|
741
|
+
export { applyAllDateConstraints, applyDateRangeConstraints, applySkipDays, shouldSkipDay, isWithinDateRange } from './utils/dateConstraints.js';
|
|
742
|
+
export * from './decorators/index.js';
|
|
743
|
+
export { backoffStrategies, constant, linear, exponential, combine, when } from './utils/backoff.js';
|
|
744
|
+
//# sourceMappingURL=index.js.map
|