agenda 4.3.0 → 5.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 (89) hide show
  1. package/History.md +12 -3
  2. package/README.md +38 -9
  3. package/dist/agenda/create.d.ts +2 -2
  4. package/dist/agenda/create.d.ts.map +1 -1
  5. package/dist/agenda/create.js.map +1 -1
  6. package/dist/agenda/define.d.ts +2 -2
  7. package/dist/agenda/define.d.ts.map +1 -1
  8. package/dist/agenda/define.js.map +1 -1
  9. package/dist/agenda/drain.d.ts +9 -0
  10. package/dist/agenda/drain.d.ts.map +1 -0
  11. package/dist/agenda/drain.js +46 -0
  12. package/dist/agenda/drain.js.map +1 -0
  13. package/dist/agenda/every.d.ts +2 -1
  14. package/dist/agenda/every.d.ts.map +1 -1
  15. package/dist/agenda/every.js +1 -1
  16. package/dist/agenda/every.js.map +1 -1
  17. package/dist/agenda/find-and-lock-next-job.d.ts.map +1 -1
  18. package/dist/agenda/find-and-lock-next-job.js +3 -4
  19. package/dist/agenda/find-and-lock-next-job.js.map +1 -1
  20. package/dist/agenda/index.d.ts +2 -0
  21. package/dist/agenda/index.d.ts.map +1 -1
  22. package/dist/agenda/index.js +2 -0
  23. package/dist/agenda/index.js.map +1 -1
  24. package/dist/agenda/now.d.ts +2 -2
  25. package/dist/agenda/now.d.ts.map +1 -1
  26. package/dist/agenda/now.js.map +1 -1
  27. package/dist/agenda/schedule.d.ts +3 -3
  28. package/dist/agenda/schedule.d.ts.map +1 -1
  29. package/dist/agenda/schedule.js.map +1 -1
  30. package/dist/job/compute-next-run-at.js +2 -2
  31. package/dist/job/compute-next-run-at.js.map +1 -1
  32. package/dist/job/index.d.ts +6 -3
  33. package/dist/job/index.d.ts.map +1 -1
  34. package/dist/job/index.js.map +1 -1
  35. package/lib/agenda/cancel.ts +28 -0
  36. package/lib/agenda/close.ts +37 -0
  37. package/lib/agenda/create.ts +22 -0
  38. package/lib/agenda/database.ts +63 -0
  39. package/lib/agenda/db-init.ts +45 -0
  40. package/lib/agenda/default-concurrency.ts +19 -0
  41. package/lib/agenda/default-lock-lifetime.ts +17 -0
  42. package/lib/agenda/default-lock-limit.ts +17 -0
  43. package/lib/agenda/define.ts +85 -0
  44. package/lib/agenda/disable.ts +28 -0
  45. package/lib/agenda/drain.ts +30 -0
  46. package/lib/agenda/enable.ts +29 -0
  47. package/lib/agenda/every.ts +86 -0
  48. package/lib/agenda/find-and-lock-next-job.ts +78 -0
  49. package/lib/agenda/has-mongo-protocol.ts +8 -0
  50. package/lib/agenda/index.ts +227 -0
  51. package/lib/agenda/job-processing-queue.ts +99 -0
  52. package/lib/agenda/jobs.ts +31 -0
  53. package/lib/agenda/lock-limit.ts +17 -0
  54. package/lib/agenda/max-concurrency.ts +20 -0
  55. package/lib/agenda/mongo.ts +21 -0
  56. package/lib/agenda/name.ts +16 -0
  57. package/lib/agenda/now.ts +31 -0
  58. package/lib/agenda/process-every.ts +18 -0
  59. package/lib/agenda/purge.ts +19 -0
  60. package/lib/agenda/save-job.ts +185 -0
  61. package/lib/agenda/schedule.ts +79 -0
  62. package/lib/agenda/sort.ts +17 -0
  63. package/lib/agenda/start.ts +30 -0
  64. package/lib/agenda/stop.ts +49 -0
  65. package/lib/cjs.ts +5 -0
  66. package/lib/index.ts +11 -0
  67. package/lib/job/compute-next-run-at.ts +191 -0
  68. package/lib/job/disable.ts +11 -0
  69. package/lib/job/enable.ts +11 -0
  70. package/lib/job/fail.ts +29 -0
  71. package/lib/job/index.ts +221 -0
  72. package/lib/job/is-running.ts +29 -0
  73. package/lib/job/priority.ts +11 -0
  74. package/lib/job/remove.ts +10 -0
  75. package/lib/job/repeat-at.ts +12 -0
  76. package/lib/job/repeat-every.ts +40 -0
  77. package/lib/job/run.ts +125 -0
  78. package/lib/job/save.ts +11 -0
  79. package/lib/job/schedule.ts +15 -0
  80. package/lib/job/set-shouldsaveresult.ts +10 -0
  81. package/lib/job/to-json.ts +36 -0
  82. package/lib/job/touch.ts +11 -0
  83. package/lib/job/unique.ts +18 -0
  84. package/lib/utils/create-job.ts +13 -0
  85. package/lib/utils/index.ts +3 -0
  86. package/lib/utils/parse-priority.ts +37 -0
  87. package/lib/utils/process-jobs.ts +379 -0
  88. package/package.json +14 -12
  89. package/tsconfig.json +72 -0
