@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.
Files changed (216) hide show
  1. package/LICENSE.md +25 -0
  2. package/README.md +380 -0
  3. package/dist/Job.d.ts +190 -0
  4. package/dist/Job.js +689 -0
  5. package/dist/Job.js.map +1 -0
  6. package/dist/JobProcessingQueue.d.ts +47 -0
  7. package/dist/JobProcessingQueue.js +107 -0
  8. package/dist/JobProcessingQueue.js.map +1 -0
  9. package/dist/JobProcessor.d.ts +72 -0
  10. package/dist/JobProcessor.js +512 -0
  11. package/dist/JobProcessor.js.map +1 -0
  12. package/dist/backends/index.d.ts +18 -0
  13. package/dist/backends/index.js +20 -0
  14. package/dist/backends/index.js.map +1 -0
  15. package/dist/cjs/Job.d.ts +190 -0
  16. package/dist/cjs/Job.js +696 -0
  17. package/dist/cjs/Job.js.map +1 -0
  18. package/dist/cjs/JobProcessingQueue.d.ts +47 -0
  19. package/dist/cjs/JobProcessingQueue.js +111 -0
  20. package/dist/cjs/JobProcessingQueue.js.map +1 -0
  21. package/dist/cjs/JobProcessor.d.ts +72 -0
  22. package/dist/cjs/JobProcessor.js +519 -0
  23. package/dist/cjs/JobProcessor.js.map +1 -0
  24. package/dist/cjs/backends/index.d.ts +17 -0
  25. package/dist/cjs/backends/index.js +20 -0
  26. package/dist/cjs/backends/index.js.map +1 -0
  27. package/dist/cjs/decorators/Define.d.ts +33 -0
  28. package/dist/cjs/decorators/Define.js +47 -0
  29. package/dist/cjs/decorators/Define.js.map +1 -0
  30. package/dist/cjs/decorators/Every.d.ts +43 -0
  31. package/dist/cjs/decorators/Every.js +58 -0
  32. package/dist/cjs/decorators/Every.js.map +1 -0
  33. package/dist/cjs/decorators/JobsController.d.ts +43 -0
  34. package/dist/cjs/decorators/JobsController.js +40 -0
  35. package/dist/cjs/decorators/JobsController.js.map +1 -0
  36. package/dist/cjs/decorators/index.d.ts +41 -0
  37. package/dist/cjs/decorators/index.js +57 -0
  38. package/dist/cjs/decorators/index.js.map +1 -0
  39. package/dist/cjs/decorators/metadata.d.ts +50 -0
  40. package/dist/cjs/decorators/metadata.js +41 -0
  41. package/dist/cjs/decorators/metadata.js.map +1 -0
  42. package/dist/cjs/decorators/register.d.ts +52 -0
  43. package/dist/cjs/decorators/register.js +145 -0
  44. package/dist/cjs/decorators/register.js.map +1 -0
  45. package/dist/cjs/index.d.ts +498 -0
  46. package/dist/cjs/index.js +776 -0
  47. package/dist/cjs/index.js.map +1 -0
  48. package/dist/cjs/notifications/BaseNotificationChannel.d.ts +47 -0
  49. package/dist/cjs/notifications/BaseNotificationChannel.js +112 -0
  50. package/dist/cjs/notifications/BaseNotificationChannel.js.map +1 -0
  51. package/dist/cjs/notifications/InMemoryNotificationChannel.d.ts +13 -0
  52. package/dist/cjs/notifications/InMemoryNotificationChannel.js +37 -0
  53. package/dist/cjs/notifications/InMemoryNotificationChannel.js.map +1 -0
  54. package/dist/cjs/notifications/index.d.ts +2 -0
  55. package/dist/cjs/notifications/index.js +8 -0
  56. package/dist/cjs/notifications/index.js.map +1 -0
  57. package/dist/cjs/package.json +1 -0
  58. package/dist/cjs/types/AgendaBackend.d.ts +61 -0
  59. package/dist/cjs/types/AgendaBackend.js +3 -0
  60. package/dist/cjs/types/AgendaBackend.js.map +1 -0
  61. package/dist/cjs/types/AgendaConfig.d.ts +10 -0
  62. package/dist/cjs/types/AgendaConfig.js +3 -0
  63. package/dist/cjs/types/AgendaConfig.js.map +1 -0
  64. package/dist/cjs/types/AgendaStatus.d.ts +32 -0
  65. package/dist/cjs/types/AgendaStatus.js +3 -0
  66. package/dist/cjs/types/AgendaStatus.js.map +1 -0
  67. package/dist/cjs/types/DbOptions.d.ts +4 -0
  68. package/dist/cjs/types/DbOptions.js +3 -0
  69. package/dist/cjs/types/DbOptions.js.map +1 -0
  70. package/dist/cjs/types/DrainOptions.d.ts +37 -0
  71. package/dist/cjs/types/DrainOptions.js +3 -0
  72. package/dist/cjs/types/DrainOptions.js.map +1 -0
  73. package/dist/cjs/types/JobDefinition.d.ts +49 -0
  74. package/dist/cjs/types/JobDefinition.js +3 -0
  75. package/dist/cjs/types/JobDefinition.js.map +1 -0
  76. package/dist/cjs/types/JobLogger.d.ts +111 -0
  77. package/dist/cjs/types/JobLogger.js +3 -0
  78. package/dist/cjs/types/JobLogger.js.map +1 -0
  79. package/dist/cjs/types/JobParameters.d.ts +106 -0
  80. package/dist/cjs/types/JobParameters.js +21 -0
  81. package/dist/cjs/types/JobParameters.js.map +1 -0
  82. package/dist/cjs/types/JobQuery.d.ts +79 -0
  83. package/dist/cjs/types/JobQuery.js +36 -0
  84. package/dist/cjs/types/JobQuery.js.map +1 -0
  85. package/dist/cjs/types/JobRepository.d.ts +101 -0
  86. package/dist/cjs/types/JobRepository.js +3 -0
  87. package/dist/cjs/types/JobRepository.js.map +1 -0
  88. package/dist/cjs/types/NotificationChannel.d.ts +120 -0
  89. package/dist/cjs/types/NotificationChannel.js +3 -0
  90. package/dist/cjs/types/NotificationChannel.js.map +1 -0
  91. package/dist/cjs/utils/backoff.d.ts +152 -0
  92. package/dist/cjs/utils/backoff.js +175 -0
  93. package/dist/cjs/utils/backoff.js.map +1 -0
  94. package/dist/cjs/utils/dateConstraints.d.ts +48 -0
  95. package/dist/cjs/utils/dateConstraints.js +127 -0
  96. package/dist/cjs/utils/dateConstraints.js.map +1 -0
  97. package/dist/cjs/utils/isValidDate.d.ts +1 -0
  98. package/dist/cjs/utils/isValidDate.js +8 -0
  99. package/dist/cjs/utils/isValidDate.js.map +1 -0
  100. package/dist/cjs/utils/nextRunAt.d.ts +10 -0
  101. package/dist/cjs/utils/nextRunAt.js +119 -0
  102. package/dist/cjs/utils/nextRunAt.js.map +1 -0
  103. package/dist/cjs/utils/priority.d.ts +13 -0
  104. package/dist/cjs/utils/priority.js +23 -0
  105. package/dist/cjs/utils/priority.js.map +1 -0
  106. package/dist/cjs/utils/processEvery.d.ts +1 -0
  107. package/dist/cjs/utils/processEvery.js +13 -0
  108. package/dist/cjs/utils/processEvery.js.map +1 -0
  109. package/dist/cjs/utils/stack.d.ts +1 -0
  110. package/dist/cjs/utils/stack.js +21 -0
  111. package/dist/cjs/utils/stack.js.map +1 -0
  112. package/dist/cjs/version.d.ts +9 -0
  113. package/dist/cjs/version.js +13 -0
  114. package/dist/cjs/version.js.map +1 -0
  115. package/dist/decorators/Define.d.ts +33 -0
  116. package/dist/decorators/Define.js +44 -0
  117. package/dist/decorators/Define.js.map +1 -0
  118. package/dist/decorators/Every.d.ts +43 -0
  119. package/dist/decorators/Every.js +55 -0
  120. package/dist/decorators/Every.js.map +1 -0
  121. package/dist/decorators/JobsController.d.ts +43 -0
  122. package/dist/decorators/JobsController.js +37 -0
  123. package/dist/decorators/JobsController.js.map +1 -0
  124. package/dist/decorators/index.d.ts +41 -0
  125. package/dist/decorators/index.js +46 -0
  126. package/dist/decorators/index.js.map +1 -0
  127. package/dist/decorators/metadata.d.ts +50 -0
  128. package/dist/decorators/metadata.js +35 -0
  129. package/dist/decorators/metadata.js.map +1 -0
  130. package/dist/decorators/register.d.ts +52 -0
  131. package/dist/decorators/register.js +139 -0
  132. package/dist/decorators/register.js.map +1 -0
  133. package/dist/index.d.ts +498 -0
  134. package/dist/index.js +744 -0
  135. package/dist/index.js.map +1 -0
  136. package/dist/notifications/BaseNotificationChannel.d.ts +47 -0
  137. package/dist/notifications/BaseNotificationChannel.js +108 -0
  138. package/dist/notifications/BaseNotificationChannel.js.map +1 -0
  139. package/dist/notifications/InMemoryNotificationChannel.d.ts +13 -0
  140. package/dist/notifications/InMemoryNotificationChannel.js +33 -0
  141. package/dist/notifications/InMemoryNotificationChannel.js.map +1 -0
  142. package/dist/notifications/index.d.ts +2 -0
  143. package/dist/notifications/index.js +3 -0
  144. package/dist/notifications/index.js.map +1 -0
  145. package/dist/types/AgendaBackend.d.ts +61 -0
  146. package/dist/types/AgendaBackend.js +2 -0
  147. package/dist/types/AgendaBackend.js.map +1 -0
  148. package/dist/types/AgendaConfig.d.ts +10 -0
  149. package/dist/types/AgendaConfig.js +2 -0
  150. package/dist/types/AgendaConfig.js.map +1 -0
  151. package/dist/types/AgendaStatus.d.ts +32 -0
  152. package/dist/types/AgendaStatus.js +2 -0
  153. package/dist/types/AgendaStatus.js.map +1 -0
  154. package/dist/types/DbOptions.d.ts +4 -0
  155. package/dist/types/DbOptions.js +2 -0
  156. package/dist/types/DbOptions.js.map +1 -0
  157. package/dist/types/DrainOptions.d.ts +37 -0
  158. package/dist/types/DrainOptions.js +2 -0
  159. package/dist/types/DrainOptions.js.map +1 -0
  160. package/dist/types/JobDefinition.d.ts +49 -0
  161. package/dist/types/JobDefinition.js +2 -0
  162. package/dist/types/JobDefinition.js.map +1 -0
  163. package/dist/types/JobLogger.d.ts +111 -0
  164. package/dist/types/JobLogger.js +2 -0
  165. package/dist/types/JobLogger.js.map +1 -0
  166. package/dist/types/JobParameters.d.ts +106 -0
  167. package/dist/types/JobParameters.js +17 -0
  168. package/dist/types/JobParameters.js.map +1 -0
  169. package/dist/types/JobQuery.d.ts +79 -0
  170. package/dist/types/JobQuery.js +33 -0
  171. package/dist/types/JobQuery.js.map +1 -0
  172. package/dist/types/JobRepository.d.ts +101 -0
  173. package/dist/types/JobRepository.js +2 -0
  174. package/dist/types/JobRepository.js.map +1 -0
  175. package/dist/types/NotificationChannel.d.ts +120 -0
  176. package/dist/types/NotificationChannel.js +2 -0
  177. package/dist/types/NotificationChannel.js.map +1 -0
  178. package/dist/utils/backoff.d.ts +152 -0
  179. package/dist/utils/backoff.js +167 -0
  180. package/dist/utils/backoff.js.map +1 -0
  181. package/dist/utils/dateConstraints.d.ts +48 -0
  182. package/dist/utils/dateConstraints.js +117 -0
  183. package/dist/utils/dateConstraints.js.map +1 -0
  184. package/dist/utils/isValidDate.d.ts +1 -0
  185. package/dist/utils/isValidDate.js +5 -0
  186. package/dist/utils/isValidDate.js.map +1 -0
  187. package/dist/utils/nextRunAt.d.ts +10 -0
  188. package/dist/utils/nextRunAt.js +110 -0
  189. package/dist/utils/nextRunAt.js.map +1 -0
  190. package/dist/utils/priority.d.ts +13 -0
  191. package/dist/utils/priority.js +20 -0
  192. package/dist/utils/priority.js.map +1 -0
  193. package/dist/utils/processEvery.d.ts +1 -0
  194. package/dist/utils/processEvery.js +7 -0
  195. package/dist/utils/processEvery.js.map +1 -0
  196. package/dist/utils/stack.d.ts +1 -0
  197. package/dist/utils/stack.js +18 -0
  198. package/dist/utils/stack.js.map +1 -0
  199. package/dist/version.d.ts +9 -0
  200. package/dist/version.js +10 -0
  201. package/dist/version.js.map +1 -0
  202. package/package.json +79 -0
  203. package/test/fixtures/forkHelper.js +40 -0
  204. package/test/fixtures/someJobDefinition.ts +12 -0
  205. package/test/shared/agenda-test-suite.ts +1876 -0
  206. package/test/shared/backoff-test-suite.ts +408 -0
  207. package/test/shared/debounce-test-suite.ts +377 -0
  208. package/test/shared/full-test-suite.ts +161 -0
  209. package/test/shared/index.ts +50 -0
  210. package/test/shared/joblogger-test-suite.ts +305 -0
  211. package/test/shared/jobprocessor-test-suite.ts +266 -0
  212. package/test/shared/notification-channel-test-suite.ts +471 -0
  213. package/test/shared/removeoncomplete-test-suite.ts +228 -0
  214. package/test/shared/repository-test-suite.ts +708 -0
  215. package/test/shared/retry-test-suite.ts +174 -0
  216. 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