@monque/core 1.1.2 → 1.3.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/README.md CHANGED
@@ -8,12 +8,31 @@
8
8
  <a href="https://www.npmjs.com/package/@monque/core">
9
9
  <img src="https://img.shields.io/npm/v/%40monque%2Fcore?style=for-the-badge&label=%40monque%2Fcore" alt="@monque/core version" />
10
10
  </a>
11
+ <a href="https://github.com/ueberBrot/monque/actions/workflows/ci.yml">
12
+ <img src="https://img.shields.io/github/actions/workflow/status/ueberBrot/monque/ci.yml?branch=main&style=for-the-badge&logo=github" alt="CI Status" />
13
+ </a>
11
14
  <a href="https://codecov.io/gh/ueberBrot/monque">
12
15
  <img src="https://img.shields.io/codecov/c/github/ueberBrot/monque?style=for-the-badge&logo=codecov&logoColor=white" alt="Codecov" />
13
16
  </a>
17
+ <a href="https://opensource.org/licenses/ISC">
18
+ <img src="https://img.shields.io/badge/License-ISC-blue.svg?style=for-the-badge" alt="License: ISC" />
19
+ </a>
20
+ <a href="https://bun.sh">
21
+ <img src="https://img.shields.io/badge/Built%20with-Bun-fbf0df?style=for-the-badge&logo=bun&logoColor=black" alt="Built with Bun" />
22
+ </a>
14
23
  </p>
15
24
 
16
- <p align="center">MongoDB-backed job scheduler with atomic locking, exponential backoff, and cron scheduling.</p>
25
+ A **robust, type-safe MongoDB job queue** for TypeScript with atomic locking, exponential backoff, and cron scheduling.
26
+
27
+ ## Features
28
+
29
+ - **Atomic Locking**: Mandatory `findOneAndUpdate` for safe job acquisition in distributed environments.
30
+ - **Exponential Backoff**: Built-in retry logic with configurable backoff strategies.
31
+ - **Cron Scheduling**: Native support for recurring jobs using standard cron syntax.
32
+ - **Type Safety**: Fully typed job payloads and worker definitions.
33
+ - **Event-Driven**: Comprehensive event system for monitoring and logging.
34
+ - **Native Driver**: Uses the native MongoDB driver for maximum performance and compatibility.
35
+ - **Graceful Shutdown**: Ensures all in-progress jobs finish or are safely released before stopping.
17
36
 
18
37
  ## Installation
19
38
 
