@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/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 ?? DEFAULT_MAX_BACKOFF_DELAY;
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 ?? DEFAULT_MAX_BACKOFF_DELAY;
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. Jobs in other states are collected
1016
- * as errors in the result. Emits a 'jobs:cancelled' event with the IDs of
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 and any errors encountered
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 baseQuery = buildSelectorQuery(filter);
1033
- const errors = [];
1034
- const cancelledIds = [];
1035
- const cursor = this.ctx.collection.find(baseQuery);
1036
- for await (const doc of cursor) {
1037
- const jobId = doc._id.toString();
1038
- if (doc["status"] !== JobStatus.PENDING && doc["status"] !== JobStatus.CANCELLED) {
1039
- errors.push({
1040
- jobId,
1041
- error: `Cannot cancel job in status '${doc["status"]}'`
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
- } }, { returnDocument: "after" })) cancelledIds.push(jobId);
1056
- else errors.push({
1057
- jobId,
1058
- error: "Job status changed during cancellation"
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. Jobs in other states
1074
- * are collected as errors in the result. Emits a 'jobs:retried' event with
1075
- * the IDs of successfully retried jobs.
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 and any errors encountered
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 baseQuery = buildSelectorQuery(filter);
1090
- const errors = [];
1091
- const retriedIds = [];
1092
- const cursor = this.ctx.collection.find(baseQuery);
1093
- for await (const doc of cursor) {
1094
- const jobId = doc._id.toString();
1095
- if (doc["status"] !== JobStatus.FAILED && doc["status"] !== JobStatus.CANCELLED) {
1096
- errors.push({
1097
- jobId,
1098
- error: `Cannot retry job in status '${doc["status"]}'`
1099
- });
1100
- continue;
1101
- }
1102
- if (await this.ctx.collection.findOneAndUpdate({
1103
- _id: doc._id,
1104
- status: { $in: [JobStatus.FAILED, JobStatus.CANCELLED] }
1105
- }, {
1106
- $set: {
1107
- status: JobStatus.PENDING,
1108
- failCount: 0,
1109
- nextRunAt: /* @__PURE__ */ new Date(),
1110
- updatedAt: /* @__PURE__ */ new Date()
1111
- },
1112
- $unset: {
1113
- failReason: "",
1114
- lockedAt: "",
1115
- claimedBy: "",
1116
- lastHeartbeat: "",
1117
- heartbeatInterval: ""
1118
- }
1119
- }, { returnDocument: "after" })) retriedIds.push(jobId);
1120
- else errors.push({
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
- const result = await this.ctx.collection.deleteMany(query);
1157
- if (result.deletedCount > 0) this.ctx.emit("jobs:deleted", { count: result.deletedCount });
1158
- return {
1159
- count: result.deletedCount,
1160
- errors: []
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 (!result) return stats;
1706
- const statusCounts = result["statusCounts"];
1707
- for (const entry of statusCounts) {
1708
- const status = entry._id;
1709
- const count = entry.count;
1710
- switch (status) {
1711
- case JobStatus.PENDING:
1712
- stats.pending = count;
1713
- break;
1714
- case JobStatus.PROCESSING:
1715
- stats.processing = count;
1716
- break;
1717
- case JobStatus.COMPLETED:
1718
- stats.completed = count;
1719
- break;
1720
- case JobStatus.FAILED:
1721
- stats.failed = count;
1722
- break;
1723
- case JobStatus.CANCELLED:
1724
- stats.cancelled = count;
1725
- break;
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
- const totalResult = result["total"];
1729
- if (totalResult.length > 0 && totalResult[0]) stats.total = totalResult[0].count;
1730
- const avgDurationResult = result["avgDuration"];
1731
- if (avgDurationResult.length > 0 && avgDurationResult[0]) {
1732
- const avgMs = avgDurationResult[0].avgMs;
1733
- if (typeof avgMs === "number" && !Number.isNaN(avgMs)) stats.avgProcessingDurationMs = Math.round(avgMs);
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
- * Clean up old completed and failed jobs based on retention policy.
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
- * - Removes completed jobs older than `jobRetention.completed`
2231
- * - Removes failed jobs older than `jobRetention.failed`
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
- * The cleanup runs concurrently for both statuses if configured.
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 cleanupJobs() {
2238
- if (!this.collection || !this.options.jobRetention) return;
2239
- const { completed, failed } = this.options.jobRetention;
2240
- const now = Date.now();
2241
- const deletions = [];
2242
- if (completed) {
2243
- const cutoff = new Date(now - completed);
2244
- deletions.push(this.collection.deleteMany({
2245
- status: JobStatus.COMPLETED,
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. Jobs in other states are collected
2474
- * as errors in the result. Emits a 'jobs:cancelled' event with the IDs of
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 and any errors encountered
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. Jobs in other states
2497
- * are collected as errors in the result. Emits a 'jobs:retried' event with
2498
- * the IDs of successfully retried jobs.
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 and any errors encountered
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.pollIntervalId = setInterval(() => {
2801
- this.processor.poll().catch((error) => {
2802
- this.emit("job:error", { error: toError(error) });
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
- await this.changeStreamHandler.close();
2862
- if (this.cleanupIntervalId) {
2863
- clearInterval(this.cleanupIntervalId);
2864
- this.cleanupIntervalId = null;
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