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