@monque/core 0.1.0 → 0.2.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
@@ -4,6 +4,15 @@
4
4
 
5
5
  <h1 align="center">@monque/core</h1>
6
6
 
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@monque/core">
9
+ <img src="https://img.shields.io/npm/v/%40monque%2Fcore?style=for-the-badge&label=%40monque%2Fcore" alt="@monque/core version" />
10
+ </a>
11
+ <a href="https://codecov.io/gh/ueberBrot/monque">
12
+ <img src="https://img.shields.io/codecov/c/github/ueberBrot/monque?style=for-the-badge&logo=codecov&logoColor=white" alt="Codecov" />
13
+ </a>
14
+ </p>
15
+
7
16
  <p align="center">MongoDB-backed job scheduler with atomic locking, exponential backoff, and cron scheduling.</p>
8
17
 
9
18
  > [!WARNING]
@@ -42,7 +51,7 @@ const monque = new Monque(client.db('myapp'), {
42
51
  await monque.initialize();
43
52
 
44
53
  // Register workers
45
- monque.worker('send-email', async (job) => {
54
+ monque.register('send-email', async (job) => {
46
55
  await sendEmail(job.data.to, job.data.subject);
47
56
  });
48
57
 
@@ -81,7 +90,7 @@ Creates a new Monque instance.
81
90
  - `enqueue(name, data, options?)` - Enqueue a job
82
91
  - `now(name, data)` - Enqueue for immediate processing
83
92
  - `schedule(cron, name, data)` - Schedule recurring job
84
- - `worker(name, handler, options?)` - Register a worker
93
+ - `register(name, handler, options?)` - Register a worker
85
94
  - `start()` - Start processing jobs
86
95
  - `stop()` - Graceful shutdown
87
96
  - `isHealthy()` - Check scheduler health
@@ -99,8 +99,8 @@ var ShutdownTimeoutError = class ShutdownTimeoutError extends MonqueError {
99
99
  * @example
100
100
  * ```typescript
101
101
  * try {
102
- * monque.worker('send-email', handler1);
103
- * monque.worker('send-email', handler2); // throws
102
+ * monque.register('send-email', handler1);
103
+ * monque.register('send-email', handler2); // throws
104
104
  * } catch (error) {
105
105
  * if (error instanceof WorkerRegistrationError) {
106
106
  * console.error('Worker already registered for:', error.jobName);
@@ -108,7 +108,7 @@ var ShutdownTimeoutError = class ShutdownTimeoutError extends MonqueError {
108
108
  * }
109
109
  *
110
110
  * // To intentionally replace a worker:
111
- * monque.worker('send-email', handler2, { replace: true });
111
+ * monque.register('send-email', handler2, { replace: true });
112
112
  * ```
113
113
  */
114
114
  var WorkerRegistrationError = class WorkerRegistrationError extends MonqueError {
@@ -152,4 +152,4 @@ Object.defineProperty(exports, 'WorkerRegistrationError', {
152
152
  return WorkerRegistrationError;
153
153
  }
154
154
  });
155
- //# sourceMappingURL=errors-BX3oWYfZ.cjs.map
155
+ //# sourceMappingURL=errors-D5ZGG2uI.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-D5ZGG2uI.cjs","names":[],"sources":["../src/shared/errors.ts"],"sourcesContent":["import type { Job } from '@/jobs';\n\n/**\n * Base error class for all Monque-related errors.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof MonqueError) {\n * console.error('Monque error:', error.message);\n * }\n * }\n * ```\n */\nexport class MonqueError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'MonqueError';\n\t\t// Maintains proper stack trace for where our error was thrown (only available on V8)\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, MonqueError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when an invalid cron expression is provided.\n *\n * @example\n * ```typescript\n * try {\n * await monque.schedule('invalid cron', 'job', data);\n * } catch (error) {\n * if (error instanceof InvalidCronError) {\n * console.error('Invalid expression:', error.expression);\n * }\n * }\n * ```\n */\nexport class InvalidCronError extends MonqueError {\n\tconstructor(\n\t\tpublic readonly expression: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'InvalidCronError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, InvalidCronError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when there's a database connection issue.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof ConnectionError) {\n * console.error('Database connection lost');\n * }\n * }\n * ```\n */\nexport class ConnectionError extends MonqueError {\n\tconstructor(message: string, options?: { cause?: Error }) {\n\t\tsuper(message);\n\t\tthis.name = 'ConnectionError';\n\t\tif (options?.cause) {\n\t\t\tthis.cause = options.cause;\n\t\t}\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ConnectionError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when graceful shutdown times out.\n * Includes information about jobs that were still in progress.\n *\n * @example\n * ```typescript\n * try {\n * await monque.stop();\n * } catch (error) {\n * if (error instanceof ShutdownTimeoutError) {\n * console.error('Incomplete jobs:', error.incompleteJobs.length);\n * }\n * }\n * ```\n */\nexport class ShutdownTimeoutError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly incompleteJobs: Job[],\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'ShutdownTimeoutError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ShutdownTimeoutError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when attempting to register a worker for a job name\n * that already has a registered worker, without explicitly allowing replacement.\n *\n * @example\n * ```typescript\n * try {\n * monque.register('send-email', handler1);\n * monque.register('send-email', handler2); // throws\n * } catch (error) {\n * if (error instanceof WorkerRegistrationError) {\n * console.error('Worker already registered for:', error.jobName);\n * }\n * }\n *\n * // To intentionally replace a worker:\n * monque.register('send-email', handler2, { replace: true });\n * ```\n */\nexport class WorkerRegistrationError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly jobName: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'WorkerRegistrationError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, WorkerRegistrationError);\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,IAAa,cAAb,MAAa,oBAAoB,MAAM;CACtC,YAAY,SAAiB;AAC5B,QAAM,QAAQ;AACd,OAAK,OAAO;;AAGZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,YAAY;;;;;;;;;;;;;;;;;AAmB7C,IAAa,mBAAb,MAAa,yBAAyB,YAAY;CACjD,YACC,AAAgB,YAChB,SACC;AACD,QAAM,QAAQ;EAHE;AAIhB,OAAK,OAAO;;AAEZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,iBAAiB;;;;;;;;;;;;;;;;;AAmBlD,IAAa,kBAAb,MAAa,wBAAwB,YAAY;CAChD,YAAY,SAAiB,SAA6B;AACzD,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,MAAI,SAAS,MACZ,MAAK,QAAQ,QAAQ;;AAGtB,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;AAoBjD,IAAa,uBAAb,MAAa,6BAA6B,YAAY;CACrD,YACC,SACA,AAAgB,gBACf;AACD,QAAM,QAAQ;EAFE;AAGhB,OAAK,OAAO;;AAEZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;AAwBtD,IAAa,0BAAb,MAAa,gCAAgC,YAAY;CACxD,YACC,SACA,AAAgB,SACf;AACD,QAAM,QAAQ;EAFE;AAGhB,OAAK,OAAO;;AAEZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,wBAAwB"}
@@ -1,3 +1,3 @@
1
- import { a as WorkerRegistrationError, i as ShutdownTimeoutError, n as InvalidCronError, r as MonqueError, t as ConnectionError } from "./errors-DW20rHWR.mjs";
1
+ import { a as WorkerRegistrationError, i as ShutdownTimeoutError, n as InvalidCronError, r as MonqueError, t as ConnectionError } from "./errors-DQ2_gprw.mjs";
2
2
 
3
3
  export { ShutdownTimeoutError };
@@ -98,8 +98,8 @@ var ShutdownTimeoutError = class ShutdownTimeoutError extends MonqueError {
98
98
  * @example
99
99
  * ```typescript
100
100
  * try {
101
- * monque.worker('send-email', handler1);
102
- * monque.worker('send-email', handler2); // throws
101
+ * monque.register('send-email', handler1);
102
+ * monque.register('send-email', handler2); // throws
103
103
  * } catch (error) {
104
104
  * if (error instanceof WorkerRegistrationError) {
105
105
  * console.error('Worker already registered for:', error.jobName);
@@ -107,7 +107,7 @@ var ShutdownTimeoutError = class ShutdownTimeoutError extends MonqueError {
107
107
  * }
108
108
  *
109
109
  * // To intentionally replace a worker:
110
- * monque.worker('send-email', handler2, { replace: true });
110
+ * monque.register('send-email', handler2, { replace: true });
111
111
  * ```
112
112
  */
113
113
  var WorkerRegistrationError = class WorkerRegistrationError extends MonqueError {
@@ -122,4 +122,4 @@ var WorkerRegistrationError = class WorkerRegistrationError extends MonqueError
122
122
 
123
123
  //#endregion
124
124
  export { WorkerRegistrationError as a, ShutdownTimeoutError as i, InvalidCronError as n, MonqueError as r, ConnectionError as t };
125
- //# sourceMappingURL=errors-DW20rHWR.mjs.map
125
+ //# sourceMappingURL=errors-DQ2_gprw.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-DQ2_gprw.mjs","names":[],"sources":["../src/shared/errors.ts"],"sourcesContent":["import type { Job } from '@/jobs';\n\n/**\n * Base error class for all Monque-related errors.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof MonqueError) {\n * console.error('Monque error:', error.message);\n * }\n * }\n * ```\n */\nexport class MonqueError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'MonqueError';\n\t\t// Maintains proper stack trace for where our error was thrown (only available on V8)\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, MonqueError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when an invalid cron expression is provided.\n *\n * @example\n * ```typescript\n * try {\n * await monque.schedule('invalid cron', 'job', data);\n * } catch (error) {\n * if (error instanceof InvalidCronError) {\n * console.error('Invalid expression:', error.expression);\n * }\n * }\n * ```\n */\nexport class InvalidCronError extends MonqueError {\n\tconstructor(\n\t\tpublic readonly expression: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'InvalidCronError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, InvalidCronError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when there's a database connection issue.\n *\n * @example\n * ```typescript\n * try {\n * await monque.enqueue('job', data);\n * } catch (error) {\n * if (error instanceof ConnectionError) {\n * console.error('Database connection lost');\n * }\n * }\n * ```\n */\nexport class ConnectionError extends MonqueError {\n\tconstructor(message: string, options?: { cause?: Error }) {\n\t\tsuper(message);\n\t\tthis.name = 'ConnectionError';\n\t\tif (options?.cause) {\n\t\t\tthis.cause = options.cause;\n\t\t}\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ConnectionError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when graceful shutdown times out.\n * Includes information about jobs that were still in progress.\n *\n * @example\n * ```typescript\n * try {\n * await monque.stop();\n * } catch (error) {\n * if (error instanceof ShutdownTimeoutError) {\n * console.error('Incomplete jobs:', error.incompleteJobs.length);\n * }\n * }\n * ```\n */\nexport class ShutdownTimeoutError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly incompleteJobs: Job[],\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'ShutdownTimeoutError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, ShutdownTimeoutError);\n\t\t}\n\t}\n}\n\n/**\n * Error thrown when attempting to register a worker for a job name\n * that already has a registered worker, without explicitly allowing replacement.\n *\n * @example\n * ```typescript\n * try {\n * monque.register('send-email', handler1);\n * monque.register('send-email', handler2); // throws\n * } catch (error) {\n * if (error instanceof WorkerRegistrationError) {\n * console.error('Worker already registered for:', error.jobName);\n * }\n * }\n *\n * // To intentionally replace a worker:\n * monque.register('send-email', handler2, { replace: true });\n * ```\n */\nexport class WorkerRegistrationError extends MonqueError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly jobName: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'WorkerRegistrationError';\n\t\t/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */\n\t\tif (Error.captureStackTrace) {\n\t\t\tError.captureStackTrace(this, WorkerRegistrationError);\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAa,cAAb,MAAa,oBAAoB,MAAM;CACtC,YAAY,SAAiB;AAC5B,QAAM,QAAQ;AACd,OAAK,OAAO;;AAGZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,YAAY;;;;;;;;;;;;;;;;;AAmB7C,IAAa,mBAAb,MAAa,yBAAyB,YAAY;CACjD,YACC,AAAgB,YAChB,SACC;AACD,QAAM,QAAQ;EAHE;AAIhB,OAAK,OAAO;;AAEZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,iBAAiB;;;;;;;;;;;;;;;;;AAmBlD,IAAa,kBAAb,MAAa,wBAAwB,YAAY;CAChD,YAAY,SAAiB,SAA6B;AACzD,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,MAAI,SAAS,MACZ,MAAK,QAAQ,QAAQ;;AAGtB,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;AAoBjD,IAAa,uBAAb,MAAa,6BAA6B,YAAY;CACrD,YACC,SACA,AAAgB,gBACf;AACD,QAAM,QAAQ;EAFE;AAGhB,OAAK,OAAO;;AAEZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;AAwBtD,IAAa,0BAAb,MAAa,gCAAgC,YAAY;CACxD,YACC,SACA,AAAgB,SACf;AACD,QAAM,QAAQ;EAFE;AAGhB,OAAK,OAAO;;AAEZ,MAAI,MAAM,kBACT,OAAM,kBAAkB,MAAM,wBAAwB"}
@@ -1,3 +1,3 @@
1
- const require_errors = require('./errors-BX3oWYfZ.cjs');
1
+ const require_errors = require('./errors-D5ZGG2uI.cjs');
2
2
 
3
3
  exports.ShutdownTimeoutError = require_errors.ShutdownTimeoutError;
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_errors = require('./errors-BX3oWYfZ.cjs');
1
+ const require_errors = require('./errors-D5ZGG2uI.cjs');
2
2
  let node_crypto = require("node:crypto");
3
3
  let node_events = require("node:events");
4
4
  let cron_parser = require("cron-parser");
@@ -396,7 +396,7 @@ type EmailJob = {};
396
396
  * body: string
397
397
  * }
398
398
  *
399
- * monque.worker<EmailJob>('send-email', async (job) =>
399
+ * monque.register<EmailJob>('send-email', async (job) =>
400
400
  {
401
401
  * await emailService.send(job.data.to, job.data.subject, job.data.body)
402
402
  *
@@ -845,7 +845,7 @@ var Monque = class extends node_events.EventEmitter {
845
845
  * body: string;
846
846
  * }
847
847
  *
848
- * monque.worker<EmailJob>('send-email', async (job) => {
848
+ * monque.register<EmailJob>('send-email', async (job) => {
849
849
  * await emailService.send(job.data.to, job.data.subject, job.data.body);
850
850
  * });
851
851
  * ```
@@ -853,7 +853,7 @@ var Monque = class extends node_events.EventEmitter {
853
853
  * @example Worker with custom concurrency
854
854
  * ```typescript
855
855
  * // Limit to 2 concurrent video processing jobs (resource-intensive)
856
- * monque.worker('process-video', async (job) => {
856
+ * monque.register('process-video', async (job) => {
857
857
  * await videoProcessor.transcode(job.data.videoId);
858
858
  * }, { concurrency: 2 });
859
859
  * ```
@@ -861,12 +861,12 @@ var Monque = class extends node_events.EventEmitter {
861
861
  * @example Replacing an existing worker
862
862
  * ```typescript
863
863
  * // Replace the existing handler for 'send-email'
864
- * monque.worker('send-email', newEmailHandler, { replace: true });
864
+ * monque.register('send-email', newEmailHandler, { replace: true });
865
865
  * ```
866
866
  *
867
867
  * @example Worker with error handling
868
868
  * ```typescript
869
- * monque.worker('sync-user', async (job) => {
869
+ * monque.register('sync-user', async (job) => {
870
870
  * try {
871
871
  * await externalApi.syncUser(job.data.userId);
872
872
  * } catch (error) {
@@ -877,7 +877,7 @@ var Monque = class extends node_events.EventEmitter {
877
877
  * });
878
878
  * ```
879
879
  */
880
- worker(name, handler, options = {}) {
880
+ register(name, handler, options = {}) {
881
881
  const concurrency = options.concurrency ?? this.options.defaultConcurrency;
882
882
  if (this.workers.has(name) && options.replace !== true) throw new require_errors.WorkerRegistrationError(`Worker already registered for job name "${name}". Use { replace: true } to replace.`, name);
883
883
  this.workers.set(name, {
@@ -901,8 +901,8 @@ var Monque = class extends node_events.EventEmitter {
901
901
  * const monque = new Monque(db);
902
902
  * await monque.initialize();
903
903
  *
904
- * monque.worker('send-email', emailHandler);
905
- * monque.worker('process-order', orderHandler);
904
+ * monque.register('send-email', emailHandler);
905
+ * monque.register('process-order', orderHandler);
906
906
  *
907
907
  * monque.start(); // Begin processing jobs
908
908
  * ```
@@ -1037,7 +1037,7 @@ var Monque = class extends node_events.EventEmitter {
1037
1037
  }
1038
1038
  if (result === "timeout") {
1039
1039
  const incompleteJobs = this.getActiveJobsList();
1040
- const { ShutdownTimeoutError: ShutdownTimeoutError$1 } = await Promise.resolve().then(() => require("./errors-Ca92IlaL.cjs"));
1040
+ const { ShutdownTimeoutError: ShutdownTimeoutError$1 } = await Promise.resolve().then(() => require("./errors-Dfli-u59.cjs"));
1041
1041
  const error = new ShutdownTimeoutError$1(`Shutdown timed out after ${this.options.shutdownTimeout}ms with ${incompleteJobs.length} incomplete jobs`, incompleteJobs);
1042
1042
  this.emit("job:error", { error });
1043
1043
  }
@@ -1195,6 +1195,7 @@ var Monque = class extends node_events.EventEmitter {
1195
1195
  *
1196
1196
  * Called at regular intervals (configured by `pollInterval`). For each registered worker,
1197
1197
  * attempts to acquire jobs up to the worker's available concurrency slots.
1198
+ * Aborts early if the scheduler is stopping (`isRunning` is false).
1198
1199
  *
1199
1200
  * @private
1200
1201
  */
@@ -1204,6 +1205,7 @@ var Monque = class extends node_events.EventEmitter {
1204
1205
  const availableSlots = worker.concurrency - worker.activeJobs.size;
1205
1206
  if (availableSlots <= 0) continue;
1206
1207
  for (let i = 0; i < availableSlots; i++) {
1208
+ if (!this.isRunning) return;
1207
1209
  const job = await this.acquireJob(name);
1208
1210
  if (job) this.processJob(job, worker).catch((error) => {
1209
1211
  this.emit("job:error", {
@@ -1224,12 +1226,14 @@ var Monque = class extends node_events.EventEmitter {
1224
1226
  * - Has nextRunAt <= now
1225
1227
  * - Is not claimed by another instance (claimedBy is null/undefined)
1226
1228
  *
1229
+ * Returns `null` immediately if scheduler is stopping (`isRunning` is false).
1230
+ *
1227
1231
  * @private
1228
1232
  * @param name - The job type to acquire
1229
1233
  * @returns The acquired job with updated status, claimedBy, and heartbeat info, or `null` if no jobs available
1230
1234
  */
1231
1235
  async acquireJob(name) {
1232
- if (!this.collection) return null;
1236
+ if (!this.collection || !this.isRunning) return null;
1233
1237
  const now = /* @__PURE__ */ new Date();
1234
1238
  const result = await this.collection.findOneAndUpdate({
1235
1239
  name,