@monque/core 1.6.0 → 1.7.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 +30 -0
- package/dist/index.cjs +281 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +62 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +279 -125
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +3 -0
- package/src/scheduler/monque.ts +87 -26
- package/src/scheduler/services/index.ts +1 -1
- package/src/scheduler/services/job-manager.ts +151 -117
- package/src/scheduler/services/job-processor.ts +67 -49
- package/src/scheduler/services/job-scheduler.ts +24 -5
- package/src/scheduler/services/lifecycle-manager.ts +6 -1
- package/src/scheduler/services/types.ts +3 -0
- package/src/shared/errors.ts +29 -0
- package/src/shared/index.ts +3 -0
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/job-identifiers.ts +71 -0
package/dist/index.cjs
CHANGED
|
@@ -455,6 +455,30 @@ var InvalidCursorError = class InvalidCursorError extends MonqueError {
|
|
|
455
455
|
}
|
|
456
456
|
};
|
|
457
457
|
/**
|
|
458
|
+
* Error thrown when a public job identifier fails validation.
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```typescript
|
|
462
|
+
* try {
|
|
463
|
+
* await monque.enqueue('invalid job name', {});
|
|
464
|
+
* } catch (error) {
|
|
465
|
+
* if (error instanceof InvalidJobIdentifierError) {
|
|
466
|
+
* console.error(`Invalid ${error.field}: ${error.message}`);
|
|
467
|
+
* }
|
|
468
|
+
* }
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
var InvalidJobIdentifierError = class InvalidJobIdentifierError extends MonqueError {
|
|
472
|
+
constructor(field, value, message) {
|
|
473
|
+
super(message);
|
|
474
|
+
this.field = field;
|
|
475
|
+
this.value = value;
|
|
476
|
+
this.name = "InvalidJobIdentifierError";
|
|
477
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
478
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, InvalidJobIdentifierError);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
458
482
|
* Error thrown when a statistics aggregation times out.
|
|
459
483
|
*
|
|
460
484
|
* @example
|
|
@@ -648,6 +672,34 @@ function toError(value) {
|
|
|
648
672
|
}
|
|
649
673
|
}
|
|
650
674
|
//#endregion
|
|
675
|
+
//#region src/shared/utils/job-identifiers.ts
|
|
676
|
+
const JOB_NAME_PATTERN = /^[^\s\p{Cc}]+$/u;
|
|
677
|
+
const CONTROL_CHARACTER_PATTERN = /\p{Cc}/u;
|
|
678
|
+
const MAX_JOB_NAME_LENGTH = 255;
|
|
679
|
+
const MAX_UNIQUE_KEY_LENGTH = 1024;
|
|
680
|
+
/**
|
|
681
|
+
* Validate a public job name before it is registered or scheduled.
|
|
682
|
+
*
|
|
683
|
+
* @param name - The job name to validate
|
|
684
|
+
* @throws {InvalidJobIdentifierError} If the job name is empty, too long, or contains unsupported characters
|
|
685
|
+
*/
|
|
686
|
+
function validateJobName(name) {
|
|
687
|
+
if (name.length === 0 || name.trim().length === 0) throw new InvalidJobIdentifierError("name", name, "Job name cannot be empty or whitespace only.");
|
|
688
|
+
if (name.length > MAX_JOB_NAME_LENGTH) throw new InvalidJobIdentifierError("name", name, `Job name cannot exceed ${MAX_JOB_NAME_LENGTH} characters.`);
|
|
689
|
+
if (!JOB_NAME_PATTERN.test(name)) throw new InvalidJobIdentifierError("name", name, "Job name cannot contain whitespace or control characters.");
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Validate a deduplication key before it is stored or used in a unique query.
|
|
693
|
+
*
|
|
694
|
+
* @param uniqueKey - The unique key to validate
|
|
695
|
+
* @throws {InvalidJobIdentifierError} If the key is empty, too long, or contains control characters
|
|
696
|
+
*/
|
|
697
|
+
function validateUniqueKey(uniqueKey) {
|
|
698
|
+
if (uniqueKey.length === 0 || uniqueKey.trim().length === 0) throw new InvalidJobIdentifierError("uniqueKey", uniqueKey, "Unique key cannot be empty or whitespace only.");
|
|
699
|
+
if (uniqueKey.length > MAX_UNIQUE_KEY_LENGTH) throw new InvalidJobIdentifierError("uniqueKey", uniqueKey, `Unique key cannot exceed ${MAX_UNIQUE_KEY_LENGTH} characters.`);
|
|
700
|
+
if (CONTROL_CHARACTER_PATTERN.test(uniqueKey)) throw new InvalidJobIdentifierError("uniqueKey", uniqueKey, "Unique key cannot contain control characters.");
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
651
703
|
//#region src/scheduler/helpers.ts
|
|
652
704
|
/**
|
|
653
705
|
* Build a MongoDB query filter from a JobSelector.
|
|
@@ -1012,21 +1064,28 @@ var JobManager = class {
|
|
|
1012
1064
|
async cancelJob(jobId) {
|
|
1013
1065
|
if (!mongodb.ObjectId.isValid(jobId)) return null;
|
|
1014
1066
|
const _id = new mongodb.ObjectId(jobId);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1067
|
+
try {
|
|
1068
|
+
const now = /* @__PURE__ */ new Date();
|
|
1069
|
+
const result = await this.ctx.collection.findOneAndUpdate({
|
|
1070
|
+
_id,
|
|
1071
|
+
status: JobStatus.PENDING
|
|
1072
|
+
}, { $set: {
|
|
1073
|
+
status: JobStatus.CANCELLED,
|
|
1074
|
+
updatedAt: now
|
|
1075
|
+
} }, { returnDocument: "after" });
|
|
1076
|
+
if (!result) {
|
|
1077
|
+
const jobDoc = await this.ctx.collection.findOne({ _id });
|
|
1078
|
+
if (!jobDoc) return null;
|
|
1079
|
+
if (jobDoc["status"] === JobStatus.CANCELLED) return this.ctx.documentToPersistedJob(jobDoc);
|
|
1080
|
+
throw new JobStateError(`Cannot cancel job in status '${jobDoc["status"]}'`, jobId, jobDoc["status"], "cancel");
|
|
1081
|
+
}
|
|
1082
|
+
const job = this.ctx.documentToPersistedJob(result);
|
|
1083
|
+
this.ctx.emit("job:cancelled", { job });
|
|
1084
|
+
return job;
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
if (error instanceof MonqueError) throw error;
|
|
1087
|
+
throw new ConnectionError(`Failed to cancel job: ${error instanceof Error ? error.message : "Unknown error during cancelJob"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1088
|
+
}
|
|
1030
1089
|
}
|
|
1031
1090
|
/**
|
|
1032
1091
|
* Retry a failed or cancelled job.
|
|
@@ -1049,36 +1108,51 @@ var JobManager = class {
|
|
|
1049
1108
|
async retryJob(jobId) {
|
|
1050
1109
|
if (!mongodb.ObjectId.isValid(jobId)) return null;
|
|
1051
1110
|
const _id = new mongodb.ObjectId(jobId);
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1111
|
+
try {
|
|
1112
|
+
const now = /* @__PURE__ */ new Date();
|
|
1113
|
+
const result = await this.ctx.collection.findOneAndUpdate({
|
|
1114
|
+
_id,
|
|
1115
|
+
status: { $in: [JobStatus.FAILED, JobStatus.CANCELLED] }
|
|
1116
|
+
}, {
|
|
1117
|
+
$set: {
|
|
1118
|
+
status: JobStatus.PENDING,
|
|
1119
|
+
failCount: 0,
|
|
1120
|
+
nextRunAt: now,
|
|
1121
|
+
updatedAt: now
|
|
1122
|
+
},
|
|
1123
|
+
$unset: {
|
|
1124
|
+
failReason: "",
|
|
1125
|
+
lockedAt: "",
|
|
1126
|
+
claimedBy: "",
|
|
1127
|
+
lastHeartbeat: ""
|
|
1128
|
+
}
|
|
1129
|
+
}, { returnDocument: "before" });
|
|
1130
|
+
if (!result) {
|
|
1131
|
+
const currentJob = await this.ctx.collection.findOne({ _id });
|
|
1132
|
+
if (!currentJob) return null;
|
|
1133
|
+
throw new JobStateError(`Cannot retry job in status '${currentJob["status"]}'`, jobId, currentJob["status"], "retry");
|
|
1072
1134
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1135
|
+
const previousStatus = result["status"];
|
|
1136
|
+
const updatedDoc = { ...result };
|
|
1137
|
+
updatedDoc["status"] = JobStatus.PENDING;
|
|
1138
|
+
updatedDoc["failCount"] = 0;
|
|
1139
|
+
updatedDoc["nextRunAt"] = now;
|
|
1140
|
+
updatedDoc["updatedAt"] = now;
|
|
1141
|
+
delete updatedDoc["failReason"];
|
|
1142
|
+
delete updatedDoc["lockedAt"];
|
|
1143
|
+
delete updatedDoc["claimedBy"];
|
|
1144
|
+
delete updatedDoc["lastHeartbeat"];
|
|
1145
|
+
const job = this.ctx.documentToPersistedJob(updatedDoc);
|
|
1146
|
+
this.ctx.notifyPendingJob(job.name, job.nextRunAt);
|
|
1147
|
+
this.ctx.emit("job:retried", {
|
|
1148
|
+
job,
|
|
1149
|
+
previousStatus
|
|
1150
|
+
});
|
|
1151
|
+
return job;
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
if (error instanceof MonqueError) throw error;
|
|
1154
|
+
throw new ConnectionError(`Failed to retry job: ${error instanceof Error ? error.message : "Unknown error during retryJob"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1155
|
+
}
|
|
1082
1156
|
}
|
|
1083
1157
|
/**
|
|
1084
1158
|
* Reschedule a pending job to run at a different time.
|
|
@@ -1099,20 +1173,27 @@ var JobManager = class {
|
|
|
1099
1173
|
async rescheduleJob(jobId, runAt) {
|
|
1100
1174
|
if (!mongodb.ObjectId.isValid(jobId)) return null;
|
|
1101
1175
|
const _id = new mongodb.ObjectId(jobId);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1176
|
+
try {
|
|
1177
|
+
const now = /* @__PURE__ */ new Date();
|
|
1178
|
+
const result = await this.ctx.collection.findOneAndUpdate({
|
|
1179
|
+
_id,
|
|
1180
|
+
status: JobStatus.PENDING
|
|
1181
|
+
}, { $set: {
|
|
1182
|
+
nextRunAt: runAt,
|
|
1183
|
+
updatedAt: now
|
|
1184
|
+
} }, { returnDocument: "after" });
|
|
1185
|
+
if (!result) {
|
|
1186
|
+
const currentJobDoc = await this.ctx.collection.findOne({ _id });
|
|
1187
|
+
if (!currentJobDoc) return null;
|
|
1188
|
+
throw new JobStateError(`Cannot reschedule job in status '${currentJobDoc["status"]}'`, jobId, currentJobDoc["status"], "reschedule");
|
|
1189
|
+
}
|
|
1190
|
+
const job = this.ctx.documentToPersistedJob(result);
|
|
1191
|
+
this.ctx.notifyPendingJob(job.name, job.nextRunAt);
|
|
1192
|
+
return job;
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (error instanceof MonqueError) throw error;
|
|
1195
|
+
throw new ConnectionError(`Failed to reschedule job: ${error instanceof Error ? error.message : "Unknown error during rescheduleJob"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1196
|
+
}
|
|
1116
1197
|
}
|
|
1117
1198
|
/**
|
|
1118
1199
|
* Permanently delete a job.
|
|
@@ -1134,11 +1215,16 @@ var JobManager = class {
|
|
|
1134
1215
|
async deleteJob(jobId) {
|
|
1135
1216
|
if (!mongodb.ObjectId.isValid(jobId)) return false;
|
|
1136
1217
|
const _id = new mongodb.ObjectId(jobId);
|
|
1137
|
-
|
|
1138
|
-
this.ctx.
|
|
1139
|
-
|
|
1218
|
+
try {
|
|
1219
|
+
if ((await this.ctx.collection.deleteOne({ _id })).deletedCount > 0) {
|
|
1220
|
+
this.ctx.emit("job:deleted", { jobId });
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
return false;
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
if (error instanceof MonqueError) throw error;
|
|
1226
|
+
throw new ConnectionError(`Failed to delete job: ${error instanceof Error ? error.message : "Unknown error during deleteJob"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1140
1227
|
}
|
|
1141
|
-
return false;
|
|
1142
1228
|
}
|
|
1143
1229
|
/**
|
|
1144
1230
|
* Cancel multiple jobs matching the given filter via a single updateMany call.
|
|
@@ -1170,9 +1256,10 @@ var JobManager = class {
|
|
|
1170
1256
|
}
|
|
1171
1257
|
query["status"] = JobStatus.PENDING;
|
|
1172
1258
|
try {
|
|
1259
|
+
const now = /* @__PURE__ */ new Date();
|
|
1173
1260
|
const count = (await this.ctx.collection.updateMany(query, { $set: {
|
|
1174
1261
|
status: JobStatus.CANCELLED,
|
|
1175
|
-
updatedAt:
|
|
1262
|
+
updatedAt: now
|
|
1176
1263
|
} })).modifiedCount;
|
|
1177
1264
|
if (count > 0) this.ctx.emit("jobs:cancelled", { count });
|
|
1178
1265
|
return {
|
|
@@ -1216,17 +1303,17 @@ var JobManager = class {
|
|
|
1216
1303
|
} else query["status"] = { $in: retryable };
|
|
1217
1304
|
const spreadWindowMs = 3e4;
|
|
1218
1305
|
try {
|
|
1306
|
+
const now = /* @__PURE__ */ new Date();
|
|
1219
1307
|
const count = (await this.ctx.collection.updateMany(query, [{ $set: {
|
|
1220
1308
|
status: JobStatus.PENDING,
|
|
1221
1309
|
failCount: 0,
|
|
1222
|
-
nextRunAt: { $add: [
|
|
1223
|
-
updatedAt:
|
|
1310
|
+
nextRunAt: { $add: [now, { $multiply: [{ $rand: {} }, spreadWindowMs] }] },
|
|
1311
|
+
updatedAt: now
|
|
1224
1312
|
} }, { $unset: [
|
|
1225
1313
|
"failReason",
|
|
1226
1314
|
"lockedAt",
|
|
1227
1315
|
"claimedBy",
|
|
1228
|
-
"lastHeartbeat"
|
|
1229
|
-
"heartbeatInterval"
|
|
1316
|
+
"lastHeartbeat"
|
|
1230
1317
|
] }])).modifiedCount;
|
|
1231
1318
|
if (count > 0) this.ctx.emit("jobs:retried", { count });
|
|
1232
1319
|
return {
|
|
@@ -1288,18 +1375,16 @@ var JobProcessor = class {
|
|
|
1288
1375
|
_isPolling = false;
|
|
1289
1376
|
/** Flag to request a re-poll after the current poll finishes */
|
|
1290
1377
|
_repollRequested = false;
|
|
1291
|
-
constructor(ctx) {
|
|
1292
|
-
this.ctx = ctx;
|
|
1293
|
-
}
|
|
1294
1378
|
/**
|
|
1295
|
-
*
|
|
1379
|
+
* O(1) counter tracking the total number of active jobs across all workers.
|
|
1296
1380
|
*
|
|
1297
|
-
*
|
|
1381
|
+
* Incremented when a job is added to `worker.activeJobs` in `_doPoll`,
|
|
1382
|
+
* decremented in the `processJob` finally block. Replaces the previous
|
|
1383
|
+
* O(workers) loop in `getTotalActiveJobs()` for instance-level throttling.
|
|
1298
1384
|
*/
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
return total;
|
|
1385
|
+
_totalActiveJobs = 0;
|
|
1386
|
+
constructor(ctx) {
|
|
1387
|
+
this.ctx = ctx;
|
|
1303
1388
|
}
|
|
1304
1389
|
/**
|
|
1305
1390
|
* Get the number of available slots considering the global instanceConcurrency limit.
|
|
@@ -1310,7 +1395,7 @@ var JobProcessor = class {
|
|
|
1310
1395
|
getGloballyAvailableSlots(workerAvailableSlots) {
|
|
1311
1396
|
const { instanceConcurrency } = this.ctx.options;
|
|
1312
1397
|
if (instanceConcurrency === void 0) return workerAvailableSlots;
|
|
1313
|
-
const globalAvailable = instanceConcurrency - this.
|
|
1398
|
+
const globalAvailable = instanceConcurrency - this._totalActiveJobs;
|
|
1314
1399
|
return Math.min(workerAvailableSlots, globalAvailable);
|
|
1315
1400
|
}
|
|
1316
1401
|
/**
|
|
@@ -1350,27 +1435,49 @@ var JobProcessor = class {
|
|
|
1350
1435
|
*/
|
|
1351
1436
|
async _doPoll(targetNames) {
|
|
1352
1437
|
const { instanceConcurrency } = this.ctx.options;
|
|
1353
|
-
if (instanceConcurrency !== void 0 && this.
|
|
1438
|
+
if (instanceConcurrency !== void 0 && this._totalActiveJobs >= instanceConcurrency) return;
|
|
1354
1439
|
for (const [name, worker] of this.ctx.workers) {
|
|
1355
1440
|
if (targetNames && !targetNames.has(name)) continue;
|
|
1356
1441
|
const workerAvailableSlots = worker.concurrency - worker.activeJobs.size;
|
|
1357
1442
|
if (workerAvailableSlots <= 0) continue;
|
|
1358
1443
|
const availableSlots = this.getGloballyAvailableSlots(workerAvailableSlots);
|
|
1359
1444
|
if (availableSlots <= 0) return;
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
if (
|
|
1445
|
+
if (!this.ctx.isRunning()) return;
|
|
1446
|
+
const acquisitionPromises = [];
|
|
1447
|
+
for (let i = 0; i < availableSlots; i++) acquisitionPromises.push(this.acquireJob(name).then(async (job) => {
|
|
1448
|
+
if (!job) return;
|
|
1449
|
+
if (this.ctx.isRunning()) {
|
|
1365
1450
|
worker.activeJobs.set(job._id.toString(), job);
|
|
1451
|
+
this._totalActiveJobs++;
|
|
1366
1452
|
this.processJob(job, worker).catch((error) => {
|
|
1367
1453
|
this.ctx.emit("job:error", {
|
|
1368
1454
|
error: toError(error),
|
|
1369
1455
|
job
|
|
1370
1456
|
});
|
|
1371
1457
|
});
|
|
1372
|
-
} else
|
|
1373
|
-
|
|
1458
|
+
} else try {
|
|
1459
|
+
await this.ctx.collection.updateOne({
|
|
1460
|
+
_id: job._id,
|
|
1461
|
+
status: JobStatus.PROCESSING,
|
|
1462
|
+
claimedBy: this.ctx.instanceId
|
|
1463
|
+
}, {
|
|
1464
|
+
$set: {
|
|
1465
|
+
status: JobStatus.PENDING,
|
|
1466
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1467
|
+
},
|
|
1468
|
+
$unset: {
|
|
1469
|
+
lockedAt: "",
|
|
1470
|
+
claimedBy: "",
|
|
1471
|
+
lastHeartbeat: ""
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
1476
|
+
}
|
|
1477
|
+
}).catch((error) => {
|
|
1478
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
1479
|
+
}));
|
|
1480
|
+
await Promise.allSettled(acquisitionPromises);
|
|
1374
1481
|
}
|
|
1375
1482
|
}
|
|
1376
1483
|
/**
|
|
@@ -1406,7 +1513,6 @@ var JobProcessor = class {
|
|
|
1406
1513
|
sort: { nextRunAt: 1 },
|
|
1407
1514
|
returnDocument: "after"
|
|
1408
1515
|
});
|
|
1409
|
-
if (!this.ctx.isRunning()) return null;
|
|
1410
1516
|
if (!result) return null;
|
|
1411
1517
|
return this.ctx.documentToPersistedJob(result);
|
|
1412
1518
|
}
|
|
@@ -1449,6 +1555,8 @@ var JobProcessor = class {
|
|
|
1449
1555
|
}
|
|
1450
1556
|
} finally {
|
|
1451
1557
|
worker.activeJobs.delete(jobId);
|
|
1558
|
+
this._totalActiveJobs--;
|
|
1559
|
+
this.ctx.notifyJobFinished();
|
|
1452
1560
|
}
|
|
1453
1561
|
}
|
|
1454
1562
|
/**
|
|
@@ -1468,6 +1576,7 @@ var JobProcessor = class {
|
|
|
1468
1576
|
*/
|
|
1469
1577
|
async completeJob(job) {
|
|
1470
1578
|
if (!isPersistedJob(job)) return null;
|
|
1579
|
+
const now = /* @__PURE__ */ new Date();
|
|
1471
1580
|
if (job.repeatInterval) {
|
|
1472
1581
|
const nextRunAt = getNextCronDate(job.repeatInterval);
|
|
1473
1582
|
const result = await this.ctx.collection.findOneAndUpdate({
|
|
@@ -1479,13 +1588,12 @@ var JobProcessor = class {
|
|
|
1479
1588
|
status: JobStatus.PENDING,
|
|
1480
1589
|
nextRunAt,
|
|
1481
1590
|
failCount: 0,
|
|
1482
|
-
updatedAt:
|
|
1591
|
+
updatedAt: now
|
|
1483
1592
|
},
|
|
1484
1593
|
$unset: {
|
|
1485
1594
|
lockedAt: "",
|
|
1486
1595
|
claimedBy: "",
|
|
1487
1596
|
lastHeartbeat: "",
|
|
1488
|
-
heartbeatInterval: "",
|
|
1489
1597
|
failReason: ""
|
|
1490
1598
|
}
|
|
1491
1599
|
}, { returnDocument: "after" });
|
|
@@ -1501,13 +1609,12 @@ var JobProcessor = class {
|
|
|
1501
1609
|
}, {
|
|
1502
1610
|
$set: {
|
|
1503
1611
|
status: JobStatus.COMPLETED,
|
|
1504
|
-
updatedAt:
|
|
1612
|
+
updatedAt: now
|
|
1505
1613
|
},
|
|
1506
1614
|
$unset: {
|
|
1507
1615
|
lockedAt: "",
|
|
1508
1616
|
claimedBy: "",
|
|
1509
1617
|
lastHeartbeat: "",
|
|
1510
|
-
heartbeatInterval: "",
|
|
1511
1618
|
failReason: ""
|
|
1512
1619
|
}
|
|
1513
1620
|
}, { returnDocument: "after" });
|
|
@@ -1534,6 +1641,7 @@ var JobProcessor = class {
|
|
|
1534
1641
|
*/
|
|
1535
1642
|
async failJob(job, error) {
|
|
1536
1643
|
if (!isPersistedJob(job)) return null;
|
|
1644
|
+
const now = /* @__PURE__ */ new Date();
|
|
1537
1645
|
const newFailCount = job.failCount + 1;
|
|
1538
1646
|
if (newFailCount >= this.ctx.options.maxRetries) {
|
|
1539
1647
|
const result = await this.ctx.collection.findOneAndUpdate({
|
|
@@ -1545,13 +1653,12 @@ var JobProcessor = class {
|
|
|
1545
1653
|
status: JobStatus.FAILED,
|
|
1546
1654
|
failCount: newFailCount,
|
|
1547
1655
|
failReason: error.message,
|
|
1548
|
-
updatedAt:
|
|
1656
|
+
updatedAt: now
|
|
1549
1657
|
},
|
|
1550
1658
|
$unset: {
|
|
1551
1659
|
lockedAt: "",
|
|
1552
1660
|
claimedBy: "",
|
|
1553
|
-
lastHeartbeat: ""
|
|
1554
|
-
heartbeatInterval: ""
|
|
1661
|
+
lastHeartbeat: ""
|
|
1555
1662
|
}
|
|
1556
1663
|
}, { returnDocument: "after" });
|
|
1557
1664
|
return result ? this.ctx.documentToPersistedJob(result) : null;
|
|
@@ -1567,13 +1674,12 @@ var JobProcessor = class {
|
|
|
1567
1674
|
failCount: newFailCount,
|
|
1568
1675
|
failReason: error.message,
|
|
1569
1676
|
nextRunAt,
|
|
1570
|
-
updatedAt:
|
|
1677
|
+
updatedAt: now
|
|
1571
1678
|
},
|
|
1572
1679
|
$unset: {
|
|
1573
1680
|
lockedAt: "",
|
|
1574
1681
|
claimedBy: "",
|
|
1575
|
-
lastHeartbeat: ""
|
|
1576
|
-
heartbeatInterval: ""
|
|
1682
|
+
lastHeartbeat: ""
|
|
1577
1683
|
}
|
|
1578
1684
|
}, { returnDocument: "after" });
|
|
1579
1685
|
return result ? this.ctx.documentToPersistedJob(result) : null;
|
|
@@ -1916,6 +2022,10 @@ var JobScheduler = class {
|
|
|
1916
2022
|
constructor(ctx) {
|
|
1917
2023
|
this.ctx = ctx;
|
|
1918
2024
|
}
|
|
2025
|
+
validateJobIdentifiers(name, uniqueKey) {
|
|
2026
|
+
validateJobName(name);
|
|
2027
|
+
if (uniqueKey !== void 0) validateUniqueKey(uniqueKey);
|
|
2028
|
+
}
|
|
1919
2029
|
/**
|
|
1920
2030
|
* Validate that the job data payload does not exceed the configured maximum BSON byte size.
|
|
1921
2031
|
*
|
|
@@ -1953,6 +2063,7 @@ var JobScheduler = class {
|
|
|
1953
2063
|
* @param data - Job payload, will be passed to the worker handler
|
|
1954
2064
|
* @param options - Scheduling and deduplication options
|
|
1955
2065
|
* @returns Promise resolving to the created or existing job document
|
|
2066
|
+
* @throws {InvalidJobIdentifierError} If `name` or `uniqueKey` fails public identifier validation
|
|
1956
2067
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
1957
2068
|
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
1958
2069
|
*
|
|
@@ -1982,6 +2093,7 @@ var JobScheduler = class {
|
|
|
1982
2093
|
* ```
|
|
1983
2094
|
*/
|
|
1984
2095
|
async enqueue(name, data, options = {}) {
|
|
2096
|
+
this.validateJobIdentifiers(name, options.uniqueKey);
|
|
1985
2097
|
this.validatePayloadSize(data);
|
|
1986
2098
|
const now = /* @__PURE__ */ new Date();
|
|
1987
2099
|
const job = {
|
|
@@ -1993,9 +2105,9 @@ var JobScheduler = class {
|
|
|
1993
2105
|
createdAt: now,
|
|
1994
2106
|
updatedAt: now
|
|
1995
2107
|
};
|
|
1996
|
-
if (options.uniqueKey) job.uniqueKey = options.uniqueKey;
|
|
2108
|
+
if (options.uniqueKey !== void 0) job.uniqueKey = options.uniqueKey;
|
|
1997
2109
|
try {
|
|
1998
|
-
if (options.uniqueKey) {
|
|
2110
|
+
if (options.uniqueKey !== void 0) {
|
|
1999
2111
|
const result = await this.ctx.collection.findOneAndUpdate({
|
|
2000
2112
|
name,
|
|
2001
2113
|
uniqueKey: options.uniqueKey,
|
|
@@ -2070,6 +2182,7 @@ var JobScheduler = class {
|
|
|
2070
2182
|
* @param data - Job payload, will be passed to the worker handler on each run
|
|
2071
2183
|
* @param options - Scheduling options (uniqueKey for deduplication)
|
|
2072
2184
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
2185
|
+
* @throws {InvalidJobIdentifierError} If `name` or `uniqueKey` fails public identifier validation
|
|
2073
2186
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
2074
2187
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2075
2188
|
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
@@ -2098,6 +2211,7 @@ var JobScheduler = class {
|
|
|
2098
2211
|
* ```
|
|
2099
2212
|
*/
|
|
2100
2213
|
async schedule(cron, name, data, options = {}) {
|
|
2214
|
+
this.validateJobIdentifiers(name, options.uniqueKey);
|
|
2101
2215
|
this.validatePayloadSize(data);
|
|
2102
2216
|
const nextRunAt = getNextCronDate(cron);
|
|
2103
2217
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -2111,9 +2225,9 @@ var JobScheduler = class {
|
|
|
2111
2225
|
createdAt: now,
|
|
2112
2226
|
updatedAt: now
|
|
2113
2227
|
};
|
|
2114
|
-
if (options.uniqueKey) job.uniqueKey = options.uniqueKey;
|
|
2228
|
+
if (options.uniqueKey !== void 0) job.uniqueKey = options.uniqueKey;
|
|
2115
2229
|
try {
|
|
2116
|
-
if (options.uniqueKey) {
|
|
2230
|
+
if (options.uniqueKey !== void 0) {
|
|
2117
2231
|
const result = await this.ctx.collection.findOneAndUpdate({
|
|
2118
2232
|
name,
|
|
2119
2233
|
uniqueKey: options.uniqueKey,
|
|
@@ -2147,6 +2261,10 @@ var JobScheduler = class {
|
|
|
2147
2261
|
*/
|
|
2148
2262
|
const DEFAULT_RETENTION_INTERVAL = 36e5;
|
|
2149
2263
|
/**
|
|
2264
|
+
* Statuses that are eligible for cleanup by the retention policy.
|
|
2265
|
+
*/
|
|
2266
|
+
const CLEANUP_STATUSES = [JobStatus.COMPLETED, JobStatus.FAILED];
|
|
2267
|
+
/**
|
|
2150
2268
|
* Manages scheduler lifecycle timers and job cleanup.
|
|
2151
2269
|
*
|
|
2152
2270
|
* Owns poll scheduling, heartbeat interval, cleanup interval, and the
|
|
@@ -2231,7 +2349,7 @@ var LifecycleManager = class {
|
|
|
2231
2349
|
if (!this.callbacks) return;
|
|
2232
2350
|
this.callbacks.poll().catch((error) => {
|
|
2233
2351
|
this.ctx.emit("job:error", { error: toError(error) });
|
|
2234
|
-
}).
|
|
2352
|
+
}).finally(() => {
|
|
2235
2353
|
this.scheduleNextPoll();
|
|
2236
2354
|
});
|
|
2237
2355
|
}
|
|
@@ -2373,6 +2491,14 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2373
2491
|
workers = /* @__PURE__ */ new Map();
|
|
2374
2492
|
isRunning = false;
|
|
2375
2493
|
isInitialized = false;
|
|
2494
|
+
/**
|
|
2495
|
+
* Resolve function for the reactive shutdown drain promise.
|
|
2496
|
+
* Set during stop() when active jobs need to finish; called by
|
|
2497
|
+
* onJobFinished() when the last active job completes.
|
|
2498
|
+
*
|
|
2499
|
+
* @private
|
|
2500
|
+
*/
|
|
2501
|
+
_drainResolve = null;
|
|
2376
2502
|
_scheduler = null;
|
|
2377
2503
|
_manager = null;
|
|
2378
2504
|
_query = null;
|
|
@@ -2381,6 +2507,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2381
2507
|
_lifecycleManager = null;
|
|
2382
2508
|
constructor(db, options = {}) {
|
|
2383
2509
|
super();
|
|
2510
|
+
this.setMaxListeners(20);
|
|
2384
2511
|
this.db = db;
|
|
2385
2512
|
this.options = {
|
|
2386
2513
|
collectionName: options.collectionName ?? DEFAULTS.collectionName,
|
|
@@ -2401,6 +2528,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2401
2528
|
maxPayloadSize: options.maxPayloadSize,
|
|
2402
2529
|
statsCacheTtlMs: options.statsCacheTtlMs ?? 5e3
|
|
2403
2530
|
};
|
|
2531
|
+
if (options.defaultConcurrency !== void 0) console.warn("[@monque/core] \"defaultConcurrency\" is deprecated and will be removed in a future major version. Use \"workerConcurrency\" instead.");
|
|
2532
|
+
if (options.maxConcurrency !== void 0) console.warn("[@monque/core] \"maxConcurrency\" is deprecated and will be removed in a future major version. Use \"instanceConcurrency\" instead.");
|
|
2404
2533
|
}
|
|
2405
2534
|
/**
|
|
2406
2535
|
* Initialize the scheduler by setting up the MongoDB collection and indexes.
|
|
@@ -2457,6 +2586,10 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2457
2586
|
if (!this._lifecycleManager) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
2458
2587
|
return this._lifecycleManager;
|
|
2459
2588
|
}
|
|
2589
|
+
validateSchedulingIdentifiers(name, uniqueKey) {
|
|
2590
|
+
validateJobName(name);
|
|
2591
|
+
if (uniqueKey !== void 0) validateUniqueKey(uniqueKey);
|
|
2592
|
+
}
|
|
2460
2593
|
/**
|
|
2461
2594
|
* Handle a change-stream-triggered poll and reset the safety poll timer.
|
|
2462
2595
|
*
|
|
@@ -2484,6 +2617,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2484
2617
|
if (!this.isRunning || !this._changeStreamHandler) return;
|
|
2485
2618
|
this._changeStreamHandler.notifyPendingJob(name, nextRunAt);
|
|
2486
2619
|
},
|
|
2620
|
+
notifyJobFinished: () => this.onJobFinished(),
|
|
2487
2621
|
documentToPersistedJob: (doc) => documentToPersistedJob(doc)
|
|
2488
2622
|
};
|
|
2489
2623
|
}
|
|
@@ -2557,7 +2691,18 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2557
2691
|
lastHeartbeat: 1
|
|
2558
2692
|
},
|
|
2559
2693
|
background: true
|
|
2560
|
-
}
|
|
2694
|
+
},
|
|
2695
|
+
...this.options.jobRetention ? [{
|
|
2696
|
+
key: {
|
|
2697
|
+
status: 1,
|
|
2698
|
+
updatedAt: 1
|
|
2699
|
+
},
|
|
2700
|
+
background: true,
|
|
2701
|
+
partialFilterExpression: {
|
|
2702
|
+
status: { $in: CLEANUP_STATUSES },
|
|
2703
|
+
updatedAt: { $exists: true }
|
|
2704
|
+
}
|
|
2705
|
+
}] : []
|
|
2561
2706
|
]);
|
|
2562
2707
|
}
|
|
2563
2708
|
/**
|
|
@@ -2579,8 +2724,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2579
2724
|
$unset: {
|
|
2580
2725
|
lockedAt: "",
|
|
2581
2726
|
claimedBy: "",
|
|
2582
|
-
lastHeartbeat: ""
|
|
2583
|
-
heartbeatInterval: ""
|
|
2727
|
+
lastHeartbeat: ""
|
|
2584
2728
|
}
|
|
2585
2729
|
});
|
|
2586
2730
|
if (result.modifiedCount > 0) this.emit("stale:recovered", { count: result.modifiedCount });
|
|
@@ -2621,6 +2765,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2621
2765
|
* @param data - Job payload, will be passed to the worker handler
|
|
2622
2766
|
* @param options - Scheduling and deduplication options
|
|
2623
2767
|
* @returns Promise resolving to the created or existing job document
|
|
2768
|
+
* @throws {InvalidJobIdentifierError} If `name` or `uniqueKey` fails public identifier validation
|
|
2624
2769
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2625
2770
|
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
2626
2771
|
*
|
|
@@ -2653,6 +2798,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2653
2798
|
*/
|
|
2654
2799
|
async enqueue(name, data, options = {}) {
|
|
2655
2800
|
this.ensureInitialized();
|
|
2801
|
+
this.validateSchedulingIdentifiers(name, options.uniqueKey);
|
|
2656
2802
|
return this.scheduler.enqueue(name, data, options);
|
|
2657
2803
|
}
|
|
2658
2804
|
/**
|
|
@@ -2665,6 +2811,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2665
2811
|
* @param name - Job type identifier, must match a registered worker
|
|
2666
2812
|
* @param data - Job payload, will be passed to the worker handler
|
|
2667
2813
|
* @returns Promise resolving to the created job document
|
|
2814
|
+
* @throws {InvalidJobIdentifierError} If `name` fails public identifier validation
|
|
2668
2815
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2669
2816
|
*
|
|
2670
2817
|
* @example Send email immediately
|
|
@@ -2687,6 +2834,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2687
2834
|
*/
|
|
2688
2835
|
async now(name, data) {
|
|
2689
2836
|
this.ensureInitialized();
|
|
2837
|
+
validateJobName(name);
|
|
2690
2838
|
return this.scheduler.now(name, data);
|
|
2691
2839
|
}
|
|
2692
2840
|
/**
|
|
@@ -2707,6 +2855,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2707
2855
|
* @param data - Job payload, will be passed to the worker handler on each run
|
|
2708
2856
|
* @param options - Scheduling options (uniqueKey for deduplication)
|
|
2709
2857
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
2858
|
+
* @throws {InvalidJobIdentifierError} If `name` or `uniqueKey` fails public identifier validation
|
|
2710
2859
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
2711
2860
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2712
2861
|
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
@@ -2738,6 +2887,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2738
2887
|
*/
|
|
2739
2888
|
async schedule(cron, name, data, options = {}) {
|
|
2740
2889
|
this.ensureInitialized();
|
|
2890
|
+
this.validateSchedulingIdentifiers(name, options.uniqueKey);
|
|
2741
2891
|
return this.scheduler.schedule(cron, name, data, options);
|
|
2742
2892
|
}
|
|
2743
2893
|
/**
|
|
@@ -3083,6 +3233,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
3083
3233
|
* @param options - Worker configuration
|
|
3084
3234
|
* @param options.concurrency - Maximum concurrent jobs for this worker (default: `defaultConcurrency`)
|
|
3085
3235
|
* @param options.replace - When `true`, replace existing worker instead of throwing error
|
|
3236
|
+
* @throws {InvalidJobIdentifierError} If `name` fails public identifier validation
|
|
3086
3237
|
* @throws {WorkerRegistrationError} When a worker is already registered for `name` and `replace` is not `true`
|
|
3087
3238
|
*
|
|
3088
3239
|
* @example Basic email worker
|
|
@@ -3126,6 +3277,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
3126
3277
|
* ```
|
|
3127
3278
|
*/
|
|
3128
3279
|
register(name, handler, options = {}) {
|
|
3280
|
+
validateJobName(name);
|
|
3129
3281
|
const concurrency = options.concurrency ?? this.options.workerConcurrency;
|
|
3130
3282
|
if (this.workers.has(name) && options.replace !== true) throw new WorkerRegistrationError(`Worker already registered for job name "${name}". Use { replace: true } to replace.`, name);
|
|
3131
3283
|
this.workers.set(name, {
|
|
@@ -3229,25 +3381,15 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
3229
3381
|
try {
|
|
3230
3382
|
await this.changeStreamHandler.close();
|
|
3231
3383
|
} catch {}
|
|
3232
|
-
if (this.
|
|
3233
|
-
let checkInterval;
|
|
3384
|
+
if (this.getActiveJobCount() === 0) return;
|
|
3234
3385
|
const waitForJobs = new Promise((resolve) => {
|
|
3235
|
-
|
|
3236
|
-
if (this.getActiveJobs().length === 0) {
|
|
3237
|
-
clearInterval(checkInterval);
|
|
3238
|
-
resolve(void 0);
|
|
3239
|
-
}
|
|
3240
|
-
}, 100);
|
|
3386
|
+
this._drainResolve = () => resolve(void 0);
|
|
3241
3387
|
});
|
|
3242
3388
|
const timeout = new Promise((resolve) => {
|
|
3243
3389
|
setTimeout(() => resolve("timeout"), this.options.shutdownTimeout);
|
|
3244
3390
|
});
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
result = await Promise.race([waitForJobs, timeout]);
|
|
3248
|
-
} finally {
|
|
3249
|
-
if (checkInterval) clearInterval(checkInterval);
|
|
3250
|
-
}
|
|
3391
|
+
const result = await Promise.race([waitForJobs, timeout]);
|
|
3392
|
+
this._drainResolve = null;
|
|
3251
3393
|
if (result === "timeout") {
|
|
3252
3394
|
const incompleteJobs = this.getActiveJobsList();
|
|
3253
3395
|
const error = new ShutdownTimeoutError(`Shutdown timed out after ${this.options.shutdownTimeout}ms with ${incompleteJobs.length} incomplete jobs`, incompleteJobs);
|
|
@@ -3304,6 +3446,15 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
3304
3446
|
return this.isRunning && this.isInitialized && this.collection !== null;
|
|
3305
3447
|
}
|
|
3306
3448
|
/**
|
|
3449
|
+
* Called when a job finishes processing. If a shutdown drain is pending
|
|
3450
|
+
* and no active jobs remain, resolves the drain promise.
|
|
3451
|
+
*
|
|
3452
|
+
* @private
|
|
3453
|
+
*/
|
|
3454
|
+
onJobFinished() {
|
|
3455
|
+
if (this._drainResolve && this.getActiveJobCount() === 0) this._drainResolve();
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3307
3458
|
* Ensure the scheduler is initialized before operations.
|
|
3308
3459
|
*
|
|
3309
3460
|
* @private
|
|
@@ -3313,15 +3464,18 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
3313
3464
|
if (!this.isInitialized || !this.collection) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
3314
3465
|
}
|
|
3315
3466
|
/**
|
|
3316
|
-
* Get
|
|
3467
|
+
* Get total count of active jobs across all workers.
|
|
3468
|
+
*
|
|
3469
|
+
* Returns only the count (O(workers)) instead of allocating
|
|
3470
|
+
* a throw-away array of IDs, since callers only need `.length`.
|
|
3317
3471
|
*
|
|
3318
3472
|
* @private
|
|
3319
|
-
* @returns
|
|
3473
|
+
* @returns Number of jobs currently being processed
|
|
3320
3474
|
*/
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
for (const worker of this.workers.values())
|
|
3324
|
-
return
|
|
3475
|
+
getActiveJobCount() {
|
|
3476
|
+
let count = 0;
|
|
3477
|
+
for (const worker of this.workers.values()) count += worker.activeJobs.size;
|
|
3478
|
+
return count;
|
|
3325
3479
|
}
|
|
3326
3480
|
/**
|
|
3327
3481
|
* Get list of active job documents (for shutdown timeout error).
|
|
@@ -3358,6 +3512,7 @@ exports.DEFAULT_BASE_INTERVAL = DEFAULT_BASE_INTERVAL;
|
|
|
3358
3512
|
exports.DEFAULT_MAX_BACKOFF_DELAY = DEFAULT_MAX_BACKOFF_DELAY;
|
|
3359
3513
|
exports.InvalidCronError = InvalidCronError;
|
|
3360
3514
|
exports.InvalidCursorError = InvalidCursorError;
|
|
3515
|
+
exports.InvalidJobIdentifierError = InvalidJobIdentifierError;
|
|
3361
3516
|
exports.JobStateError = JobStateError;
|
|
3362
3517
|
exports.JobStatus = JobStatus;
|
|
3363
3518
|
exports.Monque = Monque;
|
|
@@ -3377,5 +3532,7 @@ exports.isProcessingJob = isProcessingJob;
|
|
|
3377
3532
|
exports.isRecurringJob = isRecurringJob;
|
|
3378
3533
|
exports.isValidJobStatus = isValidJobStatus;
|
|
3379
3534
|
exports.validateCronExpression = validateCronExpression;
|
|
3535
|
+
exports.validateJobName = validateJobName;
|
|
3536
|
+
exports.validateUniqueKey = validateUniqueKey;
|
|
3380
3537
|
|
|
3381
3538
|
//# sourceMappingURL=index.cjs.map
|