@monque/core 1.4.0 → 1.5.1
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 +19 -0
- package/dist/index.cjs +372 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -22
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +99 -22
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +372 -214
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/events/types.ts +2 -2
- package/src/index.ts +1 -0
- package/src/jobs/document-to-persisted-job.ts +16 -16
- package/src/scheduler/monque.ts +101 -96
- package/src/scheduler/services/index.ts +1 -0
- package/src/scheduler/services/job-manager.ts +100 -116
- package/src/scheduler/services/job-query.ts +81 -36
- package/src/scheduler/services/job-scheduler.ts +42 -2
- package/src/scheduler/services/lifecycle-manager.ts +154 -0
- package/src/scheduler/services/types.ts +5 -1
- package/src/scheduler/types.ts +23 -0
- package/src/shared/errors.ts +31 -0
- package/src/shared/index.ts +1 -0
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ObjectId } from "mongodb";
|
|
1
|
+
import { BSON, ObjectId } from "mongodb";
|
|
2
2
|
import { CronExpressionParser } from "cron-parser";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
|
-
|
|
6
5
|
//#region src/jobs/document-to-persisted-job.ts
|
|
7
6
|
/**
|
|
8
7
|
* Convert a raw MongoDB document to a strongly-typed {@link PersistedJob}.
|
|
@@ -35,7 +34,6 @@ function documentToPersistedJob(doc) {
|
|
|
35
34
|
if (doc["uniqueKey"] !== void 0) job.uniqueKey = doc["uniqueKey"];
|
|
36
35
|
return job;
|
|
37
36
|
}
|
|
38
|
-
|
|
39
37
|
//#endregion
|
|
40
38
|
//#region src/jobs/types.ts
|
|
41
39
|
/**
|
|
@@ -74,7 +72,6 @@ const CursorDirection = {
|
|
|
74
72
|
FORWARD: "forward",
|
|
75
73
|
BACKWARD: "backward"
|
|
76
74
|
};
|
|
77
|
-
|
|
78
75
|
//#endregion
|
|
79
76
|
//#region src/jobs/guards.ts
|
|
80
77
|
/**
|
|
@@ -287,7 +284,6 @@ function isCancelledJob(job) {
|
|
|
287
284
|
function isRecurringJob(job) {
|
|
288
285
|
return job.repeatInterval !== void 0 && job.repeatInterval !== null;
|
|
289
286
|
}
|
|
290
|
-
|
|
291
287
|
//#endregion
|
|
292
288
|
//#region src/shared/errors.ts
|
|
293
289
|
/**
|
|
@@ -479,7 +475,32 @@ var AggregationTimeoutError = class AggregationTimeoutError extends MonqueError
|
|
|
479
475
|
if (Error.captureStackTrace) Error.captureStackTrace(this, AggregationTimeoutError);
|
|
480
476
|
}
|
|
481
477
|
};
|
|
482
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Error thrown when a job payload exceeds the configured maximum BSON byte size.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```typescript
|
|
483
|
+
* const monque = new Monque(db, { maxPayloadSize: 1_000_000 }); // 1 MB
|
|
484
|
+
*
|
|
485
|
+
* try {
|
|
486
|
+
* await monque.enqueue('job', hugePayload);
|
|
487
|
+
* } catch (error) {
|
|
488
|
+
* if (error instanceof PayloadTooLargeError) {
|
|
489
|
+
* console.error(`Payload ${error.actualSize} bytes exceeds limit ${error.maxSize} bytes`);
|
|
490
|
+
* }
|
|
491
|
+
* }
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
var PayloadTooLargeError = class PayloadTooLargeError extends MonqueError {
|
|
495
|
+
constructor(message, actualSize, maxSize) {
|
|
496
|
+
super(message);
|
|
497
|
+
this.actualSize = actualSize;
|
|
498
|
+
this.maxSize = maxSize;
|
|
499
|
+
this.name = "PayloadTooLargeError";
|
|
500
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
501
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, PayloadTooLargeError);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
483
504
|
//#endregion
|
|
484
505
|
//#region src/shared/utils/backoff.ts
|
|
485
506
|
/**
|
|
@@ -521,7 +542,7 @@ const DEFAULT_MAX_BACKOFF_DELAY = 1440 * 60 * 1e3;
|
|
|
521
542
|
* ```
|
|
522
543
|
*/
|
|
523
544
|
function calculateBackoff(failCount, baseInterval = DEFAULT_BASE_INTERVAL, maxDelay) {
|
|
524
|
-
const effectiveMaxDelay = maxDelay ??
|
|
545
|
+
const effectiveMaxDelay = maxDelay ?? 864e5;
|
|
525
546
|
let delay = 2 ** failCount * baseInterval;
|
|
526
547
|
if (delay > effectiveMaxDelay) delay = effectiveMaxDelay;
|
|
527
548
|
return new Date(Date.now() + delay);
|
|
@@ -535,12 +556,11 @@ function calculateBackoff(failCount, baseInterval = DEFAULT_BASE_INTERVAL, maxDe
|
|
|
535
556
|
* @returns The delay in milliseconds
|
|
536
557
|
*/
|
|
537
558
|
function calculateBackoffDelay(failCount, baseInterval = DEFAULT_BASE_INTERVAL, maxDelay) {
|
|
538
|
-
const effectiveMaxDelay = maxDelay ??
|
|
559
|
+
const effectiveMaxDelay = maxDelay ?? 864e5;
|
|
539
560
|
let delay = 2 ** failCount * baseInterval;
|
|
540
561
|
if (delay > effectiveMaxDelay) delay = effectiveMaxDelay;
|
|
541
562
|
return delay;
|
|
542
563
|
}
|
|
543
|
-
|
|
544
564
|
//#endregion
|
|
545
565
|
//#region src/shared/utils/cron.ts
|
|
546
566
|
/**
|
|
@@ -594,7 +614,6 @@ function validateCronExpression(expression) {
|
|
|
594
614
|
function handleCronParseError(expression, error) {
|
|
595
615
|
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)`);
|
|
596
616
|
}
|
|
597
|
-
|
|
598
617
|
//#endregion
|
|
599
618
|
//#region src/shared/utils/error.ts
|
|
600
619
|
/**
|
|
@@ -627,7 +646,6 @@ function toError(value) {
|
|
|
627
646
|
return /* @__PURE__ */ new Error(`Unserializable value (${detail})`);
|
|
628
647
|
}
|
|
629
648
|
}
|
|
630
|
-
|
|
631
649
|
//#endregion
|
|
632
650
|
//#region src/scheduler/helpers.ts
|
|
633
651
|
/**
|
|
@@ -693,7 +711,6 @@ function decodeCursor(cursor) {
|
|
|
693
711
|
throw new InvalidCursorError("Invalid cursor payload");
|
|
694
712
|
}
|
|
695
713
|
}
|
|
696
|
-
|
|
697
714
|
//#endregion
|
|
698
715
|
//#region src/scheduler/services/change-stream-handler.ts
|
|
699
716
|
/**
|
|
@@ -849,7 +866,6 @@ var ChangeStreamHandler = class {
|
|
|
849
866
|
return this.usingChangeStreams;
|
|
850
867
|
}
|
|
851
868
|
};
|
|
852
|
-
|
|
853
869
|
//#endregion
|
|
854
870
|
//#region src/scheduler/services/job-manager.ts
|
|
855
871
|
/**
|
|
@@ -1010,14 +1026,15 @@ var JobManager = class {
|
|
|
1010
1026
|
return false;
|
|
1011
1027
|
}
|
|
1012
1028
|
/**
|
|
1013
|
-
* Cancel multiple jobs matching the given filter.
|
|
1029
|
+
* Cancel multiple jobs matching the given filter via a single updateMany call.
|
|
1014
1030
|
*
|
|
1015
|
-
* Only cancels jobs in 'pending' status
|
|
1016
|
-
*
|
|
1031
|
+
* Only cancels jobs in 'pending' status — the status guard is applied regardless
|
|
1032
|
+
* of what the filter specifies. Jobs in other states are silently skipped (not
|
|
1033
|
+
* matched by the query). Emits a 'jobs:cancelled' event with the count of
|
|
1017
1034
|
* successfully cancelled jobs.
|
|
1018
1035
|
*
|
|
1019
1036
|
* @param filter - Selector for which jobs to cancel (name, status, date range)
|
|
1020
|
-
* @returns Result with count of cancelled jobs
|
|
1037
|
+
* @returns Result with count of cancelled jobs (errors array always empty for bulk ops)
|
|
1021
1038
|
*
|
|
1022
1039
|
* @example Cancel all pending jobs for a queue
|
|
1023
1040
|
* ```typescript
|
|
@@ -1029,53 +1046,39 @@ var JobManager = class {
|
|
|
1029
1046
|
* ```
|
|
1030
1047
|
*/
|
|
1031
1048
|
async cancelJobs(filter) {
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
});
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
if (doc["status"] === JobStatus.CANCELLED) {
|
|
1046
|
-
cancelledIds.push(jobId);
|
|
1047
|
-
continue;
|
|
1048
|
-
}
|
|
1049
|
-
if (await this.ctx.collection.findOneAndUpdate({
|
|
1050
|
-
_id: doc._id,
|
|
1051
|
-
status: JobStatus.PENDING
|
|
1052
|
-
}, { $set: {
|
|
1049
|
+
const query = buildSelectorQuery(filter);
|
|
1050
|
+
if (filter.status !== void 0) {
|
|
1051
|
+
if (!(Array.isArray(filter.status) ? filter.status : [filter.status]).includes(JobStatus.PENDING)) return {
|
|
1052
|
+
count: 0,
|
|
1053
|
+
errors: []
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
query["status"] = JobStatus.PENDING;
|
|
1057
|
+
try {
|
|
1058
|
+
const count = (await this.ctx.collection.updateMany(query, { $set: {
|
|
1053
1059
|
status: JobStatus.CANCELLED,
|
|
1054
1060
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1055
|
-
} }
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1061
|
+
} })).modifiedCount;
|
|
1062
|
+
if (count > 0) this.ctx.emit("jobs:cancelled", { count });
|
|
1063
|
+
return {
|
|
1064
|
+
count,
|
|
1065
|
+
errors: []
|
|
1066
|
+
};
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error instanceof MonqueError) throw error;
|
|
1069
|
+
throw new ConnectionError(`Failed to cancel jobs: ${error instanceof Error ? error.message : "Unknown error during cancelJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1060
1070
|
}
|
|
1061
|
-
if (cancelledIds.length > 0) this.ctx.emit("jobs:cancelled", {
|
|
1062
|
-
jobIds: cancelledIds,
|
|
1063
|
-
count: cancelledIds.length
|
|
1064
|
-
});
|
|
1065
|
-
return {
|
|
1066
|
-
count: cancelledIds.length,
|
|
1067
|
-
errors
|
|
1068
|
-
};
|
|
1069
1071
|
}
|
|
1070
1072
|
/**
|
|
1071
|
-
* Retry multiple jobs matching the given filter.
|
|
1073
|
+
* Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.
|
|
1072
1074
|
*
|
|
1073
|
-
* Only retries jobs in 'failed' or 'cancelled' status
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1075
|
+
* Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied
|
|
1076
|
+
* regardless of what the filter specifies. Jobs in other states are silently skipped.
|
|
1077
|
+
* Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.
|
|
1078
|
+
* Emits a 'jobs:retried' event with the count of successfully retried jobs.
|
|
1076
1079
|
*
|
|
1077
1080
|
* @param filter - Selector for which jobs to retry (name, status, date range)
|
|
1078
|
-
* @returns Result with count of retried jobs
|
|
1081
|
+
* @returns Result with count of retried jobs (errors array always empty for bulk ops)
|
|
1079
1082
|
*
|
|
1080
1083
|
* @example Retry all failed jobs
|
|
1081
1084
|
* ```typescript
|
|
@@ -1086,50 +1089,39 @@ var JobManager = class {
|
|
|
1086
1089
|
* ```
|
|
1087
1090
|
*/
|
|
1088
1091
|
async retryJobs(filter) {
|
|
1089
|
-
const
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
jobId,
|
|
1122
|
-
error: "Job status changed during retry attempt"
|
|
1123
|
-
});
|
|
1092
|
+
const query = buildSelectorQuery(filter);
|
|
1093
|
+
const retryable = [JobStatus.FAILED, JobStatus.CANCELLED];
|
|
1094
|
+
if (filter.status !== void 0) {
|
|
1095
|
+
const allowed = (Array.isArray(filter.status) ? filter.status : [filter.status]).filter((status) => status === JobStatus.FAILED || status === JobStatus.CANCELLED);
|
|
1096
|
+
if (allowed.length === 0) return {
|
|
1097
|
+
count: 0,
|
|
1098
|
+
errors: []
|
|
1099
|
+
};
|
|
1100
|
+
query["status"] = allowed.length === 1 ? allowed[0] : { $in: allowed };
|
|
1101
|
+
} else query["status"] = { $in: retryable };
|
|
1102
|
+
const spreadWindowMs = 3e4;
|
|
1103
|
+
try {
|
|
1104
|
+
const count = (await this.ctx.collection.updateMany(query, [{ $set: {
|
|
1105
|
+
status: JobStatus.PENDING,
|
|
1106
|
+
failCount: 0,
|
|
1107
|
+
nextRunAt: { $add: [/* @__PURE__ */ new Date(), { $multiply: [{ $rand: {} }, spreadWindowMs] }] },
|
|
1108
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1109
|
+
} }, { $unset: [
|
|
1110
|
+
"failReason",
|
|
1111
|
+
"lockedAt",
|
|
1112
|
+
"claimedBy",
|
|
1113
|
+
"lastHeartbeat",
|
|
1114
|
+
"heartbeatInterval"
|
|
1115
|
+
] }])).modifiedCount;
|
|
1116
|
+
if (count > 0) this.ctx.emit("jobs:retried", { count });
|
|
1117
|
+
return {
|
|
1118
|
+
count,
|
|
1119
|
+
errors: []
|
|
1120
|
+
};
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (error instanceof MonqueError) throw error;
|
|
1123
|
+
throw new ConnectionError(`Failed to retry jobs: ${error instanceof Error ? error.message : "Unknown error during retryJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1124
1124
|
}
|
|
1125
|
-
if (retriedIds.length > 0) this.ctx.emit("jobs:retried", {
|
|
1126
|
-
jobIds: retriedIds,
|
|
1127
|
-
count: retriedIds.length
|
|
1128
|
-
});
|
|
1129
|
-
return {
|
|
1130
|
-
count: retriedIds.length,
|
|
1131
|
-
errors
|
|
1132
|
-
};
|
|
1133
1125
|
}
|
|
1134
1126
|
/**
|
|
1135
1127
|
* Delete multiple jobs matching the given filter.
|
|
@@ -1153,15 +1145,19 @@ var JobManager = class {
|
|
|
1153
1145
|
*/
|
|
1154
1146
|
async deleteJobs(filter) {
|
|
1155
1147
|
const query = buildSelectorQuery(filter);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1148
|
+
try {
|
|
1149
|
+
const result = await this.ctx.collection.deleteMany(query);
|
|
1150
|
+
if (result.deletedCount > 0) this.ctx.emit("jobs:deleted", { count: result.deletedCount });
|
|
1151
|
+
return {
|
|
1152
|
+
count: result.deletedCount,
|
|
1153
|
+
errors: []
|
|
1154
|
+
};
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
if (error instanceof MonqueError) throw error;
|
|
1157
|
+
throw new ConnectionError(`Failed to delete jobs: ${error instanceof Error ? error.message : "Unknown error during deleteJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1158
|
+
}
|
|
1162
1159
|
}
|
|
1163
1160
|
};
|
|
1164
|
-
|
|
1165
1161
|
//#endregion
|
|
1166
1162
|
//#region src/scheduler/services/job-processor.ts
|
|
1167
1163
|
/**
|
|
@@ -1466,7 +1462,6 @@ var JobProcessor = class {
|
|
|
1466
1462
|
} });
|
|
1467
1463
|
}
|
|
1468
1464
|
};
|
|
1469
|
-
|
|
1470
1465
|
//#endregion
|
|
1471
1466
|
//#region src/scheduler/services/job-query.ts
|
|
1472
1467
|
/**
|
|
@@ -1477,7 +1472,9 @@ var JobProcessor = class {
|
|
|
1477
1472
|
*
|
|
1478
1473
|
* @internal Not part of public API - use Monque class methods instead.
|
|
1479
1474
|
*/
|
|
1480
|
-
var JobQueryService = class {
|
|
1475
|
+
var JobQueryService = class JobQueryService {
|
|
1476
|
+
statsCache = /* @__PURE__ */ new Map();
|
|
1477
|
+
static MAX_CACHE_SIZE = 100;
|
|
1481
1478
|
constructor(ctx) {
|
|
1482
1479
|
this.ctx = ctx;
|
|
1483
1480
|
}
|
|
@@ -1656,11 +1653,22 @@ var JobQueryService = class {
|
|
|
1656
1653
|
};
|
|
1657
1654
|
}
|
|
1658
1655
|
/**
|
|
1656
|
+
* Clear all cached getQueueStats() results.
|
|
1657
|
+
* Called on scheduler stop() for clean state on restart.
|
|
1658
|
+
* @internal
|
|
1659
|
+
*/
|
|
1660
|
+
clearStatsCache() {
|
|
1661
|
+
this.statsCache.clear();
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1659
1664
|
* Get aggregate statistics for the job queue.
|
|
1660
1665
|
*
|
|
1661
1666
|
* Uses MongoDB aggregation pipeline for efficient server-side calculation.
|
|
1662
1667
|
* Returns counts per status and optional average processing duration for completed jobs.
|
|
1663
1668
|
*
|
|
1669
|
+
* Results are cached per unique filter with a configurable TTL (default 5s).
|
|
1670
|
+
* Set `statsCacheTtlMs: 0` to disable caching.
|
|
1671
|
+
*
|
|
1664
1672
|
* @param filter - Optional filter to scope statistics by job name
|
|
1665
1673
|
* @returns Promise resolving to queue statistics
|
|
1666
1674
|
* @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
|
|
@@ -1679,6 +1687,12 @@ var JobQueryService = class {
|
|
|
1679
1687
|
* ```
|
|
1680
1688
|
*/
|
|
1681
1689
|
async getQueueStats(filter) {
|
|
1690
|
+
const ttl = this.ctx.options.statsCacheTtlMs;
|
|
1691
|
+
const cacheKey = filter?.name ?? "";
|
|
1692
|
+
if (ttl > 0) {
|
|
1693
|
+
const cached = this.statsCache.get(cacheKey);
|
|
1694
|
+
if (cached && cached.expiresAt > Date.now()) return { ...cached.data };
|
|
1695
|
+
}
|
|
1682
1696
|
const matchStage = {};
|
|
1683
1697
|
if (filter?.name) matchStage["name"] = filter.name;
|
|
1684
1698
|
const pipeline = [...Object.keys(matchStage).length > 0 ? [{ $match: matchStage }] : [], { $facet: {
|
|
@@ -1702,35 +1716,47 @@ var JobQueryService = class {
|
|
|
1702
1716
|
cancelled: 0,
|
|
1703
1717
|
total: 0
|
|
1704
1718
|
};
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1719
|
+
if (result) {
|
|
1720
|
+
const statusCounts = result["statusCounts"];
|
|
1721
|
+
for (const entry of statusCounts) {
|
|
1722
|
+
const status = entry._id;
|
|
1723
|
+
const count = entry.count;
|
|
1724
|
+
switch (status) {
|
|
1725
|
+
case JobStatus.PENDING:
|
|
1726
|
+
stats.pending = count;
|
|
1727
|
+
break;
|
|
1728
|
+
case JobStatus.PROCESSING:
|
|
1729
|
+
stats.processing = count;
|
|
1730
|
+
break;
|
|
1731
|
+
case JobStatus.COMPLETED:
|
|
1732
|
+
stats.completed = count;
|
|
1733
|
+
break;
|
|
1734
|
+
case JobStatus.FAILED:
|
|
1735
|
+
stats.failed = count;
|
|
1736
|
+
break;
|
|
1737
|
+
case JobStatus.CANCELLED:
|
|
1738
|
+
stats.cancelled = count;
|
|
1739
|
+
break;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const totalResult = result["total"];
|
|
1743
|
+
if (totalResult.length > 0 && totalResult[0]) stats.total = totalResult[0].count;
|
|
1744
|
+
const avgDurationResult = result["avgDuration"];
|
|
1745
|
+
if (avgDurationResult.length > 0 && avgDurationResult[0]) {
|
|
1746
|
+
const avgMs = avgDurationResult[0].avgMs;
|
|
1747
|
+
if (typeof avgMs === "number" && !Number.isNaN(avgMs)) stats.avgProcessingDurationMs = Math.round(avgMs);
|
|
1726
1748
|
}
|
|
1727
1749
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1750
|
+
if (ttl > 0) {
|
|
1751
|
+
this.statsCache.delete(cacheKey);
|
|
1752
|
+
if (this.statsCache.size >= JobQueryService.MAX_CACHE_SIZE) {
|
|
1753
|
+
const oldestKey = this.statsCache.keys().next().value;
|
|
1754
|
+
if (oldestKey !== void 0) this.statsCache.delete(oldestKey);
|
|
1755
|
+
}
|
|
1756
|
+
this.statsCache.set(cacheKey, {
|
|
1757
|
+
data: { ...stats },
|
|
1758
|
+
expiresAt: Date.now() + ttl
|
|
1759
|
+
});
|
|
1734
1760
|
}
|
|
1735
1761
|
return stats;
|
|
1736
1762
|
} catch (error) {
|
|
@@ -1739,7 +1765,6 @@ var JobQueryService = class {
|
|
|
1739
1765
|
}
|
|
1740
1766
|
}
|
|
1741
1767
|
};
|
|
1742
|
-
|
|
1743
1768
|
//#endregion
|
|
1744
1769
|
//#region src/scheduler/services/job-scheduler.ts
|
|
1745
1770
|
/**
|
|
@@ -1755,6 +1780,26 @@ var JobScheduler = class {
|
|
|
1755
1780
|
this.ctx = ctx;
|
|
1756
1781
|
}
|
|
1757
1782
|
/**
|
|
1783
|
+
* Validate that the job data payload does not exceed the configured maximum BSON byte size.
|
|
1784
|
+
*
|
|
1785
|
+
* @param data - The job data payload to validate
|
|
1786
|
+
* @throws {PayloadTooLargeError} If the payload exceeds `maxPayloadSize`
|
|
1787
|
+
*/
|
|
1788
|
+
validatePayloadSize(data) {
|
|
1789
|
+
const maxSize = this.ctx.options.maxPayloadSize;
|
|
1790
|
+
if (maxSize === void 0) return;
|
|
1791
|
+
let size;
|
|
1792
|
+
try {
|
|
1793
|
+
size = BSON.calculateObjectSize({ data });
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
1796
|
+
const sizeError = new PayloadTooLargeError(`Failed to calculate job payload size: ${cause.message}`, -1, maxSize);
|
|
1797
|
+
sizeError.cause = cause;
|
|
1798
|
+
throw sizeError;
|
|
1799
|
+
}
|
|
1800
|
+
if (size > maxSize) throw new PayloadTooLargeError(`Job payload exceeds maximum size: ${size} bytes > ${maxSize} bytes`, size, maxSize);
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1758
1803
|
* Enqueue a job for processing.
|
|
1759
1804
|
*
|
|
1760
1805
|
* Jobs are stored in MongoDB and processed by registered workers. Supports
|
|
@@ -1772,6 +1817,7 @@ var JobScheduler = class {
|
|
|
1772
1817
|
* @param options - Scheduling and deduplication options
|
|
1773
1818
|
* @returns Promise resolving to the created or existing job document
|
|
1774
1819
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
1820
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
1775
1821
|
*
|
|
1776
1822
|
* @example Basic job enqueueing
|
|
1777
1823
|
* ```typescript
|
|
@@ -1799,6 +1845,7 @@ var JobScheduler = class {
|
|
|
1799
1845
|
* ```
|
|
1800
1846
|
*/
|
|
1801
1847
|
async enqueue(name, data, options = {}) {
|
|
1848
|
+
this.validatePayloadSize(data);
|
|
1802
1849
|
const now = /* @__PURE__ */ new Date();
|
|
1803
1850
|
const job = {
|
|
1804
1851
|
name,
|
|
@@ -1884,6 +1931,7 @@ var JobScheduler = class {
|
|
|
1884
1931
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
1885
1932
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
1886
1933
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
1934
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
1887
1935
|
*
|
|
1888
1936
|
* @example Hourly cleanup job
|
|
1889
1937
|
* ```typescript
|
|
@@ -1909,6 +1957,7 @@ var JobScheduler = class {
|
|
|
1909
1957
|
* ```
|
|
1910
1958
|
*/
|
|
1911
1959
|
async schedule(cron, name, data, options = {}) {
|
|
1960
|
+
this.validatePayloadSize(data);
|
|
1912
1961
|
const nextRunAt = getNextCronDate(cron);
|
|
1913
1962
|
const now = /* @__PURE__ */ new Date();
|
|
1914
1963
|
const job = {
|
|
@@ -1946,7 +1995,113 @@ var JobScheduler = class {
|
|
|
1946
1995
|
}
|
|
1947
1996
|
}
|
|
1948
1997
|
};
|
|
1949
|
-
|
|
1998
|
+
//#endregion
|
|
1999
|
+
//#region src/scheduler/services/lifecycle-manager.ts
|
|
2000
|
+
/**
|
|
2001
|
+
* Default retention check interval (1 hour).
|
|
2002
|
+
*/
|
|
2003
|
+
const DEFAULT_RETENTION_INTERVAL = 36e5;
|
|
2004
|
+
/**
|
|
2005
|
+
* Manages scheduler lifecycle timers and job cleanup.
|
|
2006
|
+
*
|
|
2007
|
+
* Owns poll interval, heartbeat interval, cleanup interval, and the
|
|
2008
|
+
* cleanupJobs logic. Extracted from Monque to keep the facade thin.
|
|
2009
|
+
*
|
|
2010
|
+
* @internal Not part of public API.
|
|
2011
|
+
*/
|
|
2012
|
+
var LifecycleManager = class {
|
|
2013
|
+
ctx;
|
|
2014
|
+
pollIntervalId = null;
|
|
2015
|
+
heartbeatIntervalId = null;
|
|
2016
|
+
cleanupIntervalId = null;
|
|
2017
|
+
constructor(ctx) {
|
|
2018
|
+
this.ctx = ctx;
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Start all lifecycle timers.
|
|
2022
|
+
*
|
|
2023
|
+
* Sets up poll interval, heartbeat interval, and (if configured)
|
|
2024
|
+
* cleanup interval. Runs an initial poll immediately.
|
|
2025
|
+
*
|
|
2026
|
+
* @param callbacks - Functions to invoke on each timer tick
|
|
2027
|
+
*/
|
|
2028
|
+
startTimers(callbacks) {
|
|
2029
|
+
this.pollIntervalId = setInterval(() => {
|
|
2030
|
+
callbacks.poll().catch((error) => {
|
|
2031
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2032
|
+
});
|
|
2033
|
+
}, this.ctx.options.pollInterval);
|
|
2034
|
+
this.heartbeatIntervalId = setInterval(() => {
|
|
2035
|
+
callbacks.updateHeartbeats().catch((error) => {
|
|
2036
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2037
|
+
});
|
|
2038
|
+
}, this.ctx.options.heartbeatInterval);
|
|
2039
|
+
if (this.ctx.options.jobRetention) {
|
|
2040
|
+
const interval = this.ctx.options.jobRetention.interval ?? DEFAULT_RETENTION_INTERVAL;
|
|
2041
|
+
this.cleanupJobs().catch((error) => {
|
|
2042
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2043
|
+
});
|
|
2044
|
+
this.cleanupIntervalId = setInterval(() => {
|
|
2045
|
+
this.cleanupJobs().catch((error) => {
|
|
2046
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2047
|
+
});
|
|
2048
|
+
}, interval);
|
|
2049
|
+
}
|
|
2050
|
+
callbacks.poll().catch((error) => {
|
|
2051
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Stop all lifecycle timers.
|
|
2056
|
+
*
|
|
2057
|
+
* Clears poll, heartbeat, and cleanup intervals.
|
|
2058
|
+
*/
|
|
2059
|
+
stopTimers() {
|
|
2060
|
+
if (this.cleanupIntervalId) {
|
|
2061
|
+
clearInterval(this.cleanupIntervalId);
|
|
2062
|
+
this.cleanupIntervalId = null;
|
|
2063
|
+
}
|
|
2064
|
+
if (this.pollIntervalId) {
|
|
2065
|
+
clearInterval(this.pollIntervalId);
|
|
2066
|
+
this.pollIntervalId = null;
|
|
2067
|
+
}
|
|
2068
|
+
if (this.heartbeatIntervalId) {
|
|
2069
|
+
clearInterval(this.heartbeatIntervalId);
|
|
2070
|
+
this.heartbeatIntervalId = null;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Clean up old completed and failed jobs based on retention policy.
|
|
2075
|
+
*
|
|
2076
|
+
* - Removes completed jobs older than `jobRetention.completed`
|
|
2077
|
+
* - Removes failed jobs older than `jobRetention.failed`
|
|
2078
|
+
*
|
|
2079
|
+
* The cleanup runs concurrently for both statuses if configured.
|
|
2080
|
+
*
|
|
2081
|
+
* @returns Promise resolving when all deletion operations complete
|
|
2082
|
+
*/
|
|
2083
|
+
async cleanupJobs() {
|
|
2084
|
+
if (!this.ctx.options.jobRetention) return;
|
|
2085
|
+
const { completed, failed } = this.ctx.options.jobRetention;
|
|
2086
|
+
const now = Date.now();
|
|
2087
|
+
const deletions = [];
|
|
2088
|
+
if (completed != null) {
|
|
2089
|
+
const cutoff = new Date(now - completed);
|
|
2090
|
+
deletions.push(this.ctx.collection.deleteMany({
|
|
2091
|
+
status: JobStatus.COMPLETED,
|
|
2092
|
+
updatedAt: { $lt: cutoff }
|
|
2093
|
+
}));
|
|
2094
|
+
}
|
|
2095
|
+
if (failed != null) {
|
|
2096
|
+
const cutoff = new Date(now - failed);
|
|
2097
|
+
deletions.push(this.ctx.collection.deleteMany({
|
|
2098
|
+
status: JobStatus.FAILED,
|
|
2099
|
+
updatedAt: { $lt: cutoff }
|
|
2100
|
+
}));
|
|
2101
|
+
}
|
|
2102
|
+
if (deletions.length > 0) await Promise.all(deletions);
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
1950
2105
|
//#endregion
|
|
1951
2106
|
//#region src/scheduler/monque.ts
|
|
1952
2107
|
/**
|
|
@@ -2033,9 +2188,6 @@ var Monque = class extends EventEmitter {
|
|
|
2033
2188
|
options;
|
|
2034
2189
|
collection = null;
|
|
2035
2190
|
workers = /* @__PURE__ */ new Map();
|
|
2036
|
-
pollIntervalId = null;
|
|
2037
|
-
heartbeatIntervalId = null;
|
|
2038
|
-
cleanupIntervalId = null;
|
|
2039
2191
|
isRunning = false;
|
|
2040
2192
|
isInitialized = false;
|
|
2041
2193
|
_scheduler = null;
|
|
@@ -2043,6 +2195,7 @@ var Monque = class extends EventEmitter {
|
|
|
2043
2195
|
_query = null;
|
|
2044
2196
|
_processor = null;
|
|
2045
2197
|
_changeStreamHandler = null;
|
|
2198
|
+
_lifecycleManager = null;
|
|
2046
2199
|
constructor(db, options = {}) {
|
|
2047
2200
|
super();
|
|
2048
2201
|
this.db = db;
|
|
@@ -2060,7 +2213,9 @@ var Monque = class extends EventEmitter {
|
|
|
2060
2213
|
schedulerInstanceId: options.schedulerInstanceId ?? randomUUID(),
|
|
2061
2214
|
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
2062
2215
|
jobRetention: options.jobRetention,
|
|
2063
|
-
skipIndexCreation: options.skipIndexCreation ?? false
|
|
2216
|
+
skipIndexCreation: options.skipIndexCreation ?? false,
|
|
2217
|
+
maxPayloadSize: options.maxPayloadSize,
|
|
2218
|
+
statsCacheTtlMs: options.statsCacheTtlMs ?? 5e3
|
|
2064
2219
|
};
|
|
2065
2220
|
}
|
|
2066
2221
|
/**
|
|
@@ -2075,12 +2230,14 @@ var Monque = class extends EventEmitter {
|
|
|
2075
2230
|
this.collection = this.db.collection(this.options.collectionName);
|
|
2076
2231
|
if (!this.options.skipIndexCreation) await this.createIndexes();
|
|
2077
2232
|
if (this.options.recoverStaleJobs) await this.recoverStaleJobs();
|
|
2233
|
+
await this.checkInstanceCollision();
|
|
2078
2234
|
const ctx = this.buildContext();
|
|
2079
2235
|
this._scheduler = new JobScheduler(ctx);
|
|
2080
2236
|
this._manager = new JobManager(ctx);
|
|
2081
2237
|
this._query = new JobQueryService(ctx);
|
|
2082
2238
|
this._processor = new JobProcessor(ctx);
|
|
2083
2239
|
this._changeStreamHandler = new ChangeStreamHandler(ctx, () => this.processor.poll());
|
|
2240
|
+
this._lifecycleManager = new LifecycleManager(ctx);
|
|
2084
2241
|
this.isInitialized = true;
|
|
2085
2242
|
} catch (error) {
|
|
2086
2243
|
throw new ConnectionError(`Failed to initialize Monque: ${error instanceof Error ? error.message : "Unknown error during initialization"}`);
|
|
@@ -2111,6 +2268,11 @@ var Monque = class extends EventEmitter {
|
|
|
2111
2268
|
if (!this._changeStreamHandler) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
2112
2269
|
return this._changeStreamHandler;
|
|
2113
2270
|
}
|
|
2271
|
+
/** @throws {ConnectionError} if not initialized */
|
|
2272
|
+
get lifecycleManager() {
|
|
2273
|
+
if (!this._lifecycleManager) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
2274
|
+
return this._lifecycleManager;
|
|
2275
|
+
}
|
|
2114
2276
|
/**
|
|
2115
2277
|
* Build the shared context for internal services.
|
|
2116
2278
|
*/
|
|
@@ -2225,35 +2387,23 @@ var Monque = class extends EventEmitter {
|
|
|
2225
2387
|
if (result.modifiedCount > 0) this.emit("stale:recovered", { count: result.modifiedCount });
|
|
2226
2388
|
}
|
|
2227
2389
|
/**
|
|
2228
|
-
*
|
|
2390
|
+
* Check if another active instance is using the same schedulerInstanceId.
|
|
2391
|
+
* Uses heartbeat staleness to distinguish active instances from crashed ones.
|
|
2229
2392
|
*
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2393
|
+
* Called after stale recovery to avoid false positives: stale recovery resets
|
|
2394
|
+
* jobs with old `lockedAt`, so only jobs with recent heartbeats remain.
|
|
2232
2395
|
*
|
|
2233
|
-
*
|
|
2234
|
-
*
|
|
2235
|
-
* @returns Promise resolving when all deletion operations complete
|
|
2396
|
+
* @throws {ConnectionError} If an active instance with the same ID is detected
|
|
2236
2397
|
*/
|
|
2237
|
-
async
|
|
2238
|
-
if (!this.collection
|
|
2239
|
-
const
|
|
2240
|
-
const
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
updatedAt: { $lt: cutoff }
|
|
2247
|
-
}));
|
|
2248
|
-
}
|
|
2249
|
-
if (failed) {
|
|
2250
|
-
const cutoff = new Date(now - failed);
|
|
2251
|
-
deletions.push(this.collection.deleteMany({
|
|
2252
|
-
status: JobStatus.FAILED,
|
|
2253
|
-
updatedAt: { $lt: cutoff }
|
|
2254
|
-
}));
|
|
2255
|
-
}
|
|
2256
|
-
if (deletions.length > 0) await Promise.all(deletions);
|
|
2398
|
+
async checkInstanceCollision() {
|
|
2399
|
+
if (!this.collection) return;
|
|
2400
|
+
const aliveThreshold = /* @__PURE__ */ new Date(Date.now() - this.options.heartbeatInterval * 2);
|
|
2401
|
+
const activeJob = await this.collection.findOne({
|
|
2402
|
+
claimedBy: this.options.schedulerInstanceId,
|
|
2403
|
+
status: JobStatus.PROCESSING,
|
|
2404
|
+
lastHeartbeat: { $gte: aliveThreshold }
|
|
2405
|
+
});
|
|
2406
|
+
if (activeJob) throw new ConnectionError(`Another active Monque instance is using schedulerInstanceId "${this.options.schedulerInstanceId}". Found processing job "${activeJob["name"]}" with recent heartbeat. Use a unique schedulerInstanceId or wait for the other instance to stop.`);
|
|
2257
2407
|
}
|
|
2258
2408
|
/**
|
|
2259
2409
|
* Enqueue a job for processing.
|
|
@@ -2273,6 +2423,7 @@ var Monque = class extends EventEmitter {
|
|
|
2273
2423
|
* @param options - Scheduling and deduplication options
|
|
2274
2424
|
* @returns Promise resolving to the created or existing job document
|
|
2275
2425
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2426
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
2276
2427
|
*
|
|
2277
2428
|
* @example Basic job enqueueing
|
|
2278
2429
|
* ```typescript
|
|
@@ -2298,6 +2449,8 @@ var Monque = class extends EventEmitter {
|
|
|
2298
2449
|
* });
|
|
2299
2450
|
* // Subsequent enqueues with same uniqueKey return existing pending/processing job
|
|
2300
2451
|
* ```
|
|
2452
|
+
*
|
|
2453
|
+
* @see {@link JobScheduler.enqueue}
|
|
2301
2454
|
*/
|
|
2302
2455
|
async enqueue(name, data, options = {}) {
|
|
2303
2456
|
this.ensureInitialized();
|
|
@@ -2330,6 +2483,8 @@ var Monque = class extends EventEmitter {
|
|
|
2330
2483
|
* await monque.now('process-order', { orderId: order.id });
|
|
2331
2484
|
* return order; // Return immediately, processing happens async
|
|
2332
2485
|
* ```
|
|
2486
|
+
*
|
|
2487
|
+
* @see {@link JobScheduler.now}
|
|
2333
2488
|
*/
|
|
2334
2489
|
async now(name, data) {
|
|
2335
2490
|
this.ensureInitialized();
|
|
@@ -2355,6 +2510,7 @@ var Monque = class extends EventEmitter {
|
|
|
2355
2510
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
2356
2511
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
2357
2512
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2513
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
2358
2514
|
*
|
|
2359
2515
|
* @example Hourly cleanup job
|
|
2360
2516
|
* ```typescript
|
|
@@ -2378,6 +2534,8 @@ var Monque = class extends EventEmitter {
|
|
|
2378
2534
|
* recipients: ['analytics@example.com']
|
|
2379
2535
|
* });
|
|
2380
2536
|
* ```
|
|
2537
|
+
*
|
|
2538
|
+
* @see {@link JobScheduler.schedule}
|
|
2381
2539
|
*/
|
|
2382
2540
|
async schedule(cron, name, data, options = {}) {
|
|
2383
2541
|
this.ensureInitialized();
|
|
@@ -2399,6 +2557,8 @@ var Monque = class extends EventEmitter {
|
|
|
2399
2557
|
* const job = await monque.enqueue('report', { type: 'daily' });
|
|
2400
2558
|
* await monque.cancelJob(job._id.toString());
|
|
2401
2559
|
* ```
|
|
2560
|
+
*
|
|
2561
|
+
* @see {@link JobManager.cancelJob}
|
|
2402
2562
|
*/
|
|
2403
2563
|
async cancelJob(jobId) {
|
|
2404
2564
|
this.ensureInitialized();
|
|
@@ -2421,6 +2581,8 @@ var Monque = class extends EventEmitter {
|
|
|
2421
2581
|
* await monque.retryJob(job._id.toString());
|
|
2422
2582
|
* });
|
|
2423
2583
|
* ```
|
|
2584
|
+
*
|
|
2585
|
+
* @see {@link JobManager.retryJob}
|
|
2424
2586
|
*/
|
|
2425
2587
|
async retryJob(jobId) {
|
|
2426
2588
|
this.ensureInitialized();
|
|
@@ -2441,6 +2603,8 @@ var Monque = class extends EventEmitter {
|
|
|
2441
2603
|
* const nextHour = new Date(Date.now() + 60 * 60 * 1000);
|
|
2442
2604
|
* await monque.rescheduleJob(jobId, nextHour);
|
|
2443
2605
|
* ```
|
|
2606
|
+
*
|
|
2607
|
+
* @see {@link JobManager.rescheduleJob}
|
|
2444
2608
|
*/
|
|
2445
2609
|
async rescheduleJob(jobId, runAt) {
|
|
2446
2610
|
this.ensureInitialized();
|
|
@@ -2462,20 +2626,23 @@ var Monque = class extends EventEmitter {
|
|
|
2462
2626
|
* console.log('Job permanently removed');
|
|
2463
2627
|
* }
|
|
2464
2628
|
* ```
|
|
2629
|
+
*
|
|
2630
|
+
* @see {@link JobManager.deleteJob}
|
|
2465
2631
|
*/
|
|
2466
2632
|
async deleteJob(jobId) {
|
|
2467
2633
|
this.ensureInitialized();
|
|
2468
2634
|
return this.manager.deleteJob(jobId);
|
|
2469
2635
|
}
|
|
2470
2636
|
/**
|
|
2471
|
-
* Cancel multiple jobs matching the given filter.
|
|
2637
|
+
* Cancel multiple jobs matching the given filter via a single updateMany call.
|
|
2472
2638
|
*
|
|
2473
|
-
* Only cancels jobs in 'pending' status
|
|
2474
|
-
*
|
|
2639
|
+
* Only cancels jobs in 'pending' status — the status guard is applied regardless
|
|
2640
|
+
* of what the filter specifies. Jobs in other states are silently skipped (not
|
|
2641
|
+
* matched by the query). Emits a 'jobs:cancelled' event with the count of
|
|
2475
2642
|
* successfully cancelled jobs.
|
|
2476
2643
|
*
|
|
2477
2644
|
* @param filter - Selector for which jobs to cancel (name, status, date range)
|
|
2478
|
-
* @returns Result with count of cancelled jobs
|
|
2645
|
+
* @returns Result with count of cancelled jobs (errors array always empty for bulk ops)
|
|
2479
2646
|
*
|
|
2480
2647
|
* @example Cancel all pending jobs for a queue
|
|
2481
2648
|
* ```typescript
|
|
@@ -2485,20 +2652,23 @@ var Monque = class extends EventEmitter {
|
|
|
2485
2652
|
* });
|
|
2486
2653
|
* console.log(`Cancelled ${result.count} jobs`);
|
|
2487
2654
|
* ```
|
|
2655
|
+
*
|
|
2656
|
+
* @see {@link JobManager.cancelJobs}
|
|
2488
2657
|
*/
|
|
2489
2658
|
async cancelJobs(filter) {
|
|
2490
2659
|
this.ensureInitialized();
|
|
2491
2660
|
return this.manager.cancelJobs(filter);
|
|
2492
2661
|
}
|
|
2493
2662
|
/**
|
|
2494
|
-
* Retry multiple jobs matching the given filter.
|
|
2663
|
+
* Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.
|
|
2495
2664
|
*
|
|
2496
|
-
* Only retries jobs in 'failed' or 'cancelled' status
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2665
|
+
* Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied
|
|
2666
|
+
* regardless of what the filter specifies. Jobs in other states are silently skipped.
|
|
2667
|
+
* Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.
|
|
2668
|
+
* Emits a 'jobs:retried' event with the count of successfully retried jobs.
|
|
2499
2669
|
*
|
|
2500
2670
|
* @param filter - Selector for which jobs to retry (name, status, date range)
|
|
2501
|
-
* @returns Result with count of retried jobs
|
|
2671
|
+
* @returns Result with count of retried jobs (errors array always empty for bulk ops)
|
|
2502
2672
|
*
|
|
2503
2673
|
* @example Retry all failed jobs
|
|
2504
2674
|
* ```typescript
|
|
@@ -2507,6 +2677,8 @@ var Monque = class extends EventEmitter {
|
|
|
2507
2677
|
* });
|
|
2508
2678
|
* console.log(`Retried ${result.count} jobs`);
|
|
2509
2679
|
* ```
|
|
2680
|
+
*
|
|
2681
|
+
* @see {@link JobManager.retryJobs}
|
|
2510
2682
|
*/
|
|
2511
2683
|
async retryJobs(filter) {
|
|
2512
2684
|
this.ensureInitialized();
|
|
@@ -2516,6 +2688,7 @@ var Monque = class extends EventEmitter {
|
|
|
2516
2688
|
* Delete multiple jobs matching the given filter.
|
|
2517
2689
|
*
|
|
2518
2690
|
* Deletes jobs in any status. Uses a batch delete for efficiency.
|
|
2691
|
+
* Emits a 'jobs:deleted' event with the count of deleted jobs.
|
|
2519
2692
|
* Does not emit individual 'job:deleted' events to avoid noise.
|
|
2520
2693
|
*
|
|
2521
2694
|
* @param filter - Selector for which jobs to delete (name, status, date range)
|
|
@@ -2530,6 +2703,8 @@ var Monque = class extends EventEmitter {
|
|
|
2530
2703
|
* });
|
|
2531
2704
|
* console.log(`Deleted ${result.count} jobs`);
|
|
2532
2705
|
* ```
|
|
2706
|
+
*
|
|
2707
|
+
* @see {@link JobManager.deleteJobs}
|
|
2533
2708
|
*/
|
|
2534
2709
|
async deleteJobs(filter) {
|
|
2535
2710
|
this.ensureInitialized();
|
|
@@ -2565,6 +2740,8 @@ var Monque = class extends EventEmitter {
|
|
|
2565
2740
|
* res.json(job);
|
|
2566
2741
|
* });
|
|
2567
2742
|
* ```
|
|
2743
|
+
*
|
|
2744
|
+
* @see {@link JobQueryService.getJob}
|
|
2568
2745
|
*/
|
|
2569
2746
|
async getJob(id) {
|
|
2570
2747
|
this.ensureInitialized();
|
|
@@ -2611,6 +2788,8 @@ var Monque = class extends EventEmitter {
|
|
|
2611
2788
|
* const jobs = await monque.getJobs();
|
|
2612
2789
|
* const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));
|
|
2613
2790
|
* ```
|
|
2791
|
+
*
|
|
2792
|
+
* @see {@link JobQueryService.getJobs}
|
|
2614
2793
|
*/
|
|
2615
2794
|
async getJobs(filter = {}) {
|
|
2616
2795
|
this.ensureInitialized();
|
|
@@ -2644,6 +2823,8 @@ var Monque = class extends EventEmitter {
|
|
|
2644
2823
|
* });
|
|
2645
2824
|
* }
|
|
2646
2825
|
* ```
|
|
2826
|
+
*
|
|
2827
|
+
* @see {@link JobQueryService.getJobsWithCursor}
|
|
2647
2828
|
*/
|
|
2648
2829
|
async getJobsWithCursor(options = {}) {
|
|
2649
2830
|
this.ensureInitialized();
|
|
@@ -2655,6 +2836,9 @@ var Monque = class extends EventEmitter {
|
|
|
2655
2836
|
* Uses MongoDB aggregation pipeline for efficient server-side calculation.
|
|
2656
2837
|
* Returns counts per status and optional average processing duration for completed jobs.
|
|
2657
2838
|
*
|
|
2839
|
+
* Results are cached per unique filter with a configurable TTL (default 5s).
|
|
2840
|
+
* Set `statsCacheTtlMs: 0` to disable caching.
|
|
2841
|
+
*
|
|
2658
2842
|
* @param filter - Optional filter to scope statistics by job name
|
|
2659
2843
|
* @returns Promise resolving to queue statistics
|
|
2660
2844
|
* @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
|
|
@@ -2671,6 +2855,8 @@ var Monque = class extends EventEmitter {
|
|
|
2671
2855
|
* const emailStats = await monque.getQueueStats({ name: 'send-email' });
|
|
2672
2856
|
* console.log(`${emailStats.total} email jobs in queue`);
|
|
2673
2857
|
* ```
|
|
2858
|
+
*
|
|
2859
|
+
* @see {@link JobQueryService.getQueueStats}
|
|
2674
2860
|
*/
|
|
2675
2861
|
async getQueueStats(filter) {
|
|
2676
2862
|
this.ensureInitialized();
|
|
@@ -2797,29 +2983,9 @@ var Monque = class extends EventEmitter {
|
|
|
2797
2983
|
if (!this.isInitialized) throw new ConnectionError("Monque not initialized. Call initialize() before start().");
|
|
2798
2984
|
this.isRunning = true;
|
|
2799
2985
|
this.changeStreamHandler.setup();
|
|
2800
|
-
this.
|
|
2801
|
-
this.processor.poll()
|
|
2802
|
-
|
|
2803
|
-
});
|
|
2804
|
-
}, this.options.pollInterval);
|
|
2805
|
-
this.heartbeatIntervalId = setInterval(() => {
|
|
2806
|
-
this.processor.updateHeartbeats().catch((error) => {
|
|
2807
|
-
this.emit("job:error", { error: toError(error) });
|
|
2808
|
-
});
|
|
2809
|
-
}, this.options.heartbeatInterval);
|
|
2810
|
-
if (this.options.jobRetention) {
|
|
2811
|
-
const interval = this.options.jobRetention.interval ?? DEFAULTS.retentionInterval;
|
|
2812
|
-
this.cleanupJobs().catch((error) => {
|
|
2813
|
-
this.emit("job:error", { error: toError(error) });
|
|
2814
|
-
});
|
|
2815
|
-
this.cleanupIntervalId = setInterval(() => {
|
|
2816
|
-
this.cleanupJobs().catch((error) => {
|
|
2817
|
-
this.emit("job:error", { error: toError(error) });
|
|
2818
|
-
});
|
|
2819
|
-
}, interval);
|
|
2820
|
-
}
|
|
2821
|
-
this.processor.poll().catch((error) => {
|
|
2822
|
-
this.emit("job:error", { error: toError(error) });
|
|
2986
|
+
this.lifecycleManager.startTimers({
|
|
2987
|
+
poll: () => this.processor.poll(),
|
|
2988
|
+
updateHeartbeats: () => this.processor.updateHeartbeats()
|
|
2823
2989
|
});
|
|
2824
2990
|
}
|
|
2825
2991
|
/**
|
|
@@ -2857,20 +3023,12 @@ var Monque = class extends EventEmitter {
|
|
|
2857
3023
|
*/
|
|
2858
3024
|
async stop() {
|
|
2859
3025
|
if (!this.isRunning) return;
|
|
3026
|
+
this.lifecycleManager.stopTimers();
|
|
2860
3027
|
this.isRunning = false;
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
}
|
|
2866
|
-
if (this.pollIntervalId) {
|
|
2867
|
-
clearInterval(this.pollIntervalId);
|
|
2868
|
-
this.pollIntervalId = null;
|
|
2869
|
-
}
|
|
2870
|
-
if (this.heartbeatIntervalId) {
|
|
2871
|
-
clearInterval(this.heartbeatIntervalId);
|
|
2872
|
-
this.heartbeatIntervalId = null;
|
|
2873
|
-
}
|
|
3028
|
+
this._query?.clearStatsCache();
|
|
3029
|
+
try {
|
|
3030
|
+
await this.changeStreamHandler.close();
|
|
3031
|
+
} catch {}
|
|
2874
3032
|
if (this.getActiveJobs().length === 0) return;
|
|
2875
3033
|
let checkInterval;
|
|
2876
3034
|
const waitForJobs = new Promise((resolve) => {
|
|
@@ -2992,7 +3150,7 @@ var Monque = class extends EventEmitter {
|
|
|
2992
3150
|
return super.off(event, listener);
|
|
2993
3151
|
}
|
|
2994
3152
|
};
|
|
2995
|
-
|
|
2996
3153
|
//#endregion
|
|
2997
|
-
export { AggregationTimeoutError, ConnectionError, CursorDirection, DEFAULT_BASE_INTERVAL, DEFAULT_MAX_BACKOFF_DELAY, InvalidCronError, InvalidCursorError, JobStateError, JobStatus, Monque, MonqueError, ShutdownTimeoutError, WorkerRegistrationError, calculateBackoff, calculateBackoffDelay, getNextCronDate, isCancelledJob, isCompletedJob, isFailedJob, isPendingJob, isPersistedJob, isProcessingJob, isRecurringJob, isValidJobStatus, validateCronExpression };
|
|
3154
|
+
export { AggregationTimeoutError, ConnectionError, CursorDirection, DEFAULT_BASE_INTERVAL, DEFAULT_MAX_BACKOFF_DELAY, InvalidCronError, InvalidCursorError, JobStateError, JobStatus, Monque, MonqueError, PayloadTooLargeError, ShutdownTimeoutError, WorkerRegistrationError, calculateBackoff, calculateBackoffDelay, getNextCronDate, isCancelledJob, isCompletedJob, isFailedJob, isPendingJob, isPersistedJob, isProcessingJob, isRecurringJob, isValidJobStatus, validateCronExpression };
|
|
3155
|
+
|
|
2998
3156
|
//# sourceMappingURL=index.mjs.map
|