package/dist/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @monque/core
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#158](https://github.com/ueberBrot/monque/pull/158) [`2f83396`](https://github.com/ueberBrot/monque/commit/2f833966d7798307deaa7a1e655e0623cfb42a3e) Thanks [@renovate](https://github.com/apps/renovate)! - mongodb (^7.0.0 → ^7.1.0)
8
+
9
+ ### Patch Changes
10
+
11
+ - [#160](https://github.com/ueberBrot/monque/pull/160) [`b5fcaf8`](https://github.com/ueberBrot/monque/commit/b5fcaf8be2a49fb1ba97b8d3d9f28f00850f77a1) Thanks [@ueberBrot](https://github.com/ueberBrot)! - Fix race condition where concurrent poll cycles could exceed workerConcurrency limit. Added a guard to prevent overlapping poll() execution from setInterval and change stream triggers.
12
+
13
+ ## 1.2.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#112](https://github.com/ueberBrot/monque/pull/112) [`9b7f44f`](https://github.com/ueberBrot/monque/commit/9b7f44f5c1f6b4aa4215e571189cc03cbaa49865) Thanks [@ueberBrot](https://github.com/ueberBrot)! - Add instance-level concurrency throttling and deprecated old naming conventions.
18
+
19
+ - Added `instanceConcurrency` option (formerly `maxConcurrency`) to limit the total number of jobs processed concurrently across all workers on a single Monque instance.
20
+ - Added `workerConcurrency` as a clearer alias for `defaultConcurrency`.
21
+ - Deprecated `maxConcurrency` and `defaultConcurrency` in favor of the new explicit names. They will be removed in the next major version.
22
+
3
23
  ## 1.1.2
4
24
 
5
25
  ### Patch Changes
package/dist/README.md CHANGED
@@ -8,12 +8,31 @@
8
8
  <a href="https://www.npmjs.com/package/@monque/core">
9
9
  <img src="https://img.shields.io/npm/v/%40monque%2Fcore?style=for-the-badge&label=%40monque%2Fcore" alt="@monque/core version" />
10
10
  </a>
11
+ <a href="https://github.com/ueberBrot/monque/actions/workflows/ci.yml">
12
+ <img src="https://img.shields.io/github/actions/workflow/status/ueberBrot/monque/ci.yml?branch=main&style=for-the-badge&logo=github" alt="CI Status" />
13
+ </a>
11
14
  <a href="https://codecov.io/gh/ueberBrot/monque">
12
15
  <img src="https://img.shields.io/codecov/c/github/ueberBrot/monque?style=for-the-badge&logo=codecov&logoColor=white" alt="Codecov" />
13
16
  </a>
17
+ <a href="https://opensource.org/licenses/ISC">
18
+ <img src="https://img.shields.io/badge/License-ISC-blue.svg?style=for-the-badge" alt="License: ISC" />
19
+ </a>
20
+ <a href="https://bun.sh">
21
+ <img src="https://img.shields.io/badge/Built%20with-Bun-fbf0df?style=for-the-badge&logo=bun&logoColor=black" alt="Built with Bun" />
22
+ </a>
14
23
  </p>
15
24
 
16
- <p align="center">MongoDB-backed job scheduler with atomic locking, exponential backoff, and cron scheduling.</p>
25
+ A **robust, type-safe MongoDB job queue** for TypeScript with atomic locking, exponential backoff, and cron scheduling.
26
+
27
+ ## Features
28
+
29
+ - **Atomic Locking**: Mandatory `findOneAndUpdate` for safe job acquisition in distributed environments.
30
+ - **Exponential Backoff**: Built-in retry logic with configurable backoff strategies.
31
+ - **Cron Scheduling**: Native support for recurring jobs using standard cron syntax.
32
+ - **Type Safety**: Fully typed job payloads and worker definitions.
33
+ - **Event-Driven**: Comprehensive event system for monitoring and logging.
34
+ - **Native Driver**: Uses the native MongoDB driver for maximum performance and compatibility.
35
+ - **Graceful Shutdown**: Ensures all in-progress jobs finish or are safely released before stopping.
17
36
 
18
37
  ## Installation
19
38
 
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1
2
  let mongodb = require("mongodb");
2
3
  let cron_parser = require("cron-parser");
3
4
  let node_crypto = require("node:crypto");
@@ -1110,31 +1111,74 @@ var JobManager = class {
1110
1111
  * @internal Not part of public API.
1111
1112
  */
1112
1113
  var JobProcessor = class {
1114
+ /** Guard flag to prevent concurrent poll() execution */
1115
+ _isPolling = false;
1113
1116
  constructor(ctx) {
1114
1117
  this.ctx = ctx;
1115
1118
  }
1116
1119
  /**
1120
+ * Get the total number of active jobs across all workers.
1121
+ *
1122
+ * Used for instance-level throttling when `instanceConcurrency` is configured.
1123
+ */
1124
+ getTotalActiveJobs() {
1125
+ let total = 0;
1126
+ for (const worker of this.ctx.workers.values()) total += worker.activeJobs.size;
1127
+ return total;
1128
+ }
1129
+ /**
1130
+ * Get the number of available slots considering the global instanceConcurrency limit.
1131
+ *
1132
+ * @param workerAvailableSlots - Available slots for the specific worker
1133
+ * @returns Number of slots available after applying global limit
1134
+ */
1135
+ getGloballyAvailableSlots(workerAvailableSlots) {
1136
+ const { instanceConcurrency } = this.ctx.options;
1137
+ if (instanceConcurrency === void 0) return workerAvailableSlots;
1138
+ const globalAvailable = instanceConcurrency - this.getTotalActiveJobs();
1139
+ return Math.min(workerAvailableSlots, globalAvailable);
1140
+ }
1141
+ /**
1117
1142
  * Poll for available jobs and process them.
1118
1143
  *
1119
1144
  * Called at regular intervals (configured by `pollInterval`). For each registered worker,
1120
1145
  * attempts to acquire jobs up to the worker's available concurrency slots.
1121
- * Aborts early if the scheduler is stopping (`isRunning` is false).
1146
+ * Aborts early if the scheduler is stopping (`isRunning` is false) or if
1147
+ * the instance-level `instanceConcurrency` limit is reached.
1122
1148
  */
1123
1149
  async poll() {
1124
- if (!this.ctx.isRunning()) return;
1150
+ if (!this.ctx.isRunning() || this._isPolling) return;
1151
+ this._isPolling = true;
1152
+ try {
1153
+ await this._doPoll();
1154
+ } finally {
1155
+ this._isPolling = false;
1156
+ }
1157
+ }
1158
+ /**
1159
+ * Internal poll implementation.
1160
+ */
1161
+ async _doPoll() {
1162
+ const { instanceConcurrency } = this.ctx.options;
1163
+ if (instanceConcurrency !== void 0 && this.getTotalActiveJobs() >= instanceConcurrency) return;
1125
1164
  for (const [name, worker] of this.ctx.workers) {
1126
- const availableSlots = worker.concurrency - worker.activeJobs.size;
1127
- if (availableSlots <= 0) continue;
1165
+ const workerAvailableSlots = worker.concurrency - worker.activeJobs.size;
1166
+ if (workerAvailableSlots <= 0) continue;
1167
+ const availableSlots = this.getGloballyAvailableSlots(workerAvailableSlots);
1168
+ if (availableSlots <= 0) return;
1128
1169
  for (let i = 0; i < availableSlots; i++) {
1129
1170
  if (!this.ctx.isRunning()) return;
1171
+ if (instanceConcurrency !== void 0 && this.getTotalActiveJobs() >= instanceConcurrency) return;
1130
1172
  const job = await this.acquireJob(name);
1131
- if (job) this.processJob(job, worker).catch((error) => {
1132
- this.ctx.emit("job:error", {
1133
- error,
1134
- job
1173
+ if (job) {
1174
+ worker.activeJobs.set(job._id.toString(), job);
1175
+ this.processJob(job, worker).catch((error) => {
1176
+ this.ctx.emit("job:error", {
1177
+ error,
1178
+ job
1179
+ });
1135
1180
  });
1136
- });
1137
- else break;
1181
+ } else break;
1138
1182
  }
1139
1183
  }
1140
1184
  }
@@ -1187,7 +1231,6 @@ var JobProcessor = class {
1187
1231
  */
1188
1232
  async processJob(job, worker) {
1189
1233
  const jobId = job._id.toString();
1190
- worker.activeJobs.set(jobId, job);
1191
1234
  const startTime = Date.now();
1192
1235
  this.ctx.emit("job:start", job);
1193
1236
  try {
@@ -1817,7 +1860,7 @@ const DEFAULTS = {
1817
1860
  maxRetries: 10,
1818
1861
  baseRetryInterval: 1e3,
1819
1862
  shutdownTimeout: 3e4,
1820
- defaultConcurrency: 5,
1863
+ workerConcurrency: 5,
1821
1864
  lockTimeout: 18e5,
1822
1865
  recoverStaleJobs: true,
1823
1866
  heartbeatInterval: 3e4,
@@ -1911,10 +1954,11 @@ var Monque = class extends node_events.EventEmitter {
1911
1954
  maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
1912
1955
  baseRetryInterval: options.baseRetryInterval ?? DEFAULTS.baseRetryInterval,
1913
1956
  shutdownTimeout: options.shutdownTimeout ?? DEFAULTS.shutdownTimeout,
1914
- defaultConcurrency: options.defaultConcurrency ?? DEFAULTS.defaultConcurrency,
1957
+ workerConcurrency: options.workerConcurrency ?? options.defaultConcurrency ?? DEFAULTS.workerConcurrency,
1915
1958
  lockTimeout: options.lockTimeout ?? DEFAULTS.lockTimeout,
1916
1959
  recoverStaleJobs: options.recoverStaleJobs ?? DEFAULTS.recoverStaleJobs,
1917
1960
  maxBackoffDelay: options.maxBackoffDelay,
1961
+ instanceConcurrency: options.instanceConcurrency ?? options.maxConcurrency,
1918
1962
  schedulerInstanceId: options.schedulerInstanceId ?? (0, node_crypto.randomUUID)(),
1919
1963
  heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
1920
1964
  jobRetention: options.jobRetention
@@ -2577,7 +2621,7 @@ var Monque = class extends node_events.EventEmitter {
2577
2621
  * ```
2578
2622
  */
2579
2623
  register(name, handler, options = {}) {
2580
- const concurrency = options.concurrency ?? this.options.defaultConcurrency;
2624
+ const concurrency = options.concurrency ?? this.options.workerConcurrency;
2581
2625
  if (this.workers.has(name) && options.replace !== true) throw new WorkerRegistrationError(`Worker already registered for job name "${name}". Use { replace: true } to replace.`, name);
2582
2626
  this.workers.set(name, {
2583
2627
  handler,