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