@monque/core 1.4.0 → 1.5.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 +13 -0
- package/dist/index.cjs +369 -195
- 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 +370 -197
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- 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 +98 -95
- 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,4 +1,4 @@
|
|
|
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";
|
|
@@ -479,6 +479,32 @@ var AggregationTimeoutError = class AggregationTimeoutError extends MonqueError
|
|
|
479
479
|
if (Error.captureStackTrace) Error.captureStackTrace(this, AggregationTimeoutError);
|
|
480
480
|
}
|
|
481
481
|
};
|
|
482
|
+
/**
|
|
483
|
+
* Error thrown when a job payload exceeds the configured maximum BSON byte size.
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```typescript
|
|
487
|
+
* const monque = new Monque(db, { maxPayloadSize: 1_000_000 }); // 1 MB
|
|
488
|
+
*
|
|
489
|
+
* try {
|
|
490
|
+
* await monque.enqueue('job', hugePayload);
|
|
491
|
+
* } catch (error) {
|
|
492
|
+
* if (error instanceof PayloadTooLargeError) {
|
|
493
|
+
* console.error(`Payload ${error.actualSize} bytes exceeds limit ${error.maxSize} bytes`);
|
|
494
|
+
* }
|
|
495
|
+
* }
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
var PayloadTooLargeError = class PayloadTooLargeError extends MonqueError {
|
|
499
|
+
constructor(message, actualSize, maxSize) {
|
|
500
|
+
super(message);
|
|
501
|
+
this.actualSize = actualSize;
|
|
502
|
+
this.maxSize = maxSize;
|
|
503
|
+
this.name = "PayloadTooLargeError";
|
|
504
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
505
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, PayloadTooLargeError);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
482
508
|
|
|
483
509
|
//#endregion
|
|
484
510
|
//#region src/shared/utils/backoff.ts
|
|
@@ -1010,14 +1036,15 @@ var JobManager = class {
|
|
|
1010
1036
|
return false;
|
|
1011
1037
|
}
|
|
1012
1038
|
/**
|
|
1013
|
-
* Cancel multiple jobs matching the given filter.
|
|
1039
|
+
* Cancel multiple jobs matching the given filter via a single updateMany call.
|
|
1014
1040
|
*
|
|
1015
|
-
* Only cancels jobs in 'pending' status
|
|
1016
|
-
*
|
|
1041
|
+
* Only cancels jobs in 'pending' status — the status guard is applied regardless
|
|
1042
|
+
* of what the filter specifies. Jobs in other states are silently skipped (not
|
|
1043
|
+
* matched by the query). Emits a 'jobs:cancelled' event with the count of
|
|
1017
1044
|
* successfully cancelled jobs.
|
|
1018
1045
|
*
|
|
1019
1046
|
* @param filter - Selector for which jobs to cancel (name, status, date range)
|
|
1020
|
-
* @returns Result with count of cancelled jobs
|
|
1047
|
+
* @returns Result with count of cancelled jobs (errors array always empty for bulk ops)
|
|
1021
1048
|
*
|
|
1022
1049
|
* @example Cancel all pending jobs for a queue
|
|
1023
1050
|
* ```typescript
|
|
@@ -1029,53 +1056,39 @@ var JobManager = class {
|
|
|
1029
1056
|
* ```
|
|
1030
1057
|
*/
|
|
1031
1058
|
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: {
|
|
1059
|
+
const query = buildSelectorQuery(filter);
|
|
1060
|
+
if (filter.status !== void 0) {
|
|
1061
|
+
if (!(Array.isArray(filter.status) ? filter.status : [filter.status]).includes(JobStatus.PENDING)) return {
|
|
1062
|
+
count: 0,
|
|
1063
|
+
errors: []
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
query["status"] = JobStatus.PENDING;
|
|
1067
|
+
try {
|
|
1068
|
+
const count = (await this.ctx.collection.updateMany(query, { $set: {
|
|
1053
1069
|
status: JobStatus.CANCELLED,
|
|
1054
1070
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1055
|
-
} }
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1071
|
+
} })).modifiedCount;
|
|
1072
|
+
if (count > 0) this.ctx.emit("jobs:cancelled", { count });
|
|
1073
|
+
return {
|
|
1074
|
+
count,
|
|
1075
|
+
errors: []
|
|
1076
|
+
};
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
if (error instanceof MonqueError) throw error;
|
|
1079
|
+
throw new ConnectionError(`Failed to cancel jobs: ${error instanceof Error ? error.message : "Unknown error during cancelJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1060
1080
|
}
|
|
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
1081
|
}
|
|
1070
1082
|
/**
|
|
1071
|
-
* Retry multiple jobs matching the given filter.
|
|
1083
|
+
* Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.
|
|
1072
1084
|
*
|
|
1073
|
-
* Only retries jobs in 'failed' or 'cancelled' status
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1085
|
+
* Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied
|
|
1086
|
+
* regardless of what the filter specifies. Jobs in other states are silently skipped.
|
|
1087
|
+
* Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.
|
|
1088
|
+
* Emits a 'jobs:retried' event with the count of successfully retried jobs.
|
|
1076
1089
|
*
|
|
1077
1090
|
* @param filter - Selector for which jobs to retry (name, status, date range)
|
|
1078
|
-
* @returns Result with count of retried jobs
|
|
1091
|
+
* @returns Result with count of retried jobs (errors array always empty for bulk ops)
|
|
1079
1092
|
*
|
|
1080
1093
|
* @example Retry all failed jobs
|
|
1081
1094
|
* ```typescript
|
|
@@ -1086,50 +1099,39 @@ var JobManager = class {
|
|
|
1086
1099
|
* ```
|
|
1087
1100
|
*/
|
|
1088
1101
|
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
|
-
});
|
|
1102
|
+
const query = buildSelectorQuery(filter);
|
|
1103
|
+
const retryable = [JobStatus.FAILED, JobStatus.CANCELLED];
|
|
1104
|
+
if (filter.status !== void 0) {
|
|
1105
|
+
const allowed = (Array.isArray(filter.status) ? filter.status : [filter.status]).filter((status) => status === JobStatus.FAILED || status === JobStatus.CANCELLED);
|
|
1106
|
+
if (allowed.length === 0) return {
|
|
1107
|
+
count: 0,
|
|
1108
|
+
errors: []
|
|
1109
|
+
};
|
|
1110
|
+
query["status"] = allowed.length === 1 ? allowed[0] : { $in: allowed };
|
|
1111
|
+
} else query["status"] = { $in: retryable };
|
|
1112
|
+
const spreadWindowMs = 3e4;
|
|
1113
|
+
try {
|
|
1114
|
+
const count = (await this.ctx.collection.updateMany(query, [{ $set: {
|
|
1115
|
+
status: JobStatus.PENDING,
|
|
1116
|
+
failCount: 0,
|
|
1117
|
+
nextRunAt: { $add: [/* @__PURE__ */ new Date(), { $multiply: [{ $rand: {} }, spreadWindowMs] }] },
|
|
1118
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1119
|
+
} }, { $unset: [
|
|
1120
|
+
"failReason",
|
|
1121
|
+
"lockedAt",
|
|
1122
|
+
"claimedBy",
|
|
1123
|
+
"lastHeartbeat",
|
|
1124
|
+
"heartbeatInterval"
|
|
1125
|
+
] }])).modifiedCount;
|
|
1126
|
+
if (count > 0) this.ctx.emit("jobs:retried", { count });
|
|
1127
|
+
return {
|
|
1128
|
+
count,
|
|
1129
|
+
errors: []
|
|
1130
|
+
};
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
if (error instanceof MonqueError) throw error;
|
|
1133
|
+
throw new ConnectionError(`Failed to retry jobs: ${error instanceof Error ? error.message : "Unknown error during retryJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1124
1134
|
}
|
|
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
1135
|
}
|
|
1134
1136
|
/**
|
|
1135
1137
|
* Delete multiple jobs matching the given filter.
|
|
@@ -1153,12 +1155,17 @@ var JobManager = class {
|
|
|
1153
1155
|
*/
|
|
1154
1156
|
async deleteJobs(filter) {
|
|
1155
1157
|
const query = buildSelectorQuery(filter);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1158
|
+
try {
|
|
1159
|
+
const result = await this.ctx.collection.deleteMany(query);
|
|
1160
|
+
if (result.deletedCount > 0) this.ctx.emit("jobs:deleted", { count: result.deletedCount });
|
|
1161
|
+
return {
|
|
1162
|
+
count: result.deletedCount,
|
|
1163
|
+
errors: []
|
|
1164
|
+
};
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
if (error instanceof MonqueError) throw error;
|
|
1167
|
+
throw new ConnectionError(`Failed to delete jobs: ${error instanceof Error ? error.message : "Unknown error during deleteJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1168
|
+
}
|
|
1162
1169
|
}
|
|
1163
1170
|
};
|
|
1164
1171
|
|
|
@@ -1477,7 +1484,9 @@ var JobProcessor = class {
|
|
|
1477
1484
|
*
|
|
1478
1485
|
* @internal Not part of public API - use Monque class methods instead.
|
|
1479
1486
|
*/
|
|
1480
|
-
var JobQueryService = class {
|
|
1487
|
+
var JobQueryService = class JobQueryService {
|
|
1488
|
+
statsCache = /* @__PURE__ */ new Map();
|
|
1489
|
+
static MAX_CACHE_SIZE = 100;
|
|
1481
1490
|
constructor(ctx) {
|
|
1482
1491
|
this.ctx = ctx;
|
|
1483
1492
|
}
|
|
@@ -1656,11 +1665,22 @@ var JobQueryService = class {
|
|
|
1656
1665
|
};
|
|
1657
1666
|
}
|
|
1658
1667
|
/**
|
|
1668
|
+
* Clear all cached getQueueStats() results.
|
|
1669
|
+
* Called on scheduler stop() for clean state on restart.
|
|
1670
|
+
* @internal
|
|
1671
|
+
*/
|
|
1672
|
+
clearStatsCache() {
|
|
1673
|
+
this.statsCache.clear();
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1659
1676
|
* Get aggregate statistics for the job queue.
|
|
1660
1677
|
*
|
|
1661
1678
|
* Uses MongoDB aggregation pipeline for efficient server-side calculation.
|
|
1662
1679
|
* Returns counts per status and optional average processing duration for completed jobs.
|
|
1663
1680
|
*
|
|
1681
|
+
* Results are cached per unique filter with a configurable TTL (default 5s).
|
|
1682
|
+
* Set `statsCacheTtlMs: 0` to disable caching.
|
|
1683
|
+
*
|
|
1664
1684
|
* @param filter - Optional filter to scope statistics by job name
|
|
1665
1685
|
* @returns Promise resolving to queue statistics
|
|
1666
1686
|
* @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
|
|
@@ -1679,6 +1699,12 @@ var JobQueryService = class {
|
|
|
1679
1699
|
* ```
|
|
1680
1700
|
*/
|
|
1681
1701
|
async getQueueStats(filter) {
|
|
1702
|
+
const ttl = this.ctx.options.statsCacheTtlMs;
|
|
1703
|
+
const cacheKey = filter?.name ?? "";
|
|
1704
|
+
if (ttl > 0) {
|
|
1705
|
+
const cached = this.statsCache.get(cacheKey);
|
|
1706
|
+
if (cached && cached.expiresAt > Date.now()) return { ...cached.data };
|
|
1707
|
+
}
|
|
1682
1708
|
const matchStage = {};
|
|
1683
1709
|
if (filter?.name) matchStage["name"] = filter.name;
|
|
1684
1710
|
const pipeline = [...Object.keys(matchStage).length > 0 ? [{ $match: matchStage }] : [], { $facet: {
|
|
@@ -1702,35 +1728,47 @@ var JobQueryService = class {
|
|
|
1702
1728
|
cancelled: 0,
|
|
1703
1729
|
total: 0
|
|
1704
1730
|
};
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1731
|
+
if (result) {
|
|
1732
|
+
const statusCounts = result["statusCounts"];
|
|
1733
|
+
for (const entry of statusCounts) {
|
|
1734
|
+
const status = entry._id;
|
|
1735
|
+
const count = entry.count;
|
|
1736
|
+
switch (status) {
|
|
1737
|
+
case JobStatus.PENDING:
|
|
1738
|
+
stats.pending = count;
|
|
1739
|
+
break;
|
|
1740
|
+
case JobStatus.PROCESSING:
|
|
1741
|
+
stats.processing = count;
|
|
1742
|
+
break;
|
|
1743
|
+
case JobStatus.COMPLETED:
|
|
1744
|
+
stats.completed = count;
|
|
1745
|
+
break;
|
|
1746
|
+
case JobStatus.FAILED:
|
|
1747
|
+
stats.failed = count;
|
|
1748
|
+
break;
|
|
1749
|
+
case JobStatus.CANCELLED:
|
|
1750
|
+
stats.cancelled = count;
|
|
1751
|
+
break;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
const totalResult = result["total"];
|
|
1755
|
+
if (totalResult.length > 0 && totalResult[0]) stats.total = totalResult[0].count;
|
|
1756
|
+
const avgDurationResult = result["avgDuration"];
|
|
1757
|
+
if (avgDurationResult.length > 0 && avgDurationResult[0]) {
|
|
1758
|
+
const avgMs = avgDurationResult[0].avgMs;
|
|
1759
|
+
if (typeof avgMs === "number" && !Number.isNaN(avgMs)) stats.avgProcessingDurationMs = Math.round(avgMs);
|
|
1726
1760
|
}
|
|
1727
1761
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1762
|
+
if (ttl > 0) {
|
|
1763
|
+
this.statsCache.delete(cacheKey);
|
|
1764
|
+
if (this.statsCache.size >= JobQueryService.MAX_CACHE_SIZE) {
|
|
1765
|
+
const oldestKey = this.statsCache.keys().next().value;
|
|
1766
|
+
if (oldestKey !== void 0) this.statsCache.delete(oldestKey);
|
|
1767
|
+
}
|
|
1768
|
+
this.statsCache.set(cacheKey, {
|
|
1769
|
+
data: { ...stats },
|
|
1770
|
+
expiresAt: Date.now() + ttl
|
|
1771
|
+
});
|
|
1734
1772
|
}
|
|
1735
1773
|
return stats;
|
|
1736
1774
|
} catch (error) {
|
|
@@ -1755,6 +1793,26 @@ var JobScheduler = class {
|
|
|
1755
1793
|
this.ctx = ctx;
|
|
1756
1794
|
}
|
|
1757
1795
|
/**
|
|
1796
|
+
* Validate that the job data payload does not exceed the configured maximum BSON byte size.
|
|
1797
|
+
*
|
|
1798
|
+
* @param data - The job data payload to validate
|
|
1799
|
+
* @throws {PayloadTooLargeError} If the payload exceeds `maxPayloadSize`
|
|
1800
|
+
*/
|
|
1801
|
+
validatePayloadSize(data) {
|
|
1802
|
+
const maxSize = this.ctx.options.maxPayloadSize;
|
|
1803
|
+
if (maxSize === void 0) return;
|
|
1804
|
+
let size;
|
|
1805
|
+
try {
|
|
1806
|
+
size = BSON.calculateObjectSize({ data });
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
1809
|
+
const sizeError = new PayloadTooLargeError(`Failed to calculate job payload size: ${cause.message}`, -1, maxSize);
|
|
1810
|
+
sizeError.cause = cause;
|
|
1811
|
+
throw sizeError;
|
|
1812
|
+
}
|
|
1813
|
+
if (size > maxSize) throw new PayloadTooLargeError(`Job payload exceeds maximum size: ${size} bytes > ${maxSize} bytes`, size, maxSize);
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1758
1816
|
* Enqueue a job for processing.
|
|
1759
1817
|
*
|
|
1760
1818
|
* Jobs are stored in MongoDB and processed by registered workers. Supports
|
|
@@ -1772,6 +1830,7 @@ var JobScheduler = class {
|
|
|
1772
1830
|
* @param options - Scheduling and deduplication options
|
|
1773
1831
|
* @returns Promise resolving to the created or existing job document
|
|
1774
1832
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
1833
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
1775
1834
|
*
|
|
1776
1835
|
* @example Basic job enqueueing
|
|
1777
1836
|
* ```typescript
|
|
@@ -1799,6 +1858,7 @@ var JobScheduler = class {
|
|
|
1799
1858
|
* ```
|
|
1800
1859
|
*/
|
|
1801
1860
|
async enqueue(name, data, options = {}) {
|
|
1861
|
+
this.validatePayloadSize(data);
|
|
1802
1862
|
const now = /* @__PURE__ */ new Date();
|
|
1803
1863
|
const job = {
|
|
1804
1864
|
name,
|
|
@@ -1884,6 +1944,7 @@ var JobScheduler = class {
|
|
|
1884
1944
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
1885
1945
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
1886
1946
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
1947
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
1887
1948
|
*
|
|
1888
1949
|
* @example Hourly cleanup job
|
|
1889
1950
|
* ```typescript
|
|
@@ -1909,6 +1970,7 @@ var JobScheduler = class {
|
|
|
1909
1970
|
* ```
|
|
1910
1971
|
*/
|
|
1911
1972
|
async schedule(cron, name, data, options = {}) {
|
|
1973
|
+
this.validatePayloadSize(data);
|
|
1912
1974
|
const nextRunAt = getNextCronDate(cron);
|
|
1913
1975
|
const now = /* @__PURE__ */ new Date();
|
|
1914
1976
|
const job = {
|
|
@@ -1947,6 +2009,114 @@ var JobScheduler = class {
|
|
|
1947
2009
|
}
|
|
1948
2010
|
};
|
|
1949
2011
|
|
|
2012
|
+
//#endregion
|
|
2013
|
+
//#region src/scheduler/services/lifecycle-manager.ts
|
|
2014
|
+
/**
|
|
2015
|
+
* Default retention check interval (1 hour).
|
|
2016
|
+
*/
|
|
2017
|
+
const DEFAULT_RETENTION_INTERVAL = 36e5;
|
|
2018
|
+
/**
|
|
2019
|
+
* Manages scheduler lifecycle timers and job cleanup.
|
|
2020
|
+
*
|
|
2021
|
+
* Owns poll interval, heartbeat interval, cleanup interval, and the
|
|
2022
|
+
* cleanupJobs logic. Extracted from Monque to keep the facade thin.
|
|
2023
|
+
*
|
|
2024
|
+
* @internal Not part of public API.
|
|
2025
|
+
*/
|
|
2026
|
+
var LifecycleManager = class {
|
|
2027
|
+
ctx;
|
|
2028
|
+
pollIntervalId = null;
|
|
2029
|
+
heartbeatIntervalId = null;
|
|
2030
|
+
cleanupIntervalId = null;
|
|
2031
|
+
constructor(ctx) {
|
|
2032
|
+
this.ctx = ctx;
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Start all lifecycle timers.
|
|
2036
|
+
*
|
|
2037
|
+
* Sets up poll interval, heartbeat interval, and (if configured)
|
|
2038
|
+
* cleanup interval. Runs an initial poll immediately.
|
|
2039
|
+
*
|
|
2040
|
+
* @param callbacks - Functions to invoke on each timer tick
|
|
2041
|
+
*/
|
|
2042
|
+
startTimers(callbacks) {
|
|
2043
|
+
this.pollIntervalId = setInterval(() => {
|
|
2044
|
+
callbacks.poll().catch((error) => {
|
|
2045
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2046
|
+
});
|
|
2047
|
+
}, this.ctx.options.pollInterval);
|
|
2048
|
+
this.heartbeatIntervalId = setInterval(() => {
|
|
2049
|
+
callbacks.updateHeartbeats().catch((error) => {
|
|
2050
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2051
|
+
});
|
|
2052
|
+
}, this.ctx.options.heartbeatInterval);
|
|
2053
|
+
if (this.ctx.options.jobRetention) {
|
|
2054
|
+
const interval = this.ctx.options.jobRetention.interval ?? DEFAULT_RETENTION_INTERVAL;
|
|
2055
|
+
this.cleanupJobs().catch((error) => {
|
|
2056
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2057
|
+
});
|
|
2058
|
+
this.cleanupIntervalId = setInterval(() => {
|
|
2059
|
+
this.cleanupJobs().catch((error) => {
|
|
2060
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2061
|
+
});
|
|
2062
|
+
}, interval);
|
|
2063
|
+
}
|
|
2064
|
+
callbacks.poll().catch((error) => {
|
|
2065
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Stop all lifecycle timers.
|
|
2070
|
+
*
|
|
2071
|
+
* Clears poll, heartbeat, and cleanup intervals.
|
|
2072
|
+
*/
|
|
2073
|
+
stopTimers() {
|
|
2074
|
+
if (this.cleanupIntervalId) {
|
|
2075
|
+
clearInterval(this.cleanupIntervalId);
|
|
2076
|
+
this.cleanupIntervalId = null;
|
|
2077
|
+
}
|
|
2078
|
+
if (this.pollIntervalId) {
|
|
2079
|
+
clearInterval(this.pollIntervalId);
|
|
2080
|
+
this.pollIntervalId = null;
|
|
2081
|
+
}
|
|
2082
|
+
if (this.heartbeatIntervalId) {
|
|
2083
|
+
clearInterval(this.heartbeatIntervalId);
|
|
2084
|
+
this.heartbeatIntervalId = null;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Clean up old completed and failed jobs based on retention policy.
|
|
2089
|
+
*
|
|
2090
|
+
* - Removes completed jobs older than `jobRetention.completed`
|
|
2091
|
+
* - Removes failed jobs older than `jobRetention.failed`
|
|
2092
|
+
*
|
|
2093
|
+
* The cleanup runs concurrently for both statuses if configured.
|
|
2094
|
+
*
|
|
2095
|
+
* @returns Promise resolving when all deletion operations complete
|
|
2096
|
+
*/
|
|
2097
|
+
async cleanupJobs() {
|
|
2098
|
+
if (!this.ctx.options.jobRetention) return;
|
|
2099
|
+
const { completed, failed } = this.ctx.options.jobRetention;
|
|
2100
|
+
const now = Date.now();
|
|
2101
|
+
const deletions = [];
|
|
2102
|
+
if (completed != null) {
|
|
2103
|
+
const cutoff = new Date(now - completed);
|
|
2104
|
+
deletions.push(this.ctx.collection.deleteMany({
|
|
2105
|
+
status: JobStatus.COMPLETED,
|
|
2106
|
+
updatedAt: { $lt: cutoff }
|
|
2107
|
+
}));
|
|
2108
|
+
}
|
|
2109
|
+
if (failed != null) {
|
|
2110
|
+
const cutoff = new Date(now - failed);
|
|
2111
|
+
deletions.push(this.ctx.collection.deleteMany({
|
|
2112
|
+
status: JobStatus.FAILED,
|
|
2113
|
+
updatedAt: { $lt: cutoff }
|
|
2114
|
+
}));
|
|
2115
|
+
}
|
|
2116
|
+
if (deletions.length > 0) await Promise.all(deletions);
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
|
|
1950
2120
|
//#endregion
|
|
1951
2121
|
//#region src/scheduler/monque.ts
|
|
1952
2122
|
/**
|
|
@@ -2033,9 +2203,6 @@ var Monque = class extends EventEmitter {
|
|
|
2033
2203
|
options;
|
|
2034
2204
|
collection = null;
|
|
2035
2205
|
workers = /* @__PURE__ */ new Map();
|
|
2036
|
-
pollIntervalId = null;
|
|
2037
|
-
heartbeatIntervalId = null;
|
|
2038
|
-
cleanupIntervalId = null;
|
|
2039
2206
|
isRunning = false;
|
|
2040
2207
|
isInitialized = false;
|
|
2041
2208
|
_scheduler = null;
|
|
@@ -2043,6 +2210,7 @@ var Monque = class extends EventEmitter {
|
|
|
2043
2210
|
_query = null;
|
|
2044
2211
|
_processor = null;
|
|
2045
2212
|
_changeStreamHandler = null;
|
|
2213
|
+
_lifecycleManager = null;
|
|
2046
2214
|
constructor(db, options = {}) {
|
|
2047
2215
|
super();
|
|
2048
2216
|
this.db = db;
|
|
@@ -2060,7 +2228,9 @@ var Monque = class extends EventEmitter {
|
|
|
2060
2228
|
schedulerInstanceId: options.schedulerInstanceId ?? randomUUID(),
|
|
2061
2229
|
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
2062
2230
|
jobRetention: options.jobRetention,
|
|
2063
|
-
skipIndexCreation: options.skipIndexCreation ?? false
|
|
2231
|
+
skipIndexCreation: options.skipIndexCreation ?? false,
|
|
2232
|
+
maxPayloadSize: options.maxPayloadSize,
|
|
2233
|
+
statsCacheTtlMs: options.statsCacheTtlMs ?? 5e3
|
|
2064
2234
|
};
|
|
2065
2235
|
}
|
|
2066
2236
|
/**
|
|
@@ -2075,12 +2245,14 @@ var Monque = class extends EventEmitter {
|
|
|
2075
2245
|
this.collection = this.db.collection(this.options.collectionName);
|
|
2076
2246
|
if (!this.options.skipIndexCreation) await this.createIndexes();
|
|
2077
2247
|
if (this.options.recoverStaleJobs) await this.recoverStaleJobs();
|
|
2248
|
+
await this.checkInstanceCollision();
|
|
2078
2249
|
const ctx = this.buildContext();
|
|
2079
2250
|
this._scheduler = new JobScheduler(ctx);
|
|
2080
2251
|
this._manager = new JobManager(ctx);
|
|
2081
2252
|
this._query = new JobQueryService(ctx);
|
|
2082
2253
|
this._processor = new JobProcessor(ctx);
|
|
2083
2254
|
this._changeStreamHandler = new ChangeStreamHandler(ctx, () => this.processor.poll());
|
|
2255
|
+
this._lifecycleManager = new LifecycleManager(ctx);
|
|
2084
2256
|
this.isInitialized = true;
|
|
2085
2257
|
} catch (error) {
|
|
2086
2258
|
throw new ConnectionError(`Failed to initialize Monque: ${error instanceof Error ? error.message : "Unknown error during initialization"}`);
|
|
@@ -2111,6 +2283,11 @@ var Monque = class extends EventEmitter {
|
|
|
2111
2283
|
if (!this._changeStreamHandler) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
2112
2284
|
return this._changeStreamHandler;
|
|
2113
2285
|
}
|
|
2286
|
+
/** @throws {ConnectionError} if not initialized */
|
|
2287
|
+
get lifecycleManager() {
|
|
2288
|
+
if (!this._lifecycleManager) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
2289
|
+
return this._lifecycleManager;
|
|
2290
|
+
}
|
|
2114
2291
|
/**
|
|
2115
2292
|
* Build the shared context for internal services.
|
|
2116
2293
|
*/
|
|
@@ -2225,35 +2402,23 @@ var Monque = class extends EventEmitter {
|
|
|
2225
2402
|
if (result.modifiedCount > 0) this.emit("stale:recovered", { count: result.modifiedCount });
|
|
2226
2403
|
}
|
|
2227
2404
|
/**
|
|
2228
|
-
*
|
|
2405
|
+
* Check if another active instance is using the same schedulerInstanceId.
|
|
2406
|
+
* Uses heartbeat staleness to distinguish active instances from crashed ones.
|
|
2229
2407
|
*
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2408
|
+
* Called after stale recovery to avoid false positives: stale recovery resets
|
|
2409
|
+
* jobs with old `lockedAt`, so only jobs with recent heartbeats remain.
|
|
2232
2410
|
*
|
|
2233
|
-
*
|
|
2234
|
-
*
|
|
2235
|
-
* @returns Promise resolving when all deletion operations complete
|
|
2411
|
+
* @throws {ConnectionError} If an active instance with the same ID is detected
|
|
2236
2412
|
*/
|
|
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);
|
|
2413
|
+
async checkInstanceCollision() {
|
|
2414
|
+
if (!this.collection) return;
|
|
2415
|
+
const aliveThreshold = /* @__PURE__ */ new Date(Date.now() - this.options.heartbeatInterval * 2);
|
|
2416
|
+
const activeJob = await this.collection.findOne({
|
|
2417
|
+
claimedBy: this.options.schedulerInstanceId,
|
|
2418
|
+
status: JobStatus.PROCESSING,
|
|
2419
|
+
lastHeartbeat: { $gte: aliveThreshold }
|
|
2420
|
+
});
|
|
2421
|
+
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
2422
|
}
|
|
2258
2423
|
/**
|
|
2259
2424
|
* Enqueue a job for processing.
|
|
@@ -2273,6 +2438,7 @@ var Monque = class extends EventEmitter {
|
|
|
2273
2438
|
* @param options - Scheduling and deduplication options
|
|
2274
2439
|
* @returns Promise resolving to the created or existing job document
|
|
2275
2440
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2441
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
2276
2442
|
*
|
|
2277
2443
|
* @example Basic job enqueueing
|
|
2278
2444
|
* ```typescript
|
|
@@ -2298,6 +2464,8 @@ var Monque = class extends EventEmitter {
|
|
|
2298
2464
|
* });
|
|
2299
2465
|
* // Subsequent enqueues with same uniqueKey return existing pending/processing job
|
|
2300
2466
|
* ```
|
|
2467
|
+
*
|
|
2468
|
+
* @see {@link JobScheduler.enqueue}
|
|
2301
2469
|
*/
|
|
2302
2470
|
async enqueue(name, data, options = {}) {
|
|
2303
2471
|
this.ensureInitialized();
|
|
@@ -2330,6 +2498,8 @@ var Monque = class extends EventEmitter {
|
|
|
2330
2498
|
* await monque.now('process-order', { orderId: order.id });
|
|
2331
2499
|
* return order; // Return immediately, processing happens async
|
|
2332
2500
|
* ```
|
|
2501
|
+
*
|
|
2502
|
+
* @see {@link JobScheduler.now}
|
|
2333
2503
|
*/
|
|
2334
2504
|
async now(name, data) {
|
|
2335
2505
|
this.ensureInitialized();
|
|
@@ -2355,6 +2525,7 @@ var Monque = class extends EventEmitter {
|
|
|
2355
2525
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
2356
2526
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
2357
2527
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2528
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
2358
2529
|
*
|
|
2359
2530
|
* @example Hourly cleanup job
|
|
2360
2531
|
* ```typescript
|
|
@@ -2378,6 +2549,8 @@ var Monque = class extends EventEmitter {
|
|
|
2378
2549
|
* recipients: ['analytics@example.com']
|
|
2379
2550
|
* });
|
|
2380
2551
|
* ```
|
|
2552
|
+
*
|
|
2553
|
+
* @see {@link JobScheduler.schedule}
|
|
2381
2554
|
*/
|
|
2382
2555
|
async schedule(cron, name, data, options = {}) {
|
|
2383
2556
|
this.ensureInitialized();
|
|
@@ -2399,6 +2572,8 @@ var Monque = class extends EventEmitter {
|
|
|
2399
2572
|
* const job = await monque.enqueue('report', { type: 'daily' });
|
|
2400
2573
|
* await monque.cancelJob(job._id.toString());
|
|
2401
2574
|
* ```
|
|
2575
|
+
*
|
|
2576
|
+
* @see {@link JobManager.cancelJob}
|
|
2402
2577
|
*/
|
|
2403
2578
|
async cancelJob(jobId) {
|
|
2404
2579
|
this.ensureInitialized();
|
|
@@ -2421,6 +2596,8 @@ var Monque = class extends EventEmitter {
|
|
|
2421
2596
|
* await monque.retryJob(job._id.toString());
|
|
2422
2597
|
* });
|
|
2423
2598
|
* ```
|
|
2599
|
+
*
|
|
2600
|
+
* @see {@link JobManager.retryJob}
|
|
2424
2601
|
*/
|
|
2425
2602
|
async retryJob(jobId) {
|
|
2426
2603
|
this.ensureInitialized();
|
|
@@ -2441,6 +2618,8 @@ var Monque = class extends EventEmitter {
|
|
|
2441
2618
|
* const nextHour = new Date(Date.now() + 60 * 60 * 1000);
|
|
2442
2619
|
* await monque.rescheduleJob(jobId, nextHour);
|
|
2443
2620
|
* ```
|
|
2621
|
+
*
|
|
2622
|
+
* @see {@link JobManager.rescheduleJob}
|
|
2444
2623
|
*/
|
|
2445
2624
|
async rescheduleJob(jobId, runAt) {
|
|
2446
2625
|
this.ensureInitialized();
|
|
@@ -2462,20 +2641,23 @@ var Monque = class extends EventEmitter {
|
|
|
2462
2641
|
* console.log('Job permanently removed');
|
|
2463
2642
|
* }
|
|
2464
2643
|
* ```
|
|
2644
|
+
*
|
|
2645
|
+
* @see {@link JobManager.deleteJob}
|
|
2465
2646
|
*/
|
|
2466
2647
|
async deleteJob(jobId) {
|
|
2467
2648
|
this.ensureInitialized();
|
|
2468
2649
|
return this.manager.deleteJob(jobId);
|
|
2469
2650
|
}
|
|
2470
2651
|
/**
|
|
2471
|
-
* Cancel multiple jobs matching the given filter.
|
|
2652
|
+
* Cancel multiple jobs matching the given filter via a single updateMany call.
|
|
2472
2653
|
*
|
|
2473
|
-
* Only cancels jobs in 'pending' status
|
|
2474
|
-
*
|
|
2654
|
+
* Only cancels jobs in 'pending' status — the status guard is applied regardless
|
|
2655
|
+
* of what the filter specifies. Jobs in other states are silently skipped (not
|
|
2656
|
+
* matched by the query). Emits a 'jobs:cancelled' event with the count of
|
|
2475
2657
|
* successfully cancelled jobs.
|
|
2476
2658
|
*
|
|
2477
2659
|
* @param filter - Selector for which jobs to cancel (name, status, date range)
|
|
2478
|
-
* @returns Result with count of cancelled jobs
|
|
2660
|
+
* @returns Result with count of cancelled jobs (errors array always empty for bulk ops)
|
|
2479
2661
|
*
|
|
2480
2662
|
* @example Cancel all pending jobs for a queue
|
|
2481
2663
|
* ```typescript
|
|
@@ -2485,20 +2667,23 @@ var Monque = class extends EventEmitter {
|
|
|
2485
2667
|
* });
|
|
2486
2668
|
* console.log(`Cancelled ${result.count} jobs`);
|
|
2487
2669
|
* ```
|
|
2670
|
+
*
|
|
2671
|
+
* @see {@link JobManager.cancelJobs}
|
|
2488
2672
|
*/
|
|
2489
2673
|
async cancelJobs(filter) {
|
|
2490
2674
|
this.ensureInitialized();
|
|
2491
2675
|
return this.manager.cancelJobs(filter);
|
|
2492
2676
|
}
|
|
2493
2677
|
/**
|
|
2494
|
-
* Retry multiple jobs matching the given filter.
|
|
2678
|
+
* Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.
|
|
2495
2679
|
*
|
|
2496
|
-
* Only retries jobs in 'failed' or 'cancelled' status
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2680
|
+
* Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied
|
|
2681
|
+
* regardless of what the filter specifies. Jobs in other states are silently skipped.
|
|
2682
|
+
* Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.
|
|
2683
|
+
* Emits a 'jobs:retried' event with the count of successfully retried jobs.
|
|
2499
2684
|
*
|
|
2500
2685
|
* @param filter - Selector for which jobs to retry (name, status, date range)
|
|
2501
|
-
* @returns Result with count of retried jobs
|
|
2686
|
+
* @returns Result with count of retried jobs (errors array always empty for bulk ops)
|
|
2502
2687
|
*
|
|
2503
2688
|
* @example Retry all failed jobs
|
|
2504
2689
|
* ```typescript
|
|
@@ -2507,6 +2692,8 @@ var Monque = class extends EventEmitter {
|
|
|
2507
2692
|
* });
|
|
2508
2693
|
* console.log(`Retried ${result.count} jobs`);
|
|
2509
2694
|
* ```
|
|
2695
|
+
*
|
|
2696
|
+
* @see {@link JobManager.retryJobs}
|
|
2510
2697
|
*/
|
|
2511
2698
|
async retryJobs(filter) {
|
|
2512
2699
|
this.ensureInitialized();
|
|
@@ -2516,6 +2703,7 @@ var Monque = class extends EventEmitter {
|
|
|
2516
2703
|
* Delete multiple jobs matching the given filter.
|
|
2517
2704
|
*
|
|
2518
2705
|
* Deletes jobs in any status. Uses a batch delete for efficiency.
|
|
2706
|
+
* Emits a 'jobs:deleted' event with the count of deleted jobs.
|
|
2519
2707
|
* Does not emit individual 'job:deleted' events to avoid noise.
|
|
2520
2708
|
*
|
|
2521
2709
|
* @param filter - Selector for which jobs to delete (name, status, date range)
|
|
@@ -2530,6 +2718,8 @@ var Monque = class extends EventEmitter {
|
|
|
2530
2718
|
* });
|
|
2531
2719
|
* console.log(`Deleted ${result.count} jobs`);
|
|
2532
2720
|
* ```
|
|
2721
|
+
*
|
|
2722
|
+
* @see {@link JobManager.deleteJobs}
|
|
2533
2723
|
*/
|
|
2534
2724
|
async deleteJobs(filter) {
|
|
2535
2725
|
this.ensureInitialized();
|
|
@@ -2565,6 +2755,8 @@ var Monque = class extends EventEmitter {
|
|
|
2565
2755
|
* res.json(job);
|
|
2566
2756
|
* });
|
|
2567
2757
|
* ```
|
|
2758
|
+
*
|
|
2759
|
+
* @see {@link JobQueryService.getJob}
|
|
2568
2760
|
*/
|
|
2569
2761
|
async getJob(id) {
|
|
2570
2762
|
this.ensureInitialized();
|
|
@@ -2611,6 +2803,8 @@ var Monque = class extends EventEmitter {
|
|
|
2611
2803
|
* const jobs = await monque.getJobs();
|
|
2612
2804
|
* const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));
|
|
2613
2805
|
* ```
|
|
2806
|
+
*
|
|
2807
|
+
* @see {@link JobQueryService.getJobs}
|
|
2614
2808
|
*/
|
|
2615
2809
|
async getJobs(filter = {}) {
|
|
2616
2810
|
this.ensureInitialized();
|
|
@@ -2644,6 +2838,8 @@ var Monque = class extends EventEmitter {
|
|
|
2644
2838
|
* });
|
|
2645
2839
|
* }
|
|
2646
2840
|
* ```
|
|
2841
|
+
*
|
|
2842
|
+
* @see {@link JobQueryService.getJobsWithCursor}
|
|
2647
2843
|
*/
|
|
2648
2844
|
async getJobsWithCursor(options = {}) {
|
|
2649
2845
|
this.ensureInitialized();
|
|
@@ -2655,6 +2851,9 @@ var Monque = class extends EventEmitter {
|
|
|
2655
2851
|
* Uses MongoDB aggregation pipeline for efficient server-side calculation.
|
|
2656
2852
|
* Returns counts per status and optional average processing duration for completed jobs.
|
|
2657
2853
|
*
|
|
2854
|
+
* Results are cached per unique filter with a configurable TTL (default 5s).
|
|
2855
|
+
* Set `statsCacheTtlMs: 0` to disable caching.
|
|
2856
|
+
*
|
|
2658
2857
|
* @param filter - Optional filter to scope statistics by job name
|
|
2659
2858
|
* @returns Promise resolving to queue statistics
|
|
2660
2859
|
* @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
|
|
@@ -2671,6 +2870,8 @@ var Monque = class extends EventEmitter {
|
|
|
2671
2870
|
* const emailStats = await monque.getQueueStats({ name: 'send-email' });
|
|
2672
2871
|
* console.log(`${emailStats.total} email jobs in queue`);
|
|
2673
2872
|
* ```
|
|
2873
|
+
*
|
|
2874
|
+
* @see {@link JobQueryService.getQueueStats}
|
|
2674
2875
|
*/
|
|
2675
2876
|
async getQueueStats(filter) {
|
|
2676
2877
|
this.ensureInitialized();
|
|
@@ -2797,29 +2998,9 @@ var Monque = class extends EventEmitter {
|
|
|
2797
2998
|
if (!this.isInitialized) throw new ConnectionError("Monque not initialized. Call initialize() before start().");
|
|
2798
2999
|
this.isRunning = true;
|
|
2799
3000
|
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) });
|
|
3001
|
+
this.lifecycleManager.startTimers({
|
|
3002
|
+
poll: () => this.processor.poll(),
|
|
3003
|
+
updateHeartbeats: () => this.processor.updateHeartbeats()
|
|
2823
3004
|
});
|
|
2824
3005
|
}
|
|
2825
3006
|
/**
|
|
@@ -2858,19 +3039,11 @@ var Monque = class extends EventEmitter {
|
|
|
2858
3039
|
async stop() {
|
|
2859
3040
|
if (!this.isRunning) return;
|
|
2860
3041
|
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
|
-
}
|
|
3042
|
+
this._query?.clearStatsCache();
|
|
3043
|
+
try {
|
|
3044
|
+
await this.changeStreamHandler.close();
|
|
3045
|
+
} catch {}
|
|
3046
|
+
this.lifecycleManager.stopTimers();
|
|
2874
3047
|
if (this.getActiveJobs().length === 0) return;
|
|
2875
3048
|
let checkInterval;
|
|
2876
3049
|
const waitForJobs = new Promise((resolve) => {
|
|
@@ -2994,5 +3167,5 @@ var Monque = class extends EventEmitter {
|
|
|
2994
3167
|
};
|
|
2995
3168
|
|
|
2996
3169
|
//#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 };
|
|
3170
|
+
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 };
|
|
2998
3171
|
//# sourceMappingURL=index.mjs.map
|