@monque/core 1.3.0 → 1.4.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.
- package/dist/CHANGELOG.md +18 -0
- package/dist/index.cjs +235 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +10 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +235 -145
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/jobs/document-to-persisted-job.ts +52 -0
- package/src/jobs/index.ts +2 -0
- package/src/scheduler/monque.ts +33 -91
- package/src/scheduler/services/change-stream-handler.ts +2 -1
- package/src/scheduler/services/job-manager.ts +20 -32
- package/src/scheduler/services/job-processor.ts +94 -62
- package/src/scheduler/types.ts +11 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/utils/error.ts +33 -0
- package/src/shared/utils/index.ts +1 -0
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,40 @@ import { CronExpressionParser } from "cron-parser";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
5
|
|
|
6
|
+
//#region src/jobs/document-to-persisted-job.ts
|
|
7
|
+
/**
|
|
8
|
+
* Convert a raw MongoDB document to a strongly-typed {@link PersistedJob}.
|
|
9
|
+
*
|
|
10
|
+
* Maps required fields directly and conditionally includes optional fields
|
|
11
|
+
* only when they are present in the document (`!== undefined`).
|
|
12
|
+
*
|
|
13
|
+
* @internal Not part of the public API.
|
|
14
|
+
* @template T - The job data payload type
|
|
15
|
+
* @param doc - The raw MongoDB document with `_id`
|
|
16
|
+
* @returns A strongly-typed PersistedJob object with guaranteed `_id`
|
|
17
|
+
*/
|
|
18
|
+
function documentToPersistedJob(doc) {
|
|
19
|
+
const job = {
|
|
20
|
+
_id: doc._id,
|
|
21
|
+
name: doc["name"],
|
|
22
|
+
data: doc["data"],
|
|
23
|
+
status: doc["status"],
|
|
24
|
+
nextRunAt: doc["nextRunAt"],
|
|
25
|
+
failCount: doc["failCount"],
|
|
26
|
+
createdAt: doc["createdAt"],
|
|
27
|
+
updatedAt: doc["updatedAt"]
|
|
28
|
+
};
|
|
29
|
+
if (doc["lockedAt"] !== void 0) job.lockedAt = doc["lockedAt"];
|
|
30
|
+
if (doc["claimedBy"] !== void 0) job.claimedBy = doc["claimedBy"];
|
|
31
|
+
if (doc["lastHeartbeat"] !== void 0) job.lastHeartbeat = doc["lastHeartbeat"];
|
|
32
|
+
if (doc["heartbeatInterval"] !== void 0) job.heartbeatInterval = doc["heartbeatInterval"];
|
|
33
|
+
if (doc["failReason"] !== void 0) job.failReason = doc["failReason"];
|
|
34
|
+
if (doc["repeatInterval"] !== void 0) job.repeatInterval = doc["repeatInterval"];
|
|
35
|
+
if (doc["uniqueKey"] !== void 0) job.uniqueKey = doc["uniqueKey"];
|
|
36
|
+
return job;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
6
40
|
//#region src/jobs/types.ts
|
|
7
41
|
/**
|
|
8
42
|
* Represents the lifecycle states of a job in the queue.
|
|
@@ -561,6 +595,39 @@ function handleCronParseError(expression, error) {
|
|
|
561
595
|
throw new InvalidCronError(expression, `Invalid cron expression "${expression}": ${error instanceof Error ? error.message : "Unknown parsing error"}. Expected 5-field format: "minute hour day-of-month month day-of-week" or predefined expression (e.g. @daily). Example: "0 9 * * 1" (every Monday at 9am)`);
|
|
562
596
|
}
|
|
563
597
|
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/shared/utils/error.ts
|
|
600
|
+
/**
|
|
601
|
+
* Normalize an unknown caught value into a proper `Error` instance.
|
|
602
|
+
*
|
|
603
|
+
* In JavaScript, any value can be thrown — strings, numbers, objects, `undefined`, etc.
|
|
604
|
+
* This function ensures we always have a real `Error` with a proper stack trace and message.
|
|
605
|
+
*
|
|
606
|
+
* @param value - The caught value (typically from a `catch` block typed as `unknown`).
|
|
607
|
+
* @returns The original value if already an `Error`, otherwise a new `Error` wrapping `String(value)`.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* ```ts
|
|
611
|
+
* try {
|
|
612
|
+
* riskyOperation();
|
|
613
|
+
* } catch (error: unknown) {
|
|
614
|
+
* const normalized = toError(error);
|
|
615
|
+
* console.error(normalized.message);
|
|
616
|
+
* }
|
|
617
|
+
* ```
|
|
618
|
+
*
|
|
619
|
+
* @internal
|
|
620
|
+
*/
|
|
621
|
+
function toError(value) {
|
|
622
|
+
if (value instanceof Error) return value;
|
|
623
|
+
try {
|
|
624
|
+
return new Error(String(value));
|
|
625
|
+
} catch (conversionError) {
|
|
626
|
+
const detail = conversionError instanceof Error ? conversionError.message : "unknown conversion failure";
|
|
627
|
+
return /* @__PURE__ */ new Error(`Unserializable value (${detail})`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
564
631
|
//#endregion
|
|
565
632
|
//#region src/scheduler/helpers.ts
|
|
566
633
|
/**
|
|
@@ -710,7 +777,7 @@ var ChangeStreamHandler = class {
|
|
|
710
777
|
this.debounceTimer = setTimeout(() => {
|
|
711
778
|
this.debounceTimer = null;
|
|
712
779
|
this.onPoll().catch((error) => {
|
|
713
|
-
this.ctx.emit("job:error", { error });
|
|
780
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
714
781
|
});
|
|
715
782
|
}, 100);
|
|
716
783
|
}
|
|
@@ -819,9 +886,8 @@ var JobManager = class {
|
|
|
819
886
|
const _id = new ObjectId(jobId);
|
|
820
887
|
const jobDoc = await this.ctx.collection.findOne({ _id });
|
|
821
888
|
if (!jobDoc) return null;
|
|
822
|
-
|
|
823
|
-
if (
|
|
824
|
-
if (currentJob.status !== JobStatus.PENDING) throw new JobStateError(`Cannot cancel job in status '${currentJob.status}'`, jobId, currentJob.status, "cancel");
|
|
889
|
+
if (jobDoc["status"] === JobStatus.CANCELLED) return this.ctx.documentToPersistedJob(jobDoc);
|
|
890
|
+
if (jobDoc["status"] !== JobStatus.PENDING) throw new JobStateError(`Cannot cancel job in status '${jobDoc["status"]}'`, jobId, jobDoc["status"], "cancel");
|
|
825
891
|
const result = await this.ctx.collection.findOneAndUpdate({
|
|
826
892
|
_id,
|
|
827
893
|
status: JobStatus.PENDING
|
|
@@ -906,8 +972,7 @@ var JobManager = class {
|
|
|
906
972
|
const _id = new ObjectId(jobId);
|
|
907
973
|
const currentJobDoc = await this.ctx.collection.findOne({ _id });
|
|
908
974
|
if (!currentJobDoc) return null;
|
|
909
|
-
|
|
910
|
-
if (currentJob.status !== JobStatus.PENDING) throw new JobStateError(`Cannot reschedule job in status '${currentJob.status}'`, jobId, currentJob.status, "reschedule");
|
|
975
|
+
if (currentJobDoc["status"] !== JobStatus.PENDING) throw new JobStateError(`Cannot reschedule job in status '${currentJobDoc["status"]}'`, jobId, currentJobDoc["status"], "reschedule");
|
|
911
976
|
const result = await this.ctx.collection.findOneAndUpdate({
|
|
912
977
|
_id,
|
|
913
978
|
status: JobStatus.PENDING
|
|
@@ -969,21 +1034,20 @@ var JobManager = class {
|
|
|
969
1034
|
const cancelledIds = [];
|
|
970
1035
|
const cursor = this.ctx.collection.find(baseQuery);
|
|
971
1036
|
for await (const doc of cursor) {
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
if (job.status !== JobStatus.PENDING && job.status !== JobStatus.CANCELLED) {
|
|
1037
|
+
const jobId = doc._id.toString();
|
|
1038
|
+
if (doc["status"] !== JobStatus.PENDING && doc["status"] !== JobStatus.CANCELLED) {
|
|
975
1039
|
errors.push({
|
|
976
1040
|
jobId,
|
|
977
|
-
error: `Cannot cancel job in status '${
|
|
1041
|
+
error: `Cannot cancel job in status '${doc["status"]}'`
|
|
978
1042
|
});
|
|
979
1043
|
continue;
|
|
980
1044
|
}
|
|
981
|
-
if (
|
|
1045
|
+
if (doc["status"] === JobStatus.CANCELLED) {
|
|
982
1046
|
cancelledIds.push(jobId);
|
|
983
1047
|
continue;
|
|
984
1048
|
}
|
|
985
1049
|
if (await this.ctx.collection.findOneAndUpdate({
|
|
986
|
-
_id:
|
|
1050
|
+
_id: doc._id,
|
|
987
1051
|
status: JobStatus.PENDING
|
|
988
1052
|
}, { $set: {
|
|
989
1053
|
status: JobStatus.CANCELLED,
|
|
@@ -1027,17 +1091,16 @@ var JobManager = class {
|
|
|
1027
1091
|
const retriedIds = [];
|
|
1028
1092
|
const cursor = this.ctx.collection.find(baseQuery);
|
|
1029
1093
|
for await (const doc of cursor) {
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
if (job.status !== JobStatus.FAILED && job.status !== JobStatus.CANCELLED) {
|
|
1094
|
+
const jobId = doc._id.toString();
|
|
1095
|
+
if (doc["status"] !== JobStatus.FAILED && doc["status"] !== JobStatus.CANCELLED) {
|
|
1033
1096
|
errors.push({
|
|
1034
1097
|
jobId,
|
|
1035
|
-
error: `Cannot retry job in status '${
|
|
1098
|
+
error: `Cannot retry job in status '${doc["status"]}'`
|
|
1036
1099
|
});
|
|
1037
1100
|
continue;
|
|
1038
1101
|
}
|
|
1039
1102
|
if (await this.ctx.collection.findOneAndUpdate({
|
|
1040
|
-
_id:
|
|
1103
|
+
_id: doc._id,
|
|
1041
1104
|
status: { $in: [JobStatus.FAILED, JobStatus.CANCELLED] }
|
|
1042
1105
|
}, {
|
|
1043
1106
|
$set: {
|
|
@@ -1173,7 +1236,7 @@ var JobProcessor = class {
|
|
|
1173
1236
|
worker.activeJobs.set(job._id.toString(), job);
|
|
1174
1237
|
this.processJob(job, worker).catch((error) => {
|
|
1175
1238
|
this.ctx.emit("job:error", {
|
|
1176
|
-
error,
|
|
1239
|
+
error: toError(error),
|
|
1177
1240
|
job
|
|
1178
1241
|
});
|
|
1179
1242
|
});
|
|
@@ -1225,6 +1288,10 @@ var JobProcessor = class {
|
|
|
1225
1288
|
* both success and failure cases. On success, calls `completeJob()`. On failure,
|
|
1226
1289
|
* calls `failJob()` which implements exponential backoff retry logic.
|
|
1227
1290
|
*
|
|
1291
|
+
* Events are only emitted when the underlying atomic status transition succeeds,
|
|
1292
|
+
* ensuring event consumers receive reliable, consistent data backed by the actual
|
|
1293
|
+
* database state.
|
|
1294
|
+
*
|
|
1228
1295
|
* @param job - The job to process
|
|
1229
1296
|
* @param worker - The worker registration containing the handler and active job tracking
|
|
1230
1297
|
*/
|
|
@@ -1235,38 +1302,50 @@ var JobProcessor = class {
|
|
|
1235
1302
|
try {
|
|
1236
1303
|
await worker.handler(job);
|
|
1237
1304
|
const duration = Date.now() - startTime;
|
|
1238
|
-
await this.completeJob(job);
|
|
1239
|
-
this.ctx.emit("job:complete", {
|
|
1240
|
-
job,
|
|
1305
|
+
const updatedJob = await this.completeJob(job);
|
|
1306
|
+
if (updatedJob) this.ctx.emit("job:complete", {
|
|
1307
|
+
job: updatedJob,
|
|
1241
1308
|
duration
|
|
1242
1309
|
});
|
|
1243
1310
|
} catch (error) {
|
|
1244
1311
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1245
|
-
await this.failJob(job, err);
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
job,
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1312
|
+
const updatedJob = await this.failJob(job, err);
|
|
1313
|
+
if (updatedJob) {
|
|
1314
|
+
const willRetry = updatedJob.status === JobStatus.PENDING;
|
|
1315
|
+
this.ctx.emit("job:fail", {
|
|
1316
|
+
job: updatedJob,
|
|
1317
|
+
error: err,
|
|
1318
|
+
willRetry
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1252
1321
|
} finally {
|
|
1253
1322
|
worker.activeJobs.delete(jobId);
|
|
1254
1323
|
}
|
|
1255
1324
|
}
|
|
1256
1325
|
/**
|
|
1257
|
-
* Mark a job as completed successfully.
|
|
1326
|
+
* Mark a job as completed successfully using an atomic status transition.
|
|
1327
|
+
*
|
|
1328
|
+
* Uses `findOneAndUpdate` with `status: processing` and `claimedBy: instanceId`
|
|
1329
|
+
* preconditions to ensure the transition only occurs if the job is still owned by this
|
|
1330
|
+
* scheduler instance. Returns `null` if the job was concurrently modified (e.g., reclaimed
|
|
1331
|
+
* by another instance after stale recovery).
|
|
1258
1332
|
*
|
|
1259
1333
|
* For recurring jobs (with `repeatInterval`), schedules the next run based on the cron
|
|
1260
1334
|
* expression and resets `failCount` to 0. For one-time jobs, sets status to `completed`.
|
|
1261
1335
|
* Clears `lockedAt` and `failReason` fields in both cases.
|
|
1262
1336
|
*
|
|
1263
1337
|
* @param job - The job that completed successfully
|
|
1338
|
+
* @returns The updated job document, or `null` if the transition could not be applied
|
|
1264
1339
|
*/
|
|
1265
1340
|
async completeJob(job) {
|
|
1266
|
-
if (!isPersistedJob(job)) return;
|
|
1341
|
+
if (!isPersistedJob(job)) return null;
|
|
1267
1342
|
if (job.repeatInterval) {
|
|
1268
1343
|
const nextRunAt = getNextCronDate(job.repeatInterval);
|
|
1269
|
-
await this.ctx.collection.
|
|
1344
|
+
const result = await this.ctx.collection.findOneAndUpdate({
|
|
1345
|
+
_id: job._id,
|
|
1346
|
+
status: JobStatus.PROCESSING,
|
|
1347
|
+
claimedBy: this.ctx.instanceId
|
|
1348
|
+
}, {
|
|
1270
1349
|
$set: {
|
|
1271
1350
|
status: JobStatus.PENDING,
|
|
1272
1351
|
nextRunAt,
|
|
@@ -1280,61 +1359,59 @@ var JobProcessor = class {
|
|
|
1280
1359
|
heartbeatInterval: "",
|
|
1281
1360
|
failReason: ""
|
|
1282
1361
|
}
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
|
-
await this.ctx.collection.updateOne({ _id: job._id }, {
|
|
1286
|
-
$set: {
|
|
1287
|
-
status: JobStatus.COMPLETED,
|
|
1288
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1289
|
-
},
|
|
1290
|
-
$unset: {
|
|
1291
|
-
lockedAt: "",
|
|
1292
|
-
claimedBy: "",
|
|
1293
|
-
lastHeartbeat: "",
|
|
1294
|
-
heartbeatInterval: "",
|
|
1295
|
-
failReason: ""
|
|
1296
|
-
}
|
|
1297
|
-
});
|
|
1298
|
-
job.status = JobStatus.COMPLETED;
|
|
1362
|
+
}, { returnDocument: "after" });
|
|
1363
|
+
return result ? this.ctx.documentToPersistedJob(result) : null;
|
|
1299
1364
|
}
|
|
1365
|
+
const result = await this.ctx.collection.findOneAndUpdate({
|
|
1366
|
+
_id: job._id,
|
|
1367
|
+
status: JobStatus.PROCESSING,
|
|
1368
|
+
claimedBy: this.ctx.instanceId
|
|
1369
|
+
}, {
|
|
1370
|
+
$set: {
|
|
1371
|
+
status: JobStatus.COMPLETED,
|
|
1372
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1373
|
+
},
|
|
1374
|
+
$unset: {
|
|
1375
|
+
lockedAt: "",
|
|
1376
|
+
claimedBy: "",
|
|
1377
|
+
lastHeartbeat: "",
|
|
1378
|
+
heartbeatInterval: "",
|
|
1379
|
+
failReason: ""
|
|
1380
|
+
}
|
|
1381
|
+
}, { returnDocument: "after" });
|
|
1382
|
+
return result ? this.ctx.documentToPersistedJob(result) : null;
|
|
1300
1383
|
}
|
|
1301
1384
|
/**
|
|
1302
|
-
* Handle job failure with exponential backoff retry logic.
|
|
1385
|
+
* Handle job failure with exponential backoff retry logic using an atomic status transition.
|
|
1386
|
+
*
|
|
1387
|
+
* Uses `findOneAndUpdate` with `status: processing` and `claimedBy: instanceId`
|
|
1388
|
+
* preconditions to ensure the transition only occurs if the job is still owned by this
|
|
1389
|
+
* scheduler instance. Returns `null` if the job was concurrently modified (e.g., reclaimed
|
|
1390
|
+
* by another instance after stale recovery).
|
|
1303
1391
|
*
|
|
1304
1392
|
* Increments `failCount` and calculates next retry time using exponential backoff:
|
|
1305
|
-
* `nextRunAt = 2^failCount
|
|
1393
|
+
* `nextRunAt = 2^failCount * baseRetryInterval` (capped by optional `maxBackoffDelay`).
|
|
1306
1394
|
*
|
|
1307
1395
|
* If `failCount >= maxRetries`, marks job as permanently `failed`. Otherwise, resets
|
|
1308
1396
|
* to `pending` status for retry. Stores error message in `failReason` field.
|
|
1309
1397
|
*
|
|
1310
1398
|
* @param job - The job that failed
|
|
1311
1399
|
* @param error - The error that caused the failure
|
|
1400
|
+
* @returns The updated job document, or `null` if the transition could not be applied
|
|
1312
1401
|
*/
|
|
1313
1402
|
async failJob(job, error) {
|
|
1314
|
-
if (!isPersistedJob(job)) return;
|
|
1403
|
+
if (!isPersistedJob(job)) return null;
|
|
1315
1404
|
const newFailCount = job.failCount + 1;
|
|
1316
|
-
if (newFailCount >= this.ctx.options.maxRetries)
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
},
|
|
1323
|
-
$unset: {
|
|
1324
|
-
lockedAt: "",
|
|
1325
|
-
claimedBy: "",
|
|
1326
|
-
lastHeartbeat: "",
|
|
1327
|
-
heartbeatInterval: ""
|
|
1328
|
-
}
|
|
1329
|
-
});
|
|
1330
|
-
else {
|
|
1331
|
-
const nextRunAt = calculateBackoff(newFailCount, this.ctx.options.baseRetryInterval, this.ctx.options.maxBackoffDelay);
|
|
1332
|
-
await this.ctx.collection.updateOne({ _id: job._id }, {
|
|
1405
|
+
if (newFailCount >= this.ctx.options.maxRetries) {
|
|
1406
|
+
const result = await this.ctx.collection.findOneAndUpdate({
|
|
1407
|
+
_id: job._id,
|
|
1408
|
+
status: JobStatus.PROCESSING,
|
|
1409
|
+
claimedBy: this.ctx.instanceId
|
|
1410
|
+
}, {
|
|
1333
1411
|
$set: {
|
|
1334
|
-
status: JobStatus.
|
|
1412
|
+
status: JobStatus.FAILED,
|
|
1335
1413
|
failCount: newFailCount,
|
|
1336
1414
|
failReason: error.message,
|
|
1337
|
-
nextRunAt,
|
|
1338
1415
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1339
1416
|
},
|
|
1340
1417
|
$unset: {
|
|
@@ -1343,8 +1420,30 @@ var JobProcessor = class {
|
|
|
1343
1420
|
lastHeartbeat: "",
|
|
1344
1421
|
heartbeatInterval: ""
|
|
1345
1422
|
}
|
|
1346
|
-
});
|
|
1423
|
+
}, { returnDocument: "after" });
|
|
1424
|
+
return result ? this.ctx.documentToPersistedJob(result) : null;
|
|
1347
1425
|
}
|
|
1426
|
+
const nextRunAt = calculateBackoff(newFailCount, this.ctx.options.baseRetryInterval, this.ctx.options.maxBackoffDelay);
|
|
1427
|
+
const result = await this.ctx.collection.findOneAndUpdate({
|
|
1428
|
+
_id: job._id,
|
|
1429
|
+
status: JobStatus.PROCESSING,
|
|
1430
|
+
claimedBy: this.ctx.instanceId
|
|
1431
|
+
}, {
|
|
1432
|
+
$set: {
|
|
1433
|
+
status: JobStatus.PENDING,
|
|
1434
|
+
failCount: newFailCount,
|
|
1435
|
+
failReason: error.message,
|
|
1436
|
+
nextRunAt,
|
|
1437
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1438
|
+
},
|
|
1439
|
+
$unset: {
|
|
1440
|
+
lockedAt: "",
|
|
1441
|
+
claimedBy: "",
|
|
1442
|
+
lastHeartbeat: "",
|
|
1443
|
+
heartbeatInterval: ""
|
|
1444
|
+
}
|
|
1445
|
+
}, { returnDocument: "after" });
|
|
1446
|
+
return result ? this.ctx.documentToPersistedJob(result) : null;
|
|
1348
1447
|
}
|
|
1349
1448
|
/**
|
|
1350
1449
|
* Update heartbeats for all jobs claimed by this scheduler instance.
|
|
@@ -1960,7 +2059,8 @@ var Monque = class extends EventEmitter {
|
|
|
1960
2059
|
instanceConcurrency: options.instanceConcurrency ?? options.maxConcurrency,
|
|
1961
2060
|
schedulerInstanceId: options.schedulerInstanceId ?? randomUUID(),
|
|
1962
2061
|
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
1963
|
-
jobRetention: options.jobRetention
|
|
2062
|
+
jobRetention: options.jobRetention,
|
|
2063
|
+
skipIndexCreation: options.skipIndexCreation ?? false
|
|
1964
2064
|
};
|
|
1965
2065
|
}
|
|
1966
2066
|
/**
|
|
@@ -1973,7 +2073,7 @@ var Monque = class extends EventEmitter {
|
|
|
1973
2073
|
if (this.isInitialized) return;
|
|
1974
2074
|
try {
|
|
1975
2075
|
this.collection = this.db.collection(this.options.collectionName);
|
|
1976
|
-
await this.createIndexes();
|
|
2076
|
+
if (!this.options.skipIndexCreation) await this.createIndexes();
|
|
1977
2077
|
if (this.options.recoverStaleJobs) await this.recoverStaleJobs();
|
|
1978
2078
|
const ctx = this.buildContext();
|
|
1979
2079
|
this._scheduler = new JobScheduler(ctx);
|
|
@@ -2023,7 +2123,7 @@ var Monque = class extends EventEmitter {
|
|
|
2023
2123
|
workers: this.workers,
|
|
2024
2124
|
isRunning: () => this.isRunning,
|
|
2025
2125
|
emit: (event, payload) => this.emit(event, payload),
|
|
2026
|
-
documentToPersistedJob: (doc) =>
|
|
2126
|
+
documentToPersistedJob: (doc) => documentToPersistedJob(doc)
|
|
2027
2127
|
};
|
|
2028
2128
|
}
|
|
2029
2129
|
/**
|
|
@@ -2040,43 +2140,64 @@ var Monque = class extends EventEmitter {
|
|
|
2040
2140
|
*/
|
|
2041
2141
|
async createIndexes() {
|
|
2042
2142
|
if (!this.collection) throw new ConnectionError("Collection not initialized");
|
|
2043
|
-
await this.collection.
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
}, {
|
|
2051
|
-
unique: true,
|
|
2052
|
-
partialFilterExpression: {
|
|
2053
|
-
uniqueKey: { $exists: true },
|
|
2054
|
-
status: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] }
|
|
2143
|
+
await this.collection.createIndexes([
|
|
2144
|
+
{
|
|
2145
|
+
key: {
|
|
2146
|
+
status: 1,
|
|
2147
|
+
nextRunAt: 1
|
|
2148
|
+
},
|
|
2149
|
+
background: true
|
|
2055
2150
|
},
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2151
|
+
{
|
|
2152
|
+
key: {
|
|
2153
|
+
name: 1,
|
|
2154
|
+
uniqueKey: 1
|
|
2155
|
+
},
|
|
2156
|
+
unique: true,
|
|
2157
|
+
partialFilterExpression: {
|
|
2158
|
+
uniqueKey: { $exists: true },
|
|
2159
|
+
status: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] }
|
|
2160
|
+
},
|
|
2161
|
+
background: true
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
key: {
|
|
2165
|
+
name: 1,
|
|
2166
|
+
status: 1
|
|
2167
|
+
},
|
|
2168
|
+
background: true
|
|
2169
|
+
},
|
|
2170
|
+
{
|
|
2171
|
+
key: {
|
|
2172
|
+
claimedBy: 1,
|
|
2173
|
+
status: 1
|
|
2174
|
+
},
|
|
2175
|
+
background: true
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
key: {
|
|
2179
|
+
lastHeartbeat: 1,
|
|
2180
|
+
status: 1
|
|
2181
|
+
},
|
|
2182
|
+
background: true
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
key: {
|
|
2186
|
+
status: 1,
|
|
2187
|
+
nextRunAt: 1,
|
|
2188
|
+
claimedBy: 1
|
|
2189
|
+
},
|
|
2190
|
+
background: true
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
key: {
|
|
2194
|
+
status: 1,
|
|
2195
|
+
lockedAt: 1,
|
|
2196
|
+
lastHeartbeat: 1
|
|
2197
|
+
},
|
|
2198
|
+
background: true
|
|
2199
|
+
}
|
|
2200
|
+
]);
|
|
2080
2201
|
}
|
|
2081
2202
|
/**
|
|
2082
2203
|
* Recover stale jobs that were left in 'processing' status.
|
|
@@ -2678,27 +2799,27 @@ var Monque = class extends EventEmitter {
|
|
|
2678
2799
|
this.changeStreamHandler.setup();
|
|
2679
2800
|
this.pollIntervalId = setInterval(() => {
|
|
2680
2801
|
this.processor.poll().catch((error) => {
|
|
2681
|
-
this.emit("job:error", { error });
|
|
2802
|
+
this.emit("job:error", { error: toError(error) });
|
|
2682
2803
|
});
|
|
2683
2804
|
}, this.options.pollInterval);
|
|
2684
2805
|
this.heartbeatIntervalId = setInterval(() => {
|
|
2685
2806
|
this.processor.updateHeartbeats().catch((error) => {
|
|
2686
|
-
this.emit("job:error", { error });
|
|
2807
|
+
this.emit("job:error", { error: toError(error) });
|
|
2687
2808
|
});
|
|
2688
2809
|
}, this.options.heartbeatInterval);
|
|
2689
2810
|
if (this.options.jobRetention) {
|
|
2690
2811
|
const interval = this.options.jobRetention.interval ?? DEFAULTS.retentionInterval;
|
|
2691
2812
|
this.cleanupJobs().catch((error) => {
|
|
2692
|
-
this.emit("job:error", { error });
|
|
2813
|
+
this.emit("job:error", { error: toError(error) });
|
|
2693
2814
|
});
|
|
2694
2815
|
this.cleanupIntervalId = setInterval(() => {
|
|
2695
2816
|
this.cleanupJobs().catch((error) => {
|
|
2696
|
-
this.emit("job:error", { error });
|
|
2817
|
+
this.emit("job:error", { error: toError(error) });
|
|
2697
2818
|
});
|
|
2698
2819
|
}, interval);
|
|
2699
2820
|
}
|
|
2700
2821
|
this.processor.poll().catch((error) => {
|
|
2701
|
-
this.emit("job:error", { error });
|
|
2822
|
+
this.emit("job:error", { error: toError(error) });
|
|
2702
2823
|
});
|
|
2703
2824
|
}
|
|
2704
2825
|
/**
|
|
@@ -2856,37 +2977,6 @@ var Monque = class extends EventEmitter {
|
|
|
2856
2977
|
return activeJobs;
|
|
2857
2978
|
}
|
|
2858
2979
|
/**
|
|
2859
|
-
* Convert a MongoDB document to a typed PersistedJob object.
|
|
2860
|
-
*
|
|
2861
|
-
* Maps raw MongoDB document fields to the strongly-typed `PersistedJob<T>` interface,
|
|
2862
|
-
* ensuring type safety and handling optional fields (`lockedAt`, `failReason`, etc.).
|
|
2863
|
-
*
|
|
2864
|
-
* @private
|
|
2865
|
-
* @template T - The job data payload type
|
|
2866
|
-
* @param doc - The raw MongoDB document with `_id`
|
|
2867
|
-
* @returns A strongly-typed PersistedJob object with guaranteed `_id`
|
|
2868
|
-
*/
|
|
2869
|
-
documentToPersistedJob(doc) {
|
|
2870
|
-
const job = {
|
|
2871
|
-
_id: doc._id,
|
|
2872
|
-
name: doc["name"],
|
|
2873
|
-
data: doc["data"],
|
|
2874
|
-
status: doc["status"],
|
|
2875
|
-
nextRunAt: doc["nextRunAt"],
|
|
2876
|
-
failCount: doc["failCount"],
|
|
2877
|
-
createdAt: doc["createdAt"],
|
|
2878
|
-
updatedAt: doc["updatedAt"]
|
|
2879
|
-
};
|
|
2880
|
-
if (doc["lockedAt"] !== void 0) job.lockedAt = doc["lockedAt"];
|
|
2881
|
-
if (doc["claimedBy"] !== void 0) job.claimedBy = doc["claimedBy"];
|
|
2882
|
-
if (doc["lastHeartbeat"] !== void 0) job.lastHeartbeat = doc["lastHeartbeat"];
|
|
2883
|
-
if (doc["heartbeatInterval"] !== void 0) job.heartbeatInterval = doc["heartbeatInterval"];
|
|
2884
|
-
if (doc["failReason"] !== void 0) job.failReason = doc["failReason"];
|
|
2885
|
-
if (doc["repeatInterval"] !== void 0) job.repeatInterval = doc["repeatInterval"];
|
|
2886
|
-
if (doc["uniqueKey"] !== void 0) job.uniqueKey = doc["uniqueKey"];
|
|
2887
|
-
return job;
|
|
2888
|
-
}
|
|
2889
|
-
/**
|
|
2890
2980
|
* Type-safe event emitter methods
|
|
2891
2981
|
*/
|
|
2892
2982
|
emit(event, payload) {
|