@@ -0,0 +1,227 @@
1
+ import { EventEmitter } from "events";
2
+ import humanInterval from "human-interval";
3
+ import {
4
+ AnyError,
5
+ Collection,
6
+ Db as MongoDb,
7
+ MongoClient,
8
+ MongoClientOptions,
9
+ } from "mongodb";
10
+ import { Job } from "../job";
11
+ import { cancel } from "./cancel";
12
+ import { close } from "./close";
13
+ import { create } from "./create";
14
+ import { database } from "./database";
15
+ import { dbInit } from "./db-init";
16
+ import { defaultConcurrency } from "./default-concurrency";
17
+ import { defaultLockLifetime } from "./default-lock-lifetime";
18
+ import { defaultLockLimit } from "./default-lock-limit";
19
+ import { define } from "./define";
20
+ import { disable } from "./disable";
21
+ import { enable } from "./enable";
22
+ import { every } from "./every";
23
+ import { findAndLockNextJob } from "./find-and-lock-next-job";
24
+ import { JobProcessingQueue } from "./job-processing-queue";
25
+ import { jobs } from "./jobs";
26
+ import { lockLimit } from "./lock-limit";
27
+ import { maxConcurrency } from "./max-concurrency";
28
+ import { mongo } from "./mongo";
29
+ import { name } from "./name";
30
+ import { now } from "./now";
31
+ import { processEvery } from "./process-every";
32
+ import { purge } from "./purge";
33
+ import { saveJob } from "./save-job";
34
+ import { schedule } from "./schedule";
35
+ import { sort } from "./sort";
36
+ import { start } from "./start";
37
+ import { stop } from "./stop";
38
+ import { drain } from './drain';
39
+
40
+ export interface AgendaConfig {
41
+ name?: string;
42
+ processEvery?: string;
43
+ maxConcurrency?: number;
44
+ defaultConcurrency?: number;
45
+ lockLimit?: number;
46
+ defaultLockLimit?: number;
47
+ defaultLockLifetime?: number;
48
+ sort?: any;
49
+ mongo?: MongoDb;
50
+ db?: {
51
+ address: string;
52
+ collection?: string;
53
+ options?: MongoClientOptions;
54
+ };
55
+ disableAutoIndex?: boolean;
56
+ }
57
+
58
+ /**
59
+ * @class Agenda
60
+ * @param {Object} config - Agenda Config
61
+ * @param {Function} cb - Callback after Agenda has started and connected to mongo
62
+ * @property {Object} _name - Name of the current Agenda queue
63
+ * @property {Number} _processEvery
64
+ * @property {Number} _defaultConcurrency
65
+ * @property {Number} _maxConcurrency
66
+ * @property {Number} _defaultLockLimit
67
+ * @property {Number} _lockLimit
68
+ * @property {Object} _definitions
69
+ * @property {Object} _runningJobs
70
+ * @property {Object} _lockedJobs
71
+ * @property {Object} _jobQueue
72
+ * @property {Number} _defaultLockLifetime
73
+ * @property {Object} _sort
74
+ * @property {Object} _indices
75
+ * @property {Boolean} _isLockingOnTheFly - true if 'lockingOnTheFly' is currently running. Prevent concurrent execution of this method.
76
+ * @property {Map} _isJobQueueFilling - A map of jobQueues and if the 'jobQueueFilling' method is currently running for a given map. 'lockingOnTheFly' and 'jobQueueFilling' should not run concurrently for the same jobQueue. It can cause that lock limits aren't honored.
77
+ * @property {Array} _jobsToLock
78
+ */
79
+ class Agenda extends EventEmitter {
80
+ _defaultConcurrency: any;
81
+ _defaultLockLifetime: any;
82
+ _defaultLockLimit: any;
83
+ _definitions: any;
84
+ _findAndLockNextJob = findAndLockNextJob;
85
+ _indices: any;
86
+ _disableAutoIndex: boolean;
87
+ _isLockingOnTheFly: boolean;
88
+ _isJobQueueFilling: Map<string, boolean>;
89
+ _jobQueue: JobProcessingQueue;
90
+ _jobsToLock: Job[];
91
+ _lockedJobs: Job[];
92
+ _runningJobs: Job[];
93
+ _lockLimit: any;
94
+ _maxConcurrency: any;
95
+ _mongoUseUnifiedTopology?: boolean;
96
+ _name: any;
97
+ _processEvery: number;
98
+ _ready: Promise<unknown>;
99
+ _sort: any;
100
+ _db!: MongoClient;
101
+ _mdb!: MongoDb;
102
+ _collection!: Collection;
103
+ _nextScanAt: any;
104
+ _processInterval: any;
105
+
106
+ cancel!: typeof cancel;
107
+ close!: typeof close;
108
+ create!: typeof create;
109
+ database!: typeof database;
110
+ db_init!: typeof dbInit;
111
+ defaultConcurrency!: typeof defaultConcurrency;
112
+ defaultLockLifetime!: typeof defaultLockLifetime;
113
+ defaultLockLimit!: typeof defaultLockLimit;
114
+ define!: typeof define;
115
+ disable!: typeof disable;
116
+ enable!: typeof enable;
117
+ every!: typeof every;
118
+ jobs!: typeof jobs;
119
+ lockLimit!: typeof lockLimit;
120
+ maxConcurrency!: typeof maxConcurrency;
121
+ mongo!: typeof mongo;
122
+ name!: typeof name;
123
+ now!: typeof now;
124
+ processEvery!: typeof processEvery;
125
+ purge!: typeof purge;
126
+ saveJob!: typeof saveJob;
127
+ schedule!: typeof schedule;
128
+ sort!: typeof sort;
129
+ start!: typeof start;
130
+ stop!: typeof stop;
131
+ drain!: typeof drain;
132
+
133
+ /**
134
+ * Constructs a new Agenda object.
135
+ * @param config Optional configuration to initialize the Agenda.
136
+ * @param cb Optional callback called with the MongoDB collection.
137
+ */
138
+ constructor(
139
+ config: AgendaConfig = {},
140
+ cb?: (
141
+ error: AnyError | undefined,
142
+ collection: Collection<any> | null
143
+ ) => void
144
+ ) {
145
+ super();
146
+
147
+ this._name = config.name;
148
+ this._processEvery = (humanInterval(config.processEvery) ??
149
+ humanInterval("5 seconds")) as number; // eslint-disable-line @typescript-eslint/non-nullable-type-assertion-style
150
+ this._defaultConcurrency = config.defaultConcurrency || 5;
151
+ this._maxConcurrency = config.maxConcurrency || 20;
152
+ this._defaultLockLimit = config.defaultLockLimit || 0;
153
+ this._lockLimit = config.lockLimit || 0;
154
+ this._definitions = {};
155
+ this._runningJobs = [];
156
+ this._lockedJobs = [];
157
+ this._jobQueue = new JobProcessingQueue();
158
+ this._defaultLockLifetime = config.defaultLockLifetime || 10 * 60 * 1000; // 10 minute default lockLifetime
159
+ this._sort = config.sort || { nextRunAt: 1, priority: -1 };
160
+ this._indices = {
161
+ name: 1,
162
+ ...this._sort,
163
+ priority: -1,
164
+ lockedAt: 1,
165
+ nextRunAt: 1,
166
+ disabled: 1,
167
+ };
168
+ this._disableAutoIndex = config.disableAutoIndex === true;
169
+
170
+ this._isLockingOnTheFly = false;
171
+ this._isJobQueueFilling = new Map<string, boolean>();
172
+ this._jobsToLock = [];
173
+ this._ready = new Promise((resolve) => {
174
+ this.once("ready", resolve);
175
+ });
176
+
177
+ if (config.mongo) {
178
+ this.mongo(
179
+ config.mongo,
180
+ config.db ? config.db.collection : undefined,
181
+ cb
182
+ ); // @ts-expect-error // the documentation shows it should be correct: http://mongodb.github.io/node-mongodb-native/3.6/api/Db.html
183
+ if (config.mongo.s && config.mongo.topology && config.mongo.topology.s) {
184
+ this._mongoUseUnifiedTopology = Boolean(
185
+ // @ts-expect-error
186
+ config.mongo.topology.s.options.useUnifiedTopology
187
+ );
188
+ }
189
+ } else if (config.db) {
190
+ this.database(
191
+ config.db.address,
192
+ config.db.collection,
193
+ config.db.options,
194
+ cb
195
+ );
196
+ }
197
+ }
198
+ }
199
+
200
+ Agenda.prototype.cancel = cancel;
201
+ Agenda.prototype.close = close;
202
+ Agenda.prototype.create = create;
203
+ Agenda.prototype.database = database;
204
+ Agenda.prototype.db_init = dbInit;
205
+ Agenda.prototype.defaultConcurrency = defaultConcurrency;
206
+ Agenda.prototype.defaultLockLifetime = defaultLockLifetime;
207
+ Agenda.prototype.defaultLockLimit = defaultLockLimit;
208
+ Agenda.prototype.define = define;
209
+ Agenda.prototype.disable = disable;
210
+ Agenda.prototype.enable = enable;
211
+ Agenda.prototype.every = every;
212
+ Agenda.prototype.jobs = jobs;
213
+ Agenda.prototype.lockLimit = lockLimit;
214
+ Agenda.prototype.maxConcurrency = maxConcurrency;
215
+ Agenda.prototype.mongo = mongo;
216
+ Agenda.prototype.name = name;
217
+ Agenda.prototype.now = now;
218
+ Agenda.prototype.processEvery = processEvery;
219
+ Agenda.prototype.purge = purge;
220
+ Agenda.prototype.saveJob = saveJob;
221
+ Agenda.prototype.schedule = schedule;
222
+ Agenda.prototype.sort = sort;
223
+ Agenda.prototype.start = start;
224
+ Agenda.prototype.stop = stop;
225
+ Agenda.prototype.drain = drain;
226
+
227
+ export { Agenda };
@@ -0,0 +1,99 @@
1
+ import { Job } from "../job";
2
+
3
+ /**
4
+ * @class
5
+ * @param {Object} args - Job Options
6
+ * @property {Object} agenda - The Agenda instance
7
+ * @property {Object} attrs
8
+ */
9
+ class JobProcessingQueue {
10
+ pop!: () => Job | undefined;
11
+ push!: (job: Job) => void;
12
+ insert!: (job: Job) => void;
13
+ returnNextConcurrencyFreeJob!: (agendaDefinitions: any) => Job;
14
+
15
+ protected _queue: Job[];
16
+
17
+ constructor() {
18
+ this._queue = [];
19
+ }
20
+
21
+ get length(): number {
22
+ return this._queue.length;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Pops and returns last queue element (next job to be processed) without checking concurrency.
28
+ * @returns Next Job to be processed
29
+ */
30
+ JobProcessingQueue.prototype.pop = function (this: JobProcessingQueue) {
31
+ return this._queue.pop();
32
+ };
33
+
34
+ /**
35
+ * Inserts job in first queue position
36
+ * @param job job to add to queue
37
+ */
38
+ JobProcessingQueue.prototype.push = function (
39
+ this: JobProcessingQueue,
40
+ job: Job
41
+ ) {
42
+ this._queue.push(job);
43
+ };
44
+
45
+ /**
46
+ * Inserts job in queue where it will be order from left to right in decreasing
47
+ * order of nextRunAt and priority (in case of same nextRunAt), if all values
48
+ * are even the first jobs to be introduced will have priority
49
+ * @param job job to add to queue
50
+ */
51
+ JobProcessingQueue.prototype.insert = function (
52
+ this: JobProcessingQueue,
53
+ job: Job
54
+ ) {
55
+ const matchIndex = this._queue.findIndex((element) => {
56
+ if (element.attrs.nextRunAt!.getTime() <= job.attrs.nextRunAt!.getTime()) {
57
+ if (
58
+ element.attrs.nextRunAt!.getTime() === job.attrs.nextRunAt!.getTime()
59
+ ) {
60
+ if (element.attrs.priority >= job.attrs.priority) {
61
+ return true;
62
+ }
63
+ } else {
64
+ return true;
65
+ }
66
+ }
67
+
68
+ return false;
69
+ });
70
+
71
+ if (matchIndex === -1) {
72
+ this._queue.push(job);
73
+ } else {
74
+ this._queue.splice(matchIndex, 0, job);
75
+ }
76
+ };
77
+
78
+ /**
79
+ * Returns (does not pop, element remains in queue) first element (always from the right)
80
+ * that can be processed (not blocked by concurrency execution)
81
+ * @param agendaDefinitions job to add to queue
82
+ * @returns Next Job to be processed
83
+ */
84
+ JobProcessingQueue.prototype.returnNextConcurrencyFreeJob = function (
85
+ this: JobProcessingQueue,
86
+ agendaDefinitions: any
87
+ ) {
88
+ let next;
89
+ for (next = this._queue.length - 1; next > 0; next -= 1) {
90
+ const def = agendaDefinitions[this._queue[next].attrs.name];
91
+ if (def.concurrency > def.running) {
92
+ break;
93
+ }
94
+ }
95
+
96
+ return this._queue[next];
97
+ };
98
+
99
+ export { JobProcessingQueue };
@@ -0,0 +1,31 @@
1
+ import { Filter } from "mongodb";
2
+ import { Agenda } from ".";
3
+ import { Job } from "../job";
4
+ import { createJob } from "../utils";
5
+
6
+ /**
7
+ * Finds all jobs matching 'query'
8
+ * @name Agenda#jobs
9
+ * @function
10
+ * @param [query] object for MongoDB
11
+ * @param [sort] object for MongoDB
12
+ * @param [limit] number of documents to return from MongoDB
13
+ * @param [number] of documents to skip in MongoDB
14
+ * @returns resolves when fails or passes
15
+ */
16
+ export const jobs = async function (
17
+ this: Agenda,
18
+ query: Filter<any> = {},
19
+ sort = {},
20
+ limit = 0,
21
+ skip = 0
22
+ ): Promise<Job[]> {
23
+ const result = await this._collection
24
+ .find(query) // eslint-disable-line
25
+ .sort(sort)
26
+ .limit(limit)
27
+ .skip(skip)
28
+ .toArray();
29
+
30
+ return result.map((job: any) => createJob(this, job));
31
+ };
@@ -0,0 +1,17 @@
1
+ import createDebugger from "debug";
2
+ import { Agenda } from ".";
3
+
4
+ const debug = createDebugger("agenda:locklimit");
5
+
6
+ /**
7
+ * Set the default amount jobs that are allowed to be locked at one time (GLOBAL)
8
+ * @name Agenda#locklimit
9
+ * @function
10
+ * @param limit num Lock limit
11
+ */
12
+ export const lockLimit = function (this: Agenda, limit: number): Agenda {
13
+ // @NOTE: Is this different than max concurrency?
14
+ debug("Agenda.lockLimit(%d)", limit);
15
+ this._lockLimit = limit;
16
+ return this;
17
+ };
@@ -0,0 +1,20 @@
1
+ import createDebugger from "debug";
2
+ import { Agenda } from ".";
3
+
4
+ const debug = createDebugger("agenda:maxConcurrency");
5
+
6
+ /**
7
+ * Set the concurrency for jobs (globally), type does not matter
8
+ * @name Agenda#maxConcurrency
9
+ * @function
10
+ * @param concurrency max concurrency value
11
+ * @returns agenda instance
12
+ */
13
+ export const maxConcurrency = function (
14
+ this: Agenda,
15
+ concurrency: number
16
+ ): Agenda {
17
+ debug("Agenda.maxConcurrency(%d)", concurrency);
18
+ this._maxConcurrency = concurrency;
19
+ return this;
20
+ };
@@ -0,0 +1,21 @@
1
+ import { AnyError, Collection, Db } from "mongodb";
2
+ import { Agenda } from ".";
3
+
4
+ /**
5
+ * Build method used to add MongoDB connection details
6
+ * @name Agenda#mongo
7
+ * @function
8
+ * @param mdb instance of MongoClient to use
9
+ * @param [collection] name collection we want to use ('agendaJobs')
10
+ * @param [cb] called when MongoDB connection fails or passes
11
+ */
12
+ export const mongo = function (
13
+ this: Agenda,
14
+ mdb: Db,
15
+ collection?: string,
16
+ cb?: (error: AnyError | undefined, collection: Collection<any> | null) => void
17
+ ): Agenda {
18
+ this._mdb = mdb;
19
+ this.db_init(collection, cb);
20
+ return this;
21
+ };
@@ -0,0 +1,16 @@
1
+ import createDebugger from "debug";
2
+ import { Agenda } from ".";
3
+
4
+ const debug = createDebugger("agenda:name");
5
+
6
+ /**
7
+ * Set name of queue
8
+ * @name Agenda#name
9
+ * @function
10
+ * @param name name of agenda instance
11
+ */
12
+ export const name = function (this: Agenda, name: string): Agenda {
13
+ debug("Agenda.name(%s)", name);
14
+ this._name = name;
15
+ return this;
16
+ };
@@ -0,0 +1,31 @@
1
+ import createDebugger from "debug";
2
+ import { Agenda } from ".";
3
+ import { Job, JobAttributesData } from "../job";
4
+
5
+ const debug = createDebugger("agenda:now");
6
+
7
+ /**
8
+ * Create a job for this exact moment
9
+ * @name Agenda#now
10
+ * @function
11
+ * @param name name of job to schedule
12
+ * @param data data to pass to job
13
+ */
14
+ export const now = async function<T extends JobAttributesData> (
15
+ this: Agenda,
16
+ name: string,
17
+ data: T
18
+ ): Promise<Job> {
19
+ debug("Agenda.now(%s, [Object])", name);
20
+ try {
21
+ const job = this.create(name, data);
22
+
23
+ job.schedule(new Date());
24
+ await job.save();
25
+
26
+ return job;
27
+ } catch (error) {
28
+ debug("error trying to create a job for this exact moment");
29
+ throw error;
30
+ }
31
+ };
@@ -0,0 +1,18 @@
1
+ import createDebugger from "debug";
2
+ import humanInterval from "human-interval";
3
+ import { Agenda } from ".";
4
+
5
+ const debug = createDebugger("agenda:processEvery");
6
+
7
+ /**
8
+ * Set the default process interval
9
+ * @name Agenda#processEvery
10
+ * @function
11
+ * @param time - time to process, expressed in human interval
12
+ */
13
+ export const processEvery = function (this: Agenda, time: string): Agenda {
14
+ debug("Agenda.processEvery(%d)", time);
15
+ // @ts-expect-error
16
+ this._processEvery = humanInterval(time);
17
+ return this;
18
+ };
@@ -0,0 +1,19 @@
1
+ import createDebugger from "debug";
2
+ import { Agenda } from ".";
3
+
4
+ const debug = createDebugger("agenda:purge");
5
+
6
+ /**
7
+ * Removes all jobs from queue
8
+ * @name Agenda#purge
9
+ * @function
10
+ * @returns resolved when job cancelling fails or passes
11
+ */
12
+ export const purge = async function (
13
+ this: Agenda
14
+ ): Promise<number | undefined> {
15
+ // @NOTE: Only use after defining your jobs
16
+ const definedNames = Object.keys(this._definitions);
17
+ debug("Agenda.purge(%o)", definedNames);
18
+ return this.cancel({ name: { $not: { $in: definedNames } } });
19
+ };
@@ -0,0 +1,185 @@
1
+ import createDebugger from "debug";
2
+ import { ObjectId } from "mongodb";
3
+ import { Agenda } from ".";
4
+ import { Job } from "../job";
5
+ import { processJobs } from "../utils";
6
+
7
+ const debug = createDebugger("agenda:saveJob");
8
+
9
+ /**
10
+ * Given a result for findOneAndUpdate() or insert() above, determine whether to process
11
+ * the job immediately or to let the processJobs() interval pick it up later
12
+ * @param job job instance
13
+ * @param result the data returned from the findOneAndUpdate() call or insertOne() call
14
+ * @access private
15
+ */
16
+ const processDbResult = async function (this: Agenda, job: Job, result: any) {
17
+ debug(
18
+ "processDbResult() called with success, checking whether to process job immediately or not"
19
+ );
20
+
21
+ // We have a result from the above calls
22
+ // findOneAndUpdate() returns different results than insertOne() so check for that
23
+ let resultValue = result.insertedId ? result.insertedId : result.value;
24
+
25
+ if (resultValue) {
26
+ let _id: ObjectId;
27
+ let nextRunAt: Date | null | undefined;
28
+
29
+ if (result.insertedId) {
30
+ _id = result.insertedId;
31
+ // find the doc using _id
32
+ const _job = await this._collection.findOne({ _id });
33
+
34
+ if (_job) {
35
+ nextRunAt = _job.nextRunAt;
36
+ }
37
+ } else {
38
+ // If it is an array, grab the first job
39
+ if (Array.isArray(resultValue)) {
40
+ resultValue = resultValue[0];
41
+ }
42
+
43
+ _id = resultValue._id;
44
+ nextRunAt = resultValue.nextRunAt;
45
+ }
46
+
47
+ // Grab ID and nextRunAt from MongoDB and store it as an attribute on Job
48
+ job.attrs._id = _id;
49
+ job.attrs.nextRunAt = nextRunAt;
50
+
51
+ // If the current job would have been processed in an older scan, process the job immediately
52
+ if (job.attrs.nextRunAt && job.attrs.nextRunAt < this._nextScanAt) {
53
+ debug(
54
+ "[%s:%s] job would have ran by nextScanAt, processing the job immediately",
55
+ job.attrs.name,
56
+ resultValue._id
57
+ );
58
+ await processJobs.call(this, job);
59
+ }
60
+ }
61
+
62
+ // Return the Job instance
63
+ return job;
64
+ };
65
+
66
+ /**
67
+ * Save the properties on a job to MongoDB
68
+ * @name Agenda#saveJob
69
+ * @function
70
+ * @param job job to save into MongoDB
71
+ * @returns resolves when job is saved or errors
72
+ */
73
+ export const saveJob = async function (this: Agenda, job: Job): Promise<Job> {
74
+ try {
75
+ debug("attempting to save a job into Agenda instance");
76
+
77
+ // Grab information needed to save job but that we don't want to persist in MongoDB
78
+ const id = job.attrs._id;
79
+ const { unique, uniqueOpts } = job.attrs;
80
+
81
+ // Store job as JSON and remove props we don't want to store from object
82
+ const props = job.toJSON();
83
+ delete props._id;
84
+ delete props.unique;
85
+ delete props.uniqueOpts;
86
+
87
+ // Store name of agenda queue as last modifier in job data
88
+ props.lastModifiedBy = this._name;
89
+ debug("[job %s] set job props: \n%O", id, props);
90
+
91
+ // Grab current time and set default query options for MongoDB
92
+ const now = new Date();
93
+ const protect = {};
94
+ let update = { $set: props };
95
+ debug("current time stored as %s", now.toISOString());
96
+
97
+ // If the job already had an ID, then update the properties of the job
98
+ // i.e, who last modified it, etc
99
+ if (id) {
100
+ // Update the job and process the resulting data'
101
+ debug(
102
+ "job already has _id, calling findOneAndUpdate() using _id as query"
103
+ );
104
+ const result = await this._collection.findOneAndUpdate(
105
+ { _id: id },
106
+ update,
107
+ { returnDocument: "after" }
108
+ );
109
+ return await processDbResult.call(this, job, result);
110
+ }
111
+
112
+ if (props.type === "single") {
113
+ // Job type set to 'single' so...
114
+ // NOTE: Again, not sure about difference between 'single' here and 'once' in job.js
115
+ debug('job with type of "single" found');
116
+
117
+ // If the nextRunAt time is older than the current time, "protect" that property, meaning, don't change
118
+ // a scheduled job's next run time!
119
+ if (props.nextRunAt && props.nextRunAt <= now) {
120
+ debug(
121
+ "job has a scheduled nextRunAt time, protecting that field from upsert"
122
+ );
123
+ // @ts-expect-error
124
+ protect.nextRunAt = props.nextRunAt;
125
+ delete props.nextRunAt;
126
+ }
127
+
128
+ // If we have things to protect, set them in MongoDB using $setOnInsert
129
+ if (Object.keys(protect).length > 0) {
130
+ // @ts-expect-error
131
+ update.$setOnInsert = protect;
132
+ }
133
+
134
+ // Try an upsert
135
+ // NOTE: 'single' again, not exactly sure what it means
136
+ debug(
137
+ 'calling findOneAndUpdate() with job name and type of "single" as query'
138
+ );
139
+ const result = await this._collection.findOneAndUpdate(
140
+ {
141
+ name: props.name,
142
+ type: "single",
143
+ },
144
+ update,
145
+ {
146
+ upsert: true,
147
+ returnDocument: "after",
148
+ }
149
+ );
150
+ return await processDbResult.call(this, job, result);
151
+ }
152
+
153
+ if (unique) {
154
+ // If we want the job to be unique, then we can upsert based on the 'unique' query object that was passed in
155
+ const query = job.attrs.unique;
156
+ query.name = props.name;
157
+ if (uniqueOpts?.insertOnly) {
158
+ // @ts-expect-error
159
+ update = { $setOnInsert: props };
160
+ }
161
+
162
+ // Use the 'unique' query object to find an existing job or create a new one
163
+ debug(
164
+ "calling findOneAndUpdate() with unique object as query: \n%O",
165
+ query
166
+ );
167
+ const result = await this._collection.findOneAndUpdate(query, update, {
168
+ upsert: true,
169
+ returnDocument: "after",
170
+ });
171
+ return await processDbResult.call(this, job, result);
172
+ }
173
+
174
+ // If all else fails, the job does not exist yet so we just insert it into MongoDB
175
+ debug(
176
+ "using default behavior, inserting new job via insertOne() with props that were set: \n%O",
177
+ props
178
+ );
179
+ const result = await this._collection.insertOne(props);
180
+ return await processDbResult.call(this, job, result);
181
+ } catch (error) {
182
+ debug("processDbResult() received an error, job was not updated/created");
183
+ throw error;
184
+ }
185
+ };