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