@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/Job.js ADDED
@@ -0,0 +1,689 @@
1
+ import date from 'date.js';
2
+ import debug from 'debug';
3
+ import { fork } from 'child_process';
4
+ import { datefields } from './types/JobParameters.js';
5
+ import { parsePriority } from './utils/priority.js';
6
+ import { computeFromInterval, computeFromRepeatAt } from './utils/nextRunAt.js';
7
+ import { applyAllDateConstraints } from './utils/dateConstraints.js';
8
+ const log = debug('agenda:job');
9
+ /**
10
+ * @class
11
+ */
12
+ export class Job {
13
+ getCanceledMessage() {
14
+ return typeof this.canceled === 'object'
15
+ ? this.canceled?.message || this.canceled
16
+ : this.canceled;
17
+ }
18
+ cancel(error) {
19
+ this.canceled = error || true;
20
+ if (this.forkedChild) {
21
+ try {
22
+ this.forkedChild.send('cancel');
23
+ log('canceled child', this.attrs.name, this.attrs._id);
24
+ }
25
+ catch {
26
+ log('cannot send cancel to child');
27
+ }
28
+ }
29
+ }
30
+ constructor(agenda, args, byJobProcessor = false) {
31
+ this.agenda = agenda;
32
+ this.byJobProcessor = byJobProcessor;
33
+ this.attrs = {
34
+ ...args,
35
+ // Set defaults if undefined
36
+ nextRunAt: args.nextRunAt === undefined ? new Date() : args.nextRunAt,
37
+ priority: parsePriority(args.priority),
38
+ type: args.type
39
+ };
40
+ }
41
+ /**
42
+ * Given a job, turn it into an JobParameters object
43
+ * @param excludeProcessorFields - If true, excludes fields managed by the job processor
44
+ */
45
+ toJson(excludeProcessorFields = false) {
46
+ const result = {};
47
+ const attrs = this.attrs;
48
+ for (const key of Object.keys(attrs)) {
49
+ if (Object.hasOwnProperty.call(attrs, key)) {
50
+ // Skip processor-managed fields if requested
51
+ if (excludeProcessorFields && Job.PROCESSOR_MANAGED_FIELDS.has(key)) {
52
+ continue;
53
+ }
54
+ result[key] =
55
+ datefields.includes(key) && attrs[key]
56
+ ? new Date(attrs[key])
57
+ : attrs[key];
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+ /**
63
+ * Sets a job to repeat every X amount of time
64
+ * @param interval
65
+ * @param options
66
+ */
67
+ repeatEvery(interval, options = {}) {
68
+ this.attrs.repeatInterval = interval;
69
+ this.attrs.repeatTimezone = options.timezone;
70
+ if (options.skipImmediate) {
71
+ // Set the lastRunAt time to the nextRunAt so that the new nextRunAt will be computed in reference to the current value.
72
+ this.attrs.lastRunAt = this.attrs.nextRunAt || new Date();
73
+ this.computeNextRunAt();
74
+ this.attrs.lastRunAt = undefined;
75
+ }
76
+ else {
77
+ this.computeNextRunAt();
78
+ }
79
+ return this;
80
+ }
81
+ /**
82
+ * Sets a job to repeat at a specific time
83
+ * @param time
84
+ */
85
+ repeatAt(time) {
86
+ this.attrs.repeatAt = time;
87
+ return this;
88
+ }
89
+ /**
90
+ * Sets the start date for the job.
91
+ * The job will not run before this date.
92
+ * @param date - The start date (Date object or string parseable by Date constructor)
93
+ */
94
+ startDate(dateInput) {
95
+ const d = new Date(dateInput);
96
+ if (Number.isNaN(d.getTime())) {
97
+ throw new Error(`Invalid start date: ${dateInput}`);
98
+ }
99
+ this.attrs.startDate = d;
100
+ return this;
101
+ }
102
+ /**
103
+ * Sets the end date for the job.
104
+ * The job will not run after this date (nextRunAt will be set to null).
105
+ * @param date - The end date (Date object or string parseable by Date constructor)
106
+ */
107
+ endDate(dateInput) {
108
+ const d = new Date(dateInput);
109
+ if (Number.isNaN(d.getTime())) {
110
+ throw new Error(`Invalid end date: ${dateInput}`);
111
+ }
112
+ this.attrs.endDate = d;
113
+ return this;
114
+ }
115
+ /**
116
+ * Sets the days of the week to skip.
117
+ * The job will not run on these days.
118
+ * @param days - Array of days to skip (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
119
+ */
120
+ skipDays(days) {
121
+ // Validate days
122
+ for (const day of days) {
123
+ if (day < 0 || day > 6 || !Number.isInteger(day)) {
124
+ throw new Error(`Invalid skip day: ${day}. Must be an integer 0-6 (0=Sunday, 6=Saturday)`);
125
+ }
126
+ }
127
+ this.attrs.skipDays = [...new Set(days)]; // Remove duplicates
128
+ return this;
129
+ }
130
+ /**
131
+ * if set, a job is forked via node child process and runs in a seperate/own
132
+ * thread
133
+ * @param enableForkMode
134
+ */
135
+ forkMode(enableForkMode) {
136
+ this.attrs.fork = enableForkMode;
137
+ return this;
138
+ }
139
+ /**
140
+ * Prevents the job from running
141
+ */
142
+ disable() {
143
+ this.attrs.disabled = true;
144
+ return this;
145
+ }
146
+ /**
147
+ * Allows job to run
148
+ */
149
+ enable() {
150
+ this.attrs.disabled = false;
151
+ return this;
152
+ }
153
+ /**
154
+ * Data to ensure is unique for job to be created
155
+ * @param unique
156
+ * @param opts
157
+ */
158
+ unique(unique, opts) {
159
+ this.attrs.unique = unique;
160
+ this.attrs.uniqueOpts = opts;
161
+ return this;
162
+ }
163
+ /**
164
+ * Configure debounce behavior for this job.
165
+ * Debouncing delays job execution and resets the timer on subsequent saves,
166
+ * ensuring the job only runs once after a quiet period.
167
+ *
168
+ * IMPORTANT: Requires a unique constraint to be set via `.unique()`.
169
+ * The unique constraint identifies which jobs should be debounced together.
170
+ *
171
+ * @param delay - Debounce window in milliseconds
172
+ * @param options - Optional configuration (maxWait, strategy)
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * // Basic trailing debounce - execute 2s after last save
177
+ * await agenda.create('updateIndex', { entityId: 123 })
178
+ * .unique({ 'data.entityId': 123 })
179
+ * .debounce(2000)
180
+ * .save();
181
+ *
182
+ * // With maxWait - guarantee execution within 30s
183
+ * await agenda.create('syncUser', { userId: 456 })
184
+ * .unique({ 'data.userId': 456 })
185
+ * .debounce(5000, { maxWait: 30000 })
186
+ * .save();
187
+ *
188
+ * // Leading strategy - execute immediately, ignore subsequent calls
189
+ * await agenda.create('notify', { channel: '#alerts' })
190
+ * .unique({ 'data.channel': '#alerts' })
191
+ * .debounce(60000, { strategy: 'leading' })
192
+ * .save();
193
+ * ```
194
+ */
195
+ debounce(delay, options) {
196
+ const debounceOpts = {
197
+ delay,
198
+ maxWait: options?.maxWait,
199
+ strategy: options?.strategy ?? 'trailing'
200
+ };
201
+ this.attrs.uniqueOpts = {
202
+ ...this.attrs.uniqueOpts,
203
+ debounce: debounceOpts
204
+ };
205
+ return this;
206
+ }
207
+ /**
208
+ * Schedules a job to run at specified time.
209
+ * Date constraints (startDate, endDate, skipDays) are applied if set.
210
+ * @param time
211
+ */
212
+ schedule(time) {
213
+ const d = new Date(time);
214
+ let nextRunAt = Number.isNaN(d.getTime()) ? date(time) : d;
215
+ // Apply date constraints if any are set
216
+ if (this.attrs.startDate || this.attrs.endDate || this.attrs.skipDays) {
217
+ nextRunAt = applyAllDateConstraints(nextRunAt, {
218
+ startDate: this.attrs.startDate,
219
+ endDate: this.attrs.endDate,
220
+ skipDays: this.attrs.skipDays,
221
+ timezone: this.attrs.repeatTimezone
222
+ });
223
+ }
224
+ this.attrs.nextRunAt = nextRunAt;
225
+ return this;
226
+ }
227
+ /**
228
+ * Sets priority of the job
229
+ * @param priority priority of when job should be queued
230
+ */
231
+ priority(priority) {
232
+ this.attrs.priority = parsePriority(priority);
233
+ return this;
234
+ }
235
+ /**
236
+ * Fails the job with a reason (error) specified
237
+ *
238
+ * @param reason
239
+ */
240
+ fail(reason) {
241
+ // Fork-mode children reject with plain serialized payloads (objects or
242
+ // strings), so normalize anything that is not an Error/string to keep
243
+ // failReason a string as its type promises.
244
+ let failReason;
245
+ if (reason instanceof Error) {
246
+ failReason = reason.message;
247
+ }
248
+ else if (typeof reason === 'string') {
249
+ failReason = reason;
250
+ }
251
+ else if (typeof reason === 'object' &&
252
+ reason !== null &&
253
+ typeof reason.message === 'string') {
254
+ failReason = reason.message;
255
+ }
256
+ else {
257
+ try {
258
+ failReason = JSON.stringify(reason) ?? String(reason);
259
+ }
260
+ catch {
261
+ failReason = String(reason);
262
+ }
263
+ }
264
+ this.attrs.failReason = failReason;
265
+ this.attrs.failCount = (this.attrs.failCount || 0) + 1;
266
+ const now = new Date();
267
+ this.attrs.failedAt = now;
268
+ this.attrs.lastFinishedAt = now;
269
+ log('[%s:%s] fail() called [%d] times so far', this.attrs.name, this.attrs._id, this.attrs.failCount);
270
+ return this;
271
+ }
272
+ async fetchStatus() {
273
+ const result = await this.agenda.db.queryJobs({ id: this.attrs._id?.toString() });
274
+ if (!result.jobs || result.jobs.length === 0) {
275
+ // @todo: should we just return false instead? a finished job could have been removed from database,
276
+ // and then this would throw...
277
+ throw new Error(`job with id ${this.attrs._id} not found in database`);
278
+ }
279
+ this.attrs.lastRunAt = result.jobs[0].lastRunAt;
280
+ this.attrs.lockedAt = result.jobs[0].lockedAt;
281
+ this.attrs.lastFinishedAt = result.jobs[0].lastFinishedAt;
282
+ }
283
+ /**
284
+ * A job is running if:
285
+ * (lastRunAt exists AND lastFinishedAt does not exist)
286
+ * OR
287
+ * (lastRunAt exists AND lastFinishedAt exists but the lastRunAt is newer [in time] than lastFinishedAt)
288
+ * @returns Whether or not job is running at the moment (true for running)
289
+ */
290
+ async isRunning() {
291
+ if (!this.byJobProcessor || this.attrs.fork) {
292
+ // we have no job definition, therfore we are not the job processor, but a client call
293
+ // so we get the real state from database
294
+ await this.fetchStatus();
295
+ }
296
+ if (!this.attrs.lastRunAt) {
297
+ return false;
298
+ }
299
+ if (!this.attrs.lastFinishedAt) {
300
+ return true;
301
+ }
302
+ if (this.attrs.lockedAt &&
303
+ this.attrs.lastRunAt.getTime() > this.attrs.lastFinishedAt.getTime()) {
304
+ return true;
305
+ }
306
+ return false;
307
+ }
308
+ /**
309
+ * Saves a job to database
310
+ */
311
+ async save() {
312
+ if (this.agenda.forkedWorker) {
313
+ const warning = new Error('calling save() on a Job during a forkedWorker has no effect!');
314
+ log('WARNING: %s %s', warning.message, warning.stack);
315
+ return this;
316
+ }
317
+ // ensure db connection is ready
318
+ await this.agenda.ready;
319
+ // save() is only called by user code (the job processor uses saveJobState() directly),
320
+ // so we always exclude processor-managed fields to prevent race conditions
321
+ const result = await this.agenda.db.saveJob(this.toJson(true), {
322
+ lastModifiedBy: this.agenda.attrs.name || undefined
323
+ });
324
+ // Update attrs from result
325
+ this.attrs._id = result._id;
326
+ this.attrs.nextRunAt = result.nextRunAt;
327
+ this.attrs.debounceStartedAt = result.debounceStartedAt;
328
+ // Publish notification for real-time processing if channel is configured
329
+ if (this.agenda.hasNotificationChannel()) {
330
+ await this.agenda.publishJobNotification(this);
331
+ }
332
+ return this;
333
+ }
334
+ /**
335
+ * Remove the job from database
336
+ */
337
+ remove() {
338
+ return this.agenda.cancel({ id: this.attrs._id });
339
+ }
340
+ async isDead() {
341
+ return this.isExpired();
342
+ }
343
+ async isExpired() {
344
+ if (!this.byJobProcessor || this.attrs.fork) {
345
+ // we have no job definition, therfore we are not the job processor, but a client call
346
+ // so we get the real state from database
347
+ await this.fetchStatus();
348
+ }
349
+ const definition = this.agenda.definitions[this.attrs.name];
350
+ if (!definition) {
351
+ throw new Error(`Undefined job ${this.attrs.name}`);
352
+ }
353
+ const lockDeadline = new Date(Date.now() - definition.lockLifetime);
354
+ // This means a job has "expired", as in it has not been "touched" within the lockoutTime
355
+ // Remove from local lock
356
+ if (this.attrs.lockedAt && this.attrs.lockedAt < lockDeadline) {
357
+ return true;
358
+ }
359
+ return false;
360
+ }
361
+ /**
362
+ * Updates "lockedAt" time so the job does not get picked up again
363
+ * @param progress 0 to 100
364
+ */
365
+ async touch(progress) {
366
+ if (this.canceled) {
367
+ throw new Error(`job ${this.attrs.name} got canceled already: ${this.canceled}!`);
368
+ }
369
+ this.attrs.lockedAt = new Date();
370
+ this.attrs.progress = progress;
371
+ await this.agenda.db.saveJobState(this.attrs, {
372
+ lastModifiedBy: this.agenda.attrs.name || undefined
373
+ });
374
+ // Publish progress state notification if progress was provided (fire-and-forget)
375
+ if (progress !== undefined) {
376
+ this.agenda.publishJobStateNotification(this, 'progress', {
377
+ progress
378
+ });
379
+ }
380
+ }
381
+ computeNextRunAt() {
382
+ try {
383
+ if (this.attrs.repeatInterval) {
384
+ const nextRunAt = computeFromInterval(this.attrs);
385
+ this.attrs.nextRunAt = nextRunAt;
386
+ if (nextRunAt) {
387
+ log('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, nextRunAt.toISOString());
388
+ }
389
+ else {
390
+ log('[%s:%s] nextRunAt set to null (date constraints)', this.attrs.name, this.attrs._id);
391
+ }
392
+ }
393
+ else if (this.attrs.repeatAt) {
394
+ const nextRunAt = computeFromRepeatAt(this.attrs);
395
+ this.attrs.nextRunAt = nextRunAt;
396
+ if (nextRunAt) {
397
+ log('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, nextRunAt.toISOString());
398
+ }
399
+ else {
400
+ log('[%s:%s] nextRunAt set to null (date constraints)', this.attrs.name, this.attrs._id);
401
+ }
402
+ }
403
+ else {
404
+ this.attrs.nextRunAt = null;
405
+ }
406
+ }
407
+ catch (error) {
408
+ this.attrs.nextRunAt = null;
409
+ this.fail(error);
410
+ }
411
+ return this;
412
+ }
413
+ /**
414
+ * Handle automatic retry with backoff strategy
415
+ * @param error - The error that caused the failure
416
+ */
417
+ async handleRetry(error) {
418
+ const definition = this.agenda.definitions[this.attrs.name];
419
+ // No backoff strategy configured - skip auto-retry
420
+ if (!definition?.backoff) {
421
+ return;
422
+ }
423
+ const context = {
424
+ attempt: this.attrs.failCount || 1,
425
+ error,
426
+ jobName: this.attrs.name,
427
+ jobData: this.attrs.data
428
+ };
429
+ const retryDelay = definition.backoff(context);
430
+ if (retryDelay === null) {
431
+ // No more retries - emit exhausted event
432
+ log('[%s:%s] retry attempts exhausted after %d failures', this.attrs.name, this.attrs._id, this.attrs.failCount);
433
+ this.agenda.logJobEvent({
434
+ level: 'warn',
435
+ event: 'retry:exhausted',
436
+ jobId: this.attrs._id?.toString(),
437
+ jobName: this.attrs.name,
438
+ message: `retry exhausted after ${this.attrs.failCount} failures: ${error.message}`,
439
+ error: error.message,
440
+ failCount: this.attrs.failCount
441
+ });
442
+ this.agenda.emit('retry exhausted', error, this);
443
+ this.agenda.emit(`retry exhausted:${this.attrs.name}`, error, this);
444
+ return;
445
+ }
446
+ // Schedule retry
447
+ const nextRunAt = new Date(Date.now() + retryDelay);
448
+ this.attrs.nextRunAt = nextRunAt;
449
+ log('[%s:%s] scheduling retry #%d in %dms (at %s)', this.attrs.name, this.attrs._id, this.attrs.failCount, retryDelay, nextRunAt.toISOString());
450
+ this.agenda.logJobEvent({
451
+ level: 'info',
452
+ event: 'retry',
453
+ jobId: this.attrs._id?.toString(),
454
+ jobName: this.attrs.name,
455
+ message: `retry #${this.attrs.failCount} scheduled in ${retryDelay}ms`,
456
+ retryDelay,
457
+ retryAttempt: this.attrs.failCount ?? undefined,
458
+ error: error.message
459
+ });
460
+ this.agenda.emit('retry', this, {
461
+ attempt: this.attrs.failCount,
462
+ delay: retryDelay,
463
+ nextRunAt,
464
+ error
465
+ });
466
+ this.agenda.emit(`retry:${this.attrs.name}`, this, {
467
+ attempt: this.attrs.failCount,
468
+ delay: retryDelay,
469
+ nextRunAt,
470
+ error
471
+ });
472
+ // Publish retry state notification for cross-process event propagation (fire-and-forget)
473
+ this.agenda.publishJobStateNotification(this, 'retry', {
474
+ retryAt: nextRunAt,
475
+ retryAttempt: this.attrs.failCount,
476
+ error: error.message
477
+ });
478
+ }
479
+ async run() {
480
+ this.attrs.lastRunAt = new Date();
481
+ log('[%s:%s] setting lastRunAt to: %s', this.attrs.name, this.attrs._id, this.attrs.lastRunAt.toISOString());
482
+ this.computeNextRunAt();
483
+ await this.agenda.db.saveJobState(this.attrs, {
484
+ lastModifiedBy: this.agenda.attrs.name || undefined
485
+ });
486
+ let succeeded = false;
487
+ try {
488
+ this.agenda.emit('start', this);
489
+ this.agenda.emit(`start:${this.attrs.name}`, this);
490
+ log('[%s:%s] starting job', this.attrs.name, this.attrs._id);
491
+ this.agenda.logJobEvent({
492
+ level: 'info',
493
+ event: 'start',
494
+ jobId: this.attrs._id?.toString(),
495
+ jobName: this.attrs.name,
496
+ message: `job started`
497
+ });
498
+ // Publish start state notification for cross-process event propagation (fire-and-forget)
499
+ this.agenda.publishJobStateNotification(this, 'start', {
500
+ lastRunAt: this.attrs.lastRunAt
501
+ });
502
+ if (this.attrs.fork) {
503
+ if (!this.agenda.forkHelper) {
504
+ throw new Error('no forkHelper specified, you need to set a path to a helper script');
505
+ }
506
+ const { forkHelper } = this.agenda;
507
+ await new Promise((resolve, reject) => {
508
+ this.forkedChild = fork(forkHelper.path, [
509
+ this.attrs.name,
510
+ this.attrs._id.toString(),
511
+ this.agenda.definitions[this.attrs.name].filePath || ''
512
+ ], forkHelper.options);
513
+ let childError;
514
+ this.forkedChild.on('close', code => {
515
+ if (code) {
516
+ log('fork parameters: %O name=%s id=%s filePath=%s', forkHelper, this.attrs.name, this.attrs._id, this.agenda.definitions[this.attrs.name].filePath);
517
+ const error = new Error(`child process exited with code: ${code}`);
518
+ log('fork child error: %s %O', error.message, childError || this.canceled);
519
+ reject(childError || this.canceled || error);
520
+ }
521
+ else {
522
+ resolve();
523
+ }
524
+ });
525
+ this.forkedChild.on('message', message => {
526
+ // console.log(`Message from child.js: ${message}`, JSON.stringify(message));
527
+ if (typeof message === 'string') {
528
+ try {
529
+ childError = JSON.parse(message);
530
+ }
531
+ catch {
532
+ childError = message;
533
+ }
534
+ }
535
+ else {
536
+ childError = message;
537
+ }
538
+ });
539
+ });
540
+ }
541
+ else {
542
+ await this.runJob();
543
+ }
544
+ this.attrs.lastFinishedAt = new Date();
545
+ this.agenda.emit('success', this);
546
+ this.agenda.emit(`success:${this.attrs.name}`, this);
547
+ log('[%s:%s] has succeeded', this.attrs.name, this.attrs._id);
548
+ const successDuration = this.attrs.lastFinishedAt.getTime() - (this.attrs.lastRunAt?.getTime() || 0);
549
+ this.agenda.logJobEvent({
550
+ level: 'info',
551
+ event: 'success',
552
+ jobId: this.attrs._id?.toString(),
553
+ jobName: this.attrs.name,
554
+ message: `job succeeded`,
555
+ duration: successDuration
556
+ });
557
+ succeeded = true;
558
+ // Publish success state notification for cross-process event propagation (fire-and-forget)
559
+ this.agenda.publishJobStateNotification(this, 'success', {
560
+ lastRunAt: this.attrs.lastRunAt,
561
+ lastFinishedAt: this.attrs.lastFinishedAt,
562
+ duration: this.attrs.lastFinishedAt.getTime() - (this.attrs.lastRunAt?.getTime() || 0)
563
+ });
564
+ }
565
+ catch (error) {
566
+ log('[%s:%s] unknown error occurred', this.attrs.name, this.attrs._id);
567
+ this.fail(error);
568
+ this.agenda.emit('fail', error, this);
569
+ this.agenda.emit(`fail:${this.attrs.name}`, error, this);
570
+ log('[%s:%s] has failed [%s]', this.attrs.name, this.attrs._id, error.message);
571
+ this.agenda.logJobEvent({
572
+ level: 'error',
573
+ event: 'fail',
574
+ jobId: this.attrs._id?.toString(),
575
+ jobName: this.attrs.name,
576
+ message: `job failed: ${error.message}`,
577
+ error: error.message,
578
+ failCount: this.attrs.failCount
579
+ });
580
+ // Publish fail state notification for cross-process event propagation (fire-and-forget)
581
+ this.agenda.publishJobStateNotification(this, 'fail', {
582
+ error: error.message,
583
+ failCount: this.attrs.failCount
584
+ });
585
+ // Handle automatic retry with backoff strategy
586
+ await this.handleRetry(error);
587
+ }
588
+ finally {
589
+ this.forkedChild = undefined;
590
+ this.attrs.lockedAt = undefined;
591
+ try {
592
+ await this.agenda.db.saveJobState(this.attrs, {
593
+ lastModifiedBy: this.agenda.attrs.name || undefined
594
+ });
595
+ log('[%s:%s] was saved successfully to database', this.attrs.name, this.attrs._id);
596
+ }
597
+ catch (err) {
598
+ // in case this fails, we ignore it
599
+ // this can e.g. happen if the job gets removed during the execution
600
+ log('[%s:%s] was not saved to database', this.attrs.name, this.attrs._id, err);
601
+ }
602
+ this.agenda.emit('complete', this);
603
+ this.agenda.emit(`complete:${this.attrs.name}`, this);
604
+ log('[%s:%s] job finished at [%s] and was unlocked', this.attrs.name, this.attrs._id, this.attrs.lastFinishedAt);
605
+ const completeDuration = this.attrs.lastFinishedAt && this.attrs.lastRunAt
606
+ ? this.attrs.lastFinishedAt.getTime() - this.attrs.lastRunAt.getTime()
607
+ : undefined;
608
+ this.agenda.logJobEvent({
609
+ level: 'info',
610
+ event: 'complete',
611
+ jobId: this.attrs._id?.toString(),
612
+ jobName: this.attrs.name,
613
+ message: `job complete`,
614
+ duration: completeDuration
615
+ });
616
+ // Publish complete state notification for cross-process event propagation (fire-and-forget)
617
+ const duration = this.attrs.lastFinishedAt && this.attrs.lastRunAt
618
+ ? this.attrs.lastFinishedAt.getTime() - this.attrs.lastRunAt.getTime()
619
+ : undefined;
620
+ this.agenda.publishJobStateNotification(this, 'complete', {
621
+ lastRunAt: this.attrs.lastRunAt,
622
+ lastFinishedAt: this.attrs.lastFinishedAt,
623
+ duration
624
+ });
625
+ // Auto-remove completed one-time jobs
626
+ if (succeeded && !this.attrs.nextRunAt) {
627
+ const definition = this.agenda.definitions[this.attrs.name];
628
+ const shouldRemove = definition?.removeOnComplete ?? this.agenda.attrs.removeOnComplete;
629
+ if (shouldRemove) {
630
+ try {
631
+ await this.remove();
632
+ log('[%s:%s] auto-removed completed one-time job', this.attrs.name, this.attrs._id);
633
+ }
634
+ catch (err) {
635
+ log('[%s:%s] failed to auto-remove job', this.attrs.name, this.attrs._id, err);
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+ async runJob() {
642
+ const definition = this.agenda.definitions[this.attrs.name];
643
+ if (!definition) {
644
+ log('[%s:%s] has no definition, can not run', this.attrs.name, this.attrs._id);
645
+ throw new Error('Undefined job');
646
+ }
647
+ if (definition.fn.length === 2) {
648
+ log('[%s:%s] process function being called', this.attrs.name, this.attrs._id);
649
+ await new Promise((resolve, reject) => {
650
+ try {
651
+ const result = definition.fn(this, error => {
652
+ if (error) {
653
+ reject(error);
654
+ return;
655
+ }
656
+ resolve();
657
+ });
658
+ if (this.isPromise(result)) {
659
+ result.catch((error) => reject(error));
660
+ }
661
+ }
662
+ catch (error) {
663
+ reject(error);
664
+ }
665
+ });
666
+ }
667
+ else {
668
+ log('[%s:%s] process function being called', this.attrs.name, this.attrs._id);
669
+ await definition.fn(this);
670
+ }
671
+ }
672
+ isPromise(value) {
673
+ return !!(value && typeof value.then === 'function');
674
+ }
675
+ }
676
+ /**
677
+ * Fields managed by the job processor that should not be saved by user code.
678
+ * These are excluded in save() to prevent race conditions between user code and the processor.
679
+ */
680
+ Job.PROCESSOR_MANAGED_FIELDS = new Set([
681
+ 'lockedAt',
682
+ 'lastRunAt',
683
+ 'lastFinishedAt',
684
+ 'failedAt',
685
+ 'failCount',
686
+ 'failReason',
687
+ 'progress'
688
+ ]);
689
+ //# sourceMappingURL=Job.js.map