@nicnocquee/dataqueue 1.30.0 → 1.31.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/index.cjs +769 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -1
- package/dist/index.d.ts +216 -1
- package/dist/index.js +768 -4
- package/dist/index.js.map +1 -1
- package/migrations/1781200000004_create_cron_schedules_table.sql +33 -0
- package/package.json +3 -2
- package/src/backend.ts +69 -0
- package/src/backends/postgres.ts +331 -1
- package/src/backends/redis.test.ts +350 -0
- package/src/backends/redis.ts +389 -1
- package/src/cron.test.ts +126 -0
- package/src/cron.ts +40 -0
- package/src/index.test.ts +361 -0
- package/src/index.ts +157 -4
- package/src/processor.ts +22 -4
- package/src/types.ts +149 -0
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var pg = require('pg');
|
|
|
7
7
|
var pgConnectionString = require('pg-connection-string');
|
|
8
8
|
var fs = require('fs');
|
|
9
9
|
var module$1 = require('module');
|
|
10
|
+
var croner = require('croner');
|
|
10
11
|
|
|
11
12
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
13
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -1005,6 +1006,297 @@ var PostgresBackend = class {
|
|
|
1005
1006
|
client.release();
|
|
1006
1007
|
}
|
|
1007
1008
|
}
|
|
1009
|
+
// ── Cron schedules ──────────────────────────────────────────────────
|
|
1010
|
+
/** Create a cron schedule and return its ID. */
|
|
1011
|
+
async addCronSchedule(input) {
|
|
1012
|
+
const client = await this.pool.connect();
|
|
1013
|
+
try {
|
|
1014
|
+
const result = await client.query(
|
|
1015
|
+
`INSERT INTO cron_schedules
|
|
1016
|
+
(schedule_name, cron_expression, job_type, payload, max_attempts,
|
|
1017
|
+
priority, timeout_ms, force_kill_on_timeout, tags, timezone,
|
|
1018
|
+
allow_overlap, next_run_at)
|
|
1019
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
1020
|
+
RETURNING id`,
|
|
1021
|
+
[
|
|
1022
|
+
input.scheduleName,
|
|
1023
|
+
input.cronExpression,
|
|
1024
|
+
input.jobType,
|
|
1025
|
+
input.payload,
|
|
1026
|
+
input.maxAttempts,
|
|
1027
|
+
input.priority,
|
|
1028
|
+
input.timeoutMs,
|
|
1029
|
+
input.forceKillOnTimeout,
|
|
1030
|
+
input.tags ?? null,
|
|
1031
|
+
input.timezone,
|
|
1032
|
+
input.allowOverlap,
|
|
1033
|
+
input.nextRunAt
|
|
1034
|
+
]
|
|
1035
|
+
);
|
|
1036
|
+
const id = result.rows[0].id;
|
|
1037
|
+
log(`Added cron schedule ${id}: "${input.scheduleName}"`);
|
|
1038
|
+
return id;
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (error?.code === "23505") {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`Cron schedule with name "${input.scheduleName}" already exists`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
log(`Error adding cron schedule: ${error}`);
|
|
1046
|
+
throw error;
|
|
1047
|
+
} finally {
|
|
1048
|
+
client.release();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/** Get a cron schedule by ID. */
|
|
1052
|
+
async getCronSchedule(id) {
|
|
1053
|
+
const client = await this.pool.connect();
|
|
1054
|
+
try {
|
|
1055
|
+
const result = await client.query(
|
|
1056
|
+
`SELECT id, schedule_name AS "scheduleName", cron_expression AS "cronExpression",
|
|
1057
|
+
job_type AS "jobType", payload, max_attempts AS "maxAttempts",
|
|
1058
|
+
priority, timeout_ms AS "timeoutMs",
|
|
1059
|
+
force_kill_on_timeout AS "forceKillOnTimeout", tags,
|
|
1060
|
+
timezone, allow_overlap AS "allowOverlap", status,
|
|
1061
|
+
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1062
|
+
next_run_at AS "nextRunAt",
|
|
1063
|
+
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
1064
|
+
FROM cron_schedules WHERE id = $1`,
|
|
1065
|
+
[id]
|
|
1066
|
+
);
|
|
1067
|
+
if (result.rows.length === 0) return null;
|
|
1068
|
+
return result.rows[0];
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
log(`Error getting cron schedule ${id}: ${error}`);
|
|
1071
|
+
throw error;
|
|
1072
|
+
} finally {
|
|
1073
|
+
client.release();
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/** Get a cron schedule by its unique name. */
|
|
1077
|
+
async getCronScheduleByName(name) {
|
|
1078
|
+
const client = await this.pool.connect();
|
|
1079
|
+
try {
|
|
1080
|
+
const result = await client.query(
|
|
1081
|
+
`SELECT id, schedule_name AS "scheduleName", cron_expression AS "cronExpression",
|
|
1082
|
+
job_type AS "jobType", payload, max_attempts AS "maxAttempts",
|
|
1083
|
+
priority, timeout_ms AS "timeoutMs",
|
|
1084
|
+
force_kill_on_timeout AS "forceKillOnTimeout", tags,
|
|
1085
|
+
timezone, allow_overlap AS "allowOverlap", status,
|
|
1086
|
+
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1087
|
+
next_run_at AS "nextRunAt",
|
|
1088
|
+
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
1089
|
+
FROM cron_schedules WHERE schedule_name = $1`,
|
|
1090
|
+
[name]
|
|
1091
|
+
);
|
|
1092
|
+
if (result.rows.length === 0) return null;
|
|
1093
|
+
return result.rows[0];
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
log(`Error getting cron schedule by name "${name}": ${error}`);
|
|
1096
|
+
throw error;
|
|
1097
|
+
} finally {
|
|
1098
|
+
client.release();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/** List cron schedules, optionally filtered by status. */
|
|
1102
|
+
async listCronSchedules(status) {
|
|
1103
|
+
const client = await this.pool.connect();
|
|
1104
|
+
try {
|
|
1105
|
+
let query = `SELECT id, schedule_name AS "scheduleName", cron_expression AS "cronExpression",
|
|
1106
|
+
job_type AS "jobType", payload, max_attempts AS "maxAttempts",
|
|
1107
|
+
priority, timeout_ms AS "timeoutMs",
|
|
1108
|
+
force_kill_on_timeout AS "forceKillOnTimeout", tags,
|
|
1109
|
+
timezone, allow_overlap AS "allowOverlap", status,
|
|
1110
|
+
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1111
|
+
next_run_at AS "nextRunAt",
|
|
1112
|
+
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
1113
|
+
FROM cron_schedules`;
|
|
1114
|
+
const params = [];
|
|
1115
|
+
if (status) {
|
|
1116
|
+
query += ` WHERE status = $1`;
|
|
1117
|
+
params.push(status);
|
|
1118
|
+
}
|
|
1119
|
+
query += ` ORDER BY created_at ASC`;
|
|
1120
|
+
const result = await client.query(query, params);
|
|
1121
|
+
return result.rows;
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
log(`Error listing cron schedules: ${error}`);
|
|
1124
|
+
throw error;
|
|
1125
|
+
} finally {
|
|
1126
|
+
client.release();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/** Delete a cron schedule by ID. */
|
|
1130
|
+
async removeCronSchedule(id) {
|
|
1131
|
+
const client = await this.pool.connect();
|
|
1132
|
+
try {
|
|
1133
|
+
await client.query(`DELETE FROM cron_schedules WHERE id = $1`, [id]);
|
|
1134
|
+
log(`Removed cron schedule ${id}`);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
log(`Error removing cron schedule ${id}: ${error}`);
|
|
1137
|
+
throw error;
|
|
1138
|
+
} finally {
|
|
1139
|
+
client.release();
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/** Pause a cron schedule. */
|
|
1143
|
+
async pauseCronSchedule(id) {
|
|
1144
|
+
const client = await this.pool.connect();
|
|
1145
|
+
try {
|
|
1146
|
+
await client.query(
|
|
1147
|
+
`UPDATE cron_schedules SET status = 'paused', updated_at = NOW() WHERE id = $1`,
|
|
1148
|
+
[id]
|
|
1149
|
+
);
|
|
1150
|
+
log(`Paused cron schedule ${id}`);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
log(`Error pausing cron schedule ${id}: ${error}`);
|
|
1153
|
+
throw error;
|
|
1154
|
+
} finally {
|
|
1155
|
+
client.release();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
/** Resume a paused cron schedule. */
|
|
1159
|
+
async resumeCronSchedule(id) {
|
|
1160
|
+
const client = await this.pool.connect();
|
|
1161
|
+
try {
|
|
1162
|
+
await client.query(
|
|
1163
|
+
`UPDATE cron_schedules SET status = 'active', updated_at = NOW() WHERE id = $1`,
|
|
1164
|
+
[id]
|
|
1165
|
+
);
|
|
1166
|
+
log(`Resumed cron schedule ${id}`);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
log(`Error resuming cron schedule ${id}: ${error}`);
|
|
1169
|
+
throw error;
|
|
1170
|
+
} finally {
|
|
1171
|
+
client.release();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/** Edit a cron schedule. */
|
|
1175
|
+
async editCronSchedule(id, updates, nextRunAt) {
|
|
1176
|
+
const client = await this.pool.connect();
|
|
1177
|
+
try {
|
|
1178
|
+
const updateFields = [];
|
|
1179
|
+
const params = [];
|
|
1180
|
+
let paramIdx = 1;
|
|
1181
|
+
if (updates.cronExpression !== void 0) {
|
|
1182
|
+
updateFields.push(`cron_expression = $${paramIdx++}`);
|
|
1183
|
+
params.push(updates.cronExpression);
|
|
1184
|
+
}
|
|
1185
|
+
if (updates.payload !== void 0) {
|
|
1186
|
+
updateFields.push(`payload = $${paramIdx++}`);
|
|
1187
|
+
params.push(updates.payload);
|
|
1188
|
+
}
|
|
1189
|
+
if (updates.maxAttempts !== void 0) {
|
|
1190
|
+
updateFields.push(`max_attempts = $${paramIdx++}`);
|
|
1191
|
+
params.push(updates.maxAttempts);
|
|
1192
|
+
}
|
|
1193
|
+
if (updates.priority !== void 0) {
|
|
1194
|
+
updateFields.push(`priority = $${paramIdx++}`);
|
|
1195
|
+
params.push(updates.priority);
|
|
1196
|
+
}
|
|
1197
|
+
if (updates.timeoutMs !== void 0) {
|
|
1198
|
+
updateFields.push(`timeout_ms = $${paramIdx++}`);
|
|
1199
|
+
params.push(updates.timeoutMs);
|
|
1200
|
+
}
|
|
1201
|
+
if (updates.forceKillOnTimeout !== void 0) {
|
|
1202
|
+
updateFields.push(`force_kill_on_timeout = $${paramIdx++}`);
|
|
1203
|
+
params.push(updates.forceKillOnTimeout);
|
|
1204
|
+
}
|
|
1205
|
+
if (updates.tags !== void 0) {
|
|
1206
|
+
updateFields.push(`tags = $${paramIdx++}`);
|
|
1207
|
+
params.push(updates.tags);
|
|
1208
|
+
}
|
|
1209
|
+
if (updates.timezone !== void 0) {
|
|
1210
|
+
updateFields.push(`timezone = $${paramIdx++}`);
|
|
1211
|
+
params.push(updates.timezone);
|
|
1212
|
+
}
|
|
1213
|
+
if (updates.allowOverlap !== void 0) {
|
|
1214
|
+
updateFields.push(`allow_overlap = $${paramIdx++}`);
|
|
1215
|
+
params.push(updates.allowOverlap);
|
|
1216
|
+
}
|
|
1217
|
+
if (nextRunAt !== void 0) {
|
|
1218
|
+
updateFields.push(`next_run_at = $${paramIdx++}`);
|
|
1219
|
+
params.push(nextRunAt);
|
|
1220
|
+
}
|
|
1221
|
+
if (updateFields.length === 0) {
|
|
1222
|
+
log(`No fields to update for cron schedule ${id}`);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
updateFields.push(`updated_at = NOW()`);
|
|
1226
|
+
params.push(id);
|
|
1227
|
+
const query = `UPDATE cron_schedules SET ${updateFields.join(", ")} WHERE id = $${paramIdx}`;
|
|
1228
|
+
await client.query(query, params);
|
|
1229
|
+
log(`Edited cron schedule ${id}`);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
log(`Error editing cron schedule ${id}: ${error}`);
|
|
1232
|
+
throw error;
|
|
1233
|
+
} finally {
|
|
1234
|
+
client.release();
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Atomically fetch all active cron schedules whose nextRunAt <= NOW().
|
|
1239
|
+
* Uses FOR UPDATE SKIP LOCKED to prevent duplicate enqueuing across workers.
|
|
1240
|
+
*/
|
|
1241
|
+
async getDueCronSchedules() {
|
|
1242
|
+
const client = await this.pool.connect();
|
|
1243
|
+
try {
|
|
1244
|
+
const result = await client.query(
|
|
1245
|
+
`SELECT id, schedule_name AS "scheduleName", cron_expression AS "cronExpression",
|
|
1246
|
+
job_type AS "jobType", payload, max_attempts AS "maxAttempts",
|
|
1247
|
+
priority, timeout_ms AS "timeoutMs",
|
|
1248
|
+
force_kill_on_timeout AS "forceKillOnTimeout", tags,
|
|
1249
|
+
timezone, allow_overlap AS "allowOverlap", status,
|
|
1250
|
+
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1251
|
+
next_run_at AS "nextRunAt",
|
|
1252
|
+
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
1253
|
+
FROM cron_schedules
|
|
1254
|
+
WHERE status = 'active'
|
|
1255
|
+
AND next_run_at IS NOT NULL
|
|
1256
|
+
AND next_run_at <= NOW()
|
|
1257
|
+
ORDER BY next_run_at ASC
|
|
1258
|
+
FOR UPDATE SKIP LOCKED`
|
|
1259
|
+
);
|
|
1260
|
+
log(`Found ${result.rows.length} due cron schedules`);
|
|
1261
|
+
return result.rows;
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
if (error?.code === "42P01") {
|
|
1264
|
+
log("cron_schedules table does not exist, skipping cron enqueue");
|
|
1265
|
+
return [];
|
|
1266
|
+
}
|
|
1267
|
+
log(`Error getting due cron schedules: ${error}`);
|
|
1268
|
+
throw error;
|
|
1269
|
+
} finally {
|
|
1270
|
+
client.release();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Update a cron schedule after a job has been enqueued.
|
|
1275
|
+
* Sets lastEnqueuedAt, lastJobId, and advances nextRunAt.
|
|
1276
|
+
*/
|
|
1277
|
+
async updateCronScheduleAfterEnqueue(id, lastEnqueuedAt, lastJobId, nextRunAt) {
|
|
1278
|
+
const client = await this.pool.connect();
|
|
1279
|
+
try {
|
|
1280
|
+
await client.query(
|
|
1281
|
+
`UPDATE cron_schedules
|
|
1282
|
+
SET last_enqueued_at = $2,
|
|
1283
|
+
last_job_id = $3,
|
|
1284
|
+
next_run_at = $4,
|
|
1285
|
+
updated_at = NOW()
|
|
1286
|
+
WHERE id = $1`,
|
|
1287
|
+
[id, lastEnqueuedAt, lastJobId, nextRunAt]
|
|
1288
|
+
);
|
|
1289
|
+
log(
|
|
1290
|
+
`Updated cron schedule ${id}: lastJobId=${lastJobId}, nextRunAt=${nextRunAt?.toISOString() ?? "null"}`
|
|
1291
|
+
);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
log(`Error updating cron schedule ${id} after enqueue: ${error}`);
|
|
1294
|
+
throw error;
|
|
1295
|
+
} finally {
|
|
1296
|
+
client.release();
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
1008
1300
|
async setPendingReasonForUnpickedJobs(reason, jobType) {
|
|
1009
1301
|
const client = await this.pool.connect();
|
|
1010
1302
|
try {
|
|
@@ -1735,7 +2027,7 @@ async function processBatchWithHandlers(backend, workerId, batchSize, jobType, j
|
|
|
1735
2027
|
next();
|
|
1736
2028
|
});
|
|
1737
2029
|
}
|
|
1738
|
-
var createProcessor = (backend, handlers, options = {}) => {
|
|
2030
|
+
var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
1739
2031
|
const {
|
|
1740
2032
|
workerId = `worker-${Math.random().toString(36).substring(2, 9)}`,
|
|
1741
2033
|
batchSize = 10,
|
|
@@ -1750,6 +2042,18 @@ var createProcessor = (backend, handlers, options = {}) => {
|
|
|
1750
2042
|
setLogContext(options.verbose ?? false);
|
|
1751
2043
|
const processJobs = async () => {
|
|
1752
2044
|
if (!running) return 0;
|
|
2045
|
+
if (onBeforeBatch) {
|
|
2046
|
+
try {
|
|
2047
|
+
await onBeforeBatch();
|
|
2048
|
+
} catch (hookError) {
|
|
2049
|
+
log(`onBeforeBatch hook error: ${hookError}`);
|
|
2050
|
+
if (onError) {
|
|
2051
|
+
onError(
|
|
2052
|
+
hookError instanceof Error ? hookError : new Error(String(hookError))
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1753
2057
|
log(
|
|
1754
2058
|
`Processing jobs with workerId: ${workerId}${jobType ? ` and jobType: ${Array.isArray(jobType) ? jobType.join(",") : jobType}` : ""}`
|
|
1755
2059
|
);
|
|
@@ -2968,6 +3272,332 @@ var RedisBackend = class {
|
|
|
2968
3272
|
return true;
|
|
2969
3273
|
});
|
|
2970
3274
|
}
|
|
3275
|
+
// ── Cron schedules ──────────────────────────────────────────────────
|
|
3276
|
+
/** Create a cron schedule and return its ID. */
|
|
3277
|
+
async addCronSchedule(input) {
|
|
3278
|
+
const existingId = await this.client.get(
|
|
3279
|
+
`${this.prefix}cron_name:${input.scheduleName}`
|
|
3280
|
+
);
|
|
3281
|
+
if (existingId !== null) {
|
|
3282
|
+
throw new Error(
|
|
3283
|
+
`Cron schedule with name "${input.scheduleName}" already exists`
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
const id = await this.client.incr(`${this.prefix}cron_id_seq`);
|
|
3287
|
+
const now = this.nowMs();
|
|
3288
|
+
const key = `${this.prefix}cron:${id}`;
|
|
3289
|
+
const fields = [
|
|
3290
|
+
"id",
|
|
3291
|
+
id.toString(),
|
|
3292
|
+
"scheduleName",
|
|
3293
|
+
input.scheduleName,
|
|
3294
|
+
"cronExpression",
|
|
3295
|
+
input.cronExpression,
|
|
3296
|
+
"jobType",
|
|
3297
|
+
input.jobType,
|
|
3298
|
+
"payload",
|
|
3299
|
+
JSON.stringify(input.payload),
|
|
3300
|
+
"maxAttempts",
|
|
3301
|
+
input.maxAttempts.toString(),
|
|
3302
|
+
"priority",
|
|
3303
|
+
input.priority.toString(),
|
|
3304
|
+
"timeoutMs",
|
|
3305
|
+
input.timeoutMs !== null ? input.timeoutMs.toString() : "null",
|
|
3306
|
+
"forceKillOnTimeout",
|
|
3307
|
+
input.forceKillOnTimeout ? "true" : "false",
|
|
3308
|
+
"tags",
|
|
3309
|
+
input.tags ? JSON.stringify(input.tags) : "null",
|
|
3310
|
+
"timezone",
|
|
3311
|
+
input.timezone,
|
|
3312
|
+
"allowOverlap",
|
|
3313
|
+
input.allowOverlap ? "true" : "false",
|
|
3314
|
+
"status",
|
|
3315
|
+
"active",
|
|
3316
|
+
"lastEnqueuedAt",
|
|
3317
|
+
"null",
|
|
3318
|
+
"lastJobId",
|
|
3319
|
+
"null",
|
|
3320
|
+
"nextRunAt",
|
|
3321
|
+
input.nextRunAt ? input.nextRunAt.getTime().toString() : "null",
|
|
3322
|
+
"createdAt",
|
|
3323
|
+
now.toString(),
|
|
3324
|
+
"updatedAt",
|
|
3325
|
+
now.toString()
|
|
3326
|
+
];
|
|
3327
|
+
await this.client.hmset(key, ...fields);
|
|
3328
|
+
await this.client.set(
|
|
3329
|
+
`${this.prefix}cron_name:${input.scheduleName}`,
|
|
3330
|
+
id.toString()
|
|
3331
|
+
);
|
|
3332
|
+
await this.client.sadd(`${this.prefix}crons`, id.toString());
|
|
3333
|
+
await this.client.sadd(`${this.prefix}cron_status:active`, id.toString());
|
|
3334
|
+
if (input.nextRunAt) {
|
|
3335
|
+
await this.client.zadd(
|
|
3336
|
+
`${this.prefix}cron_due`,
|
|
3337
|
+
input.nextRunAt.getTime(),
|
|
3338
|
+
id.toString()
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
log(`Added cron schedule ${id}: "${input.scheduleName}"`);
|
|
3342
|
+
return id;
|
|
3343
|
+
}
|
|
3344
|
+
/** Get a cron schedule by ID. */
|
|
3345
|
+
async getCronSchedule(id) {
|
|
3346
|
+
const data = await this.client.hgetall(`${this.prefix}cron:${id}`);
|
|
3347
|
+
if (!data || Object.keys(data).length === 0) return null;
|
|
3348
|
+
return this.deserializeCronSchedule(data);
|
|
3349
|
+
}
|
|
3350
|
+
/** Get a cron schedule by its unique name. */
|
|
3351
|
+
async getCronScheduleByName(name) {
|
|
3352
|
+
const id = await this.client.get(`${this.prefix}cron_name:${name}`);
|
|
3353
|
+
if (id === null) return null;
|
|
3354
|
+
return this.getCronSchedule(Number(id));
|
|
3355
|
+
}
|
|
3356
|
+
/** List cron schedules, optionally filtered by status. */
|
|
3357
|
+
async listCronSchedules(status) {
|
|
3358
|
+
let ids;
|
|
3359
|
+
if (status) {
|
|
3360
|
+
ids = await this.client.smembers(`${this.prefix}cron_status:${status}`);
|
|
3361
|
+
} else {
|
|
3362
|
+
ids = await this.client.smembers(`${this.prefix}crons`);
|
|
3363
|
+
}
|
|
3364
|
+
if (ids.length === 0) return [];
|
|
3365
|
+
const pipeline = this.client.pipeline();
|
|
3366
|
+
for (const id of ids) {
|
|
3367
|
+
pipeline.hgetall(`${this.prefix}cron:${id}`);
|
|
3368
|
+
}
|
|
3369
|
+
const results = await pipeline.exec();
|
|
3370
|
+
const schedules = [];
|
|
3371
|
+
if (results) {
|
|
3372
|
+
for (const [err, data] of results) {
|
|
3373
|
+
if (!err && data && typeof data === "object" && Object.keys(data).length > 0) {
|
|
3374
|
+
schedules.push(
|
|
3375
|
+
this.deserializeCronSchedule(data)
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
schedules.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
3381
|
+
return schedules;
|
|
3382
|
+
}
|
|
3383
|
+
/** Delete a cron schedule by ID. */
|
|
3384
|
+
async removeCronSchedule(id) {
|
|
3385
|
+
const data = await this.client.hgetall(`${this.prefix}cron:${id}`);
|
|
3386
|
+
if (!data || Object.keys(data).length === 0) return;
|
|
3387
|
+
const name = data.scheduleName;
|
|
3388
|
+
const status = data.status;
|
|
3389
|
+
await this.client.del(`${this.prefix}cron:${id}`);
|
|
3390
|
+
await this.client.del(`${this.prefix}cron_name:${name}`);
|
|
3391
|
+
await this.client.srem(`${this.prefix}crons`, id.toString());
|
|
3392
|
+
await this.client.srem(
|
|
3393
|
+
`${this.prefix}cron_status:${status}`,
|
|
3394
|
+
id.toString()
|
|
3395
|
+
);
|
|
3396
|
+
await this.client.zrem(`${this.prefix}cron_due`, id.toString());
|
|
3397
|
+
log(`Removed cron schedule ${id}`);
|
|
3398
|
+
}
|
|
3399
|
+
/** Pause a cron schedule. */
|
|
3400
|
+
async pauseCronSchedule(id) {
|
|
3401
|
+
const now = this.nowMs();
|
|
3402
|
+
await this.client.hset(
|
|
3403
|
+
`${this.prefix}cron:${id}`,
|
|
3404
|
+
"status",
|
|
3405
|
+
"paused",
|
|
3406
|
+
"updatedAt",
|
|
3407
|
+
now.toString()
|
|
3408
|
+
);
|
|
3409
|
+
await this.client.srem(`${this.prefix}cron_status:active`, id.toString());
|
|
3410
|
+
await this.client.sadd(`${this.prefix}cron_status:paused`, id.toString());
|
|
3411
|
+
await this.client.zrem(`${this.prefix}cron_due`, id.toString());
|
|
3412
|
+
log(`Paused cron schedule ${id}`);
|
|
3413
|
+
}
|
|
3414
|
+
/** Resume a paused cron schedule. */
|
|
3415
|
+
async resumeCronSchedule(id) {
|
|
3416
|
+
const now = this.nowMs();
|
|
3417
|
+
await this.client.hset(
|
|
3418
|
+
`${this.prefix}cron:${id}`,
|
|
3419
|
+
"status",
|
|
3420
|
+
"active",
|
|
3421
|
+
"updatedAt",
|
|
3422
|
+
now.toString()
|
|
3423
|
+
);
|
|
3424
|
+
await this.client.srem(`${this.prefix}cron_status:paused`, id.toString());
|
|
3425
|
+
await this.client.sadd(`${this.prefix}cron_status:active`, id.toString());
|
|
3426
|
+
const nextRunAt = await this.client.hget(
|
|
3427
|
+
`${this.prefix}cron:${id}`,
|
|
3428
|
+
"nextRunAt"
|
|
3429
|
+
);
|
|
3430
|
+
if (nextRunAt && nextRunAt !== "null") {
|
|
3431
|
+
await this.client.zadd(
|
|
3432
|
+
`${this.prefix}cron_due`,
|
|
3433
|
+
Number(nextRunAt),
|
|
3434
|
+
id.toString()
|
|
3435
|
+
);
|
|
3436
|
+
}
|
|
3437
|
+
log(`Resumed cron schedule ${id}`);
|
|
3438
|
+
}
|
|
3439
|
+
/** Edit a cron schedule. */
|
|
3440
|
+
async editCronSchedule(id, updates, nextRunAt) {
|
|
3441
|
+
const now = this.nowMs();
|
|
3442
|
+
const fields = [];
|
|
3443
|
+
if (updates.cronExpression !== void 0) {
|
|
3444
|
+
fields.push("cronExpression", updates.cronExpression);
|
|
3445
|
+
}
|
|
3446
|
+
if (updates.payload !== void 0) {
|
|
3447
|
+
fields.push("payload", JSON.stringify(updates.payload));
|
|
3448
|
+
}
|
|
3449
|
+
if (updates.maxAttempts !== void 0) {
|
|
3450
|
+
fields.push("maxAttempts", updates.maxAttempts.toString());
|
|
3451
|
+
}
|
|
3452
|
+
if (updates.priority !== void 0) {
|
|
3453
|
+
fields.push("priority", updates.priority.toString());
|
|
3454
|
+
}
|
|
3455
|
+
if (updates.timeoutMs !== void 0) {
|
|
3456
|
+
fields.push(
|
|
3457
|
+
"timeoutMs",
|
|
3458
|
+
updates.timeoutMs !== null ? updates.timeoutMs.toString() : "null"
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
if (updates.forceKillOnTimeout !== void 0) {
|
|
3462
|
+
fields.push(
|
|
3463
|
+
"forceKillOnTimeout",
|
|
3464
|
+
updates.forceKillOnTimeout ? "true" : "false"
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
3467
|
+
if (updates.tags !== void 0) {
|
|
3468
|
+
fields.push(
|
|
3469
|
+
"tags",
|
|
3470
|
+
updates.tags !== null ? JSON.stringify(updates.tags) : "null"
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3473
|
+
if (updates.timezone !== void 0) {
|
|
3474
|
+
fields.push("timezone", updates.timezone);
|
|
3475
|
+
}
|
|
3476
|
+
if (updates.allowOverlap !== void 0) {
|
|
3477
|
+
fields.push("allowOverlap", updates.allowOverlap ? "true" : "false");
|
|
3478
|
+
}
|
|
3479
|
+
if (nextRunAt !== void 0) {
|
|
3480
|
+
const val = nextRunAt !== null ? nextRunAt.getTime().toString() : "null";
|
|
3481
|
+
fields.push("nextRunAt", val);
|
|
3482
|
+
if (nextRunAt !== null) {
|
|
3483
|
+
await this.client.zadd(
|
|
3484
|
+
`${this.prefix}cron_due`,
|
|
3485
|
+
nextRunAt.getTime(),
|
|
3486
|
+
id.toString()
|
|
3487
|
+
);
|
|
3488
|
+
} else {
|
|
3489
|
+
await this.client.zrem(`${this.prefix}cron_due`, id.toString());
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (fields.length === 0) {
|
|
3493
|
+
log(`No fields to update for cron schedule ${id}`);
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
fields.push("updatedAt", now.toString());
|
|
3497
|
+
await this.client.hmset(`${this.prefix}cron:${id}`, ...fields);
|
|
3498
|
+
log(`Edited cron schedule ${id}`);
|
|
3499
|
+
}
|
|
3500
|
+
/**
|
|
3501
|
+
* Fetch all active cron schedules whose nextRunAt <= now.
|
|
3502
|
+
* Uses a sorted set (cron_due) for efficient range query.
|
|
3503
|
+
*/
|
|
3504
|
+
async getDueCronSchedules() {
|
|
3505
|
+
const now = this.nowMs();
|
|
3506
|
+
const ids = await this.client.zrangebyscore(
|
|
3507
|
+
`${this.prefix}cron_due`,
|
|
3508
|
+
0,
|
|
3509
|
+
now
|
|
3510
|
+
);
|
|
3511
|
+
if (ids.length === 0) {
|
|
3512
|
+
log("Found 0 due cron schedules");
|
|
3513
|
+
return [];
|
|
3514
|
+
}
|
|
3515
|
+
const schedules = [];
|
|
3516
|
+
for (const id of ids) {
|
|
3517
|
+
const data = await this.client.hgetall(`${this.prefix}cron:${id}`);
|
|
3518
|
+
if (data && Object.keys(data).length > 0 && data.status === "active") {
|
|
3519
|
+
schedules.push(this.deserializeCronSchedule(data));
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
log(`Found ${schedules.length} due cron schedules`);
|
|
3523
|
+
return schedules;
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Update a cron schedule after a job has been enqueued.
|
|
3527
|
+
* Sets lastEnqueuedAt, lastJobId, and advances nextRunAt.
|
|
3528
|
+
*/
|
|
3529
|
+
async updateCronScheduleAfterEnqueue(id, lastEnqueuedAt, lastJobId, nextRunAt) {
|
|
3530
|
+
const fields = [
|
|
3531
|
+
"lastEnqueuedAt",
|
|
3532
|
+
lastEnqueuedAt.getTime().toString(),
|
|
3533
|
+
"lastJobId",
|
|
3534
|
+
lastJobId.toString(),
|
|
3535
|
+
"nextRunAt",
|
|
3536
|
+
nextRunAt ? nextRunAt.getTime().toString() : "null",
|
|
3537
|
+
"updatedAt",
|
|
3538
|
+
this.nowMs().toString()
|
|
3539
|
+
];
|
|
3540
|
+
await this.client.hmset(`${this.prefix}cron:${id}`, ...fields);
|
|
3541
|
+
if (nextRunAt) {
|
|
3542
|
+
await this.client.zadd(
|
|
3543
|
+
`${this.prefix}cron_due`,
|
|
3544
|
+
nextRunAt.getTime(),
|
|
3545
|
+
id.toString()
|
|
3546
|
+
);
|
|
3547
|
+
} else {
|
|
3548
|
+
await this.client.zrem(`${this.prefix}cron_due`, id.toString());
|
|
3549
|
+
}
|
|
3550
|
+
log(
|
|
3551
|
+
`Updated cron schedule ${id}: lastJobId=${lastJobId}, nextRunAt=${nextRunAt?.toISOString() ?? "null"}`
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
/** Deserialize a Redis hash into a CronScheduleRecord. */
|
|
3555
|
+
deserializeCronSchedule(h) {
|
|
3556
|
+
const nullish = (v) => v === void 0 || v === "null" || v === "" ? null : v;
|
|
3557
|
+
const numOrNull = (v) => {
|
|
3558
|
+
const n = nullish(v);
|
|
3559
|
+
return n === null ? null : Number(n);
|
|
3560
|
+
};
|
|
3561
|
+
const dateOrNull = (v) => {
|
|
3562
|
+
const n = numOrNull(v);
|
|
3563
|
+
return n === null ? null : new Date(n);
|
|
3564
|
+
};
|
|
3565
|
+
let payload;
|
|
3566
|
+
try {
|
|
3567
|
+
payload = JSON.parse(h.payload);
|
|
3568
|
+
} catch {
|
|
3569
|
+
payload = h.payload;
|
|
3570
|
+
}
|
|
3571
|
+
let tags;
|
|
3572
|
+
try {
|
|
3573
|
+
const raw = h.tags;
|
|
3574
|
+
if (raw && raw !== "null") {
|
|
3575
|
+
tags = JSON.parse(raw);
|
|
3576
|
+
}
|
|
3577
|
+
} catch {
|
|
3578
|
+
}
|
|
3579
|
+
return {
|
|
3580
|
+
id: Number(h.id),
|
|
3581
|
+
scheduleName: h.scheduleName,
|
|
3582
|
+
cronExpression: h.cronExpression,
|
|
3583
|
+
jobType: h.jobType,
|
|
3584
|
+
payload,
|
|
3585
|
+
maxAttempts: Number(h.maxAttempts),
|
|
3586
|
+
priority: Number(h.priority),
|
|
3587
|
+
timeoutMs: numOrNull(h.timeoutMs),
|
|
3588
|
+
forceKillOnTimeout: h.forceKillOnTimeout === "true",
|
|
3589
|
+
tags,
|
|
3590
|
+
timezone: h.timezone,
|
|
3591
|
+
allowOverlap: h.allowOverlap === "true",
|
|
3592
|
+
status: h.status,
|
|
3593
|
+
lastEnqueuedAt: dateOrNull(h.lastEnqueuedAt),
|
|
3594
|
+
lastJobId: numOrNull(h.lastJobId),
|
|
3595
|
+
nextRunAt: dateOrNull(h.nextRunAt),
|
|
3596
|
+
createdAt: new Date(Number(h.createdAt)),
|
|
3597
|
+
updatedAt: new Date(Number(h.updatedAt))
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
// ── Private helpers (filters) ─────────────────────────────────────────
|
|
2971
3601
|
async applyFilters(ids, filters) {
|
|
2972
3602
|
let result = ids;
|
|
2973
3603
|
if (filters.jobType) {
|
|
@@ -2997,6 +3627,19 @@ var RedisBackend = class {
|
|
|
2997
3627
|
return result;
|
|
2998
3628
|
}
|
|
2999
3629
|
};
|
|
3630
|
+
function getNextCronOccurrence(cronExpression, timezone = "UTC", after, CronImpl = croner.Cron) {
|
|
3631
|
+
const cron = new CronImpl(cronExpression, { timezone });
|
|
3632
|
+
const next = cron.nextRun(after ?? /* @__PURE__ */ new Date());
|
|
3633
|
+
return next ?? null;
|
|
3634
|
+
}
|
|
3635
|
+
function validateCronExpression(cronExpression, CronImpl = croner.Cron) {
|
|
3636
|
+
try {
|
|
3637
|
+
new CronImpl(cronExpression);
|
|
3638
|
+
return true;
|
|
3639
|
+
} catch {
|
|
3640
|
+
return false;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3000
3643
|
|
|
3001
3644
|
// src/handler-validation.ts
|
|
3002
3645
|
function validateHandlerSerializable2(handler, jobType) {
|
|
@@ -3091,6 +3734,49 @@ var initJobQueue = (config) => {
|
|
|
3091
3734
|
}
|
|
3092
3735
|
return pool;
|
|
3093
3736
|
};
|
|
3737
|
+
const enqueueDueCronJobsImpl = async () => {
|
|
3738
|
+
const dueSchedules = await backend.getDueCronSchedules();
|
|
3739
|
+
let count = 0;
|
|
3740
|
+
for (const schedule of dueSchedules) {
|
|
3741
|
+
if (!schedule.allowOverlap && schedule.lastJobId !== null) {
|
|
3742
|
+
const lastJob = await backend.getJob(schedule.lastJobId);
|
|
3743
|
+
if (lastJob && (lastJob.status === "pending" || lastJob.status === "processing" || lastJob.status === "waiting")) {
|
|
3744
|
+
const nextRunAt2 = getNextCronOccurrence(
|
|
3745
|
+
schedule.cronExpression,
|
|
3746
|
+
schedule.timezone
|
|
3747
|
+
);
|
|
3748
|
+
await backend.updateCronScheduleAfterEnqueue(
|
|
3749
|
+
schedule.id,
|
|
3750
|
+
/* @__PURE__ */ new Date(),
|
|
3751
|
+
schedule.lastJobId,
|
|
3752
|
+
nextRunAt2
|
|
3753
|
+
);
|
|
3754
|
+
continue;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
const jobId = await backend.addJob({
|
|
3758
|
+
jobType: schedule.jobType,
|
|
3759
|
+
payload: schedule.payload,
|
|
3760
|
+
maxAttempts: schedule.maxAttempts,
|
|
3761
|
+
priority: schedule.priority,
|
|
3762
|
+
timeoutMs: schedule.timeoutMs ?? void 0,
|
|
3763
|
+
forceKillOnTimeout: schedule.forceKillOnTimeout,
|
|
3764
|
+
tags: schedule.tags
|
|
3765
|
+
});
|
|
3766
|
+
const nextRunAt = getNextCronOccurrence(
|
|
3767
|
+
schedule.cronExpression,
|
|
3768
|
+
schedule.timezone
|
|
3769
|
+
);
|
|
3770
|
+
await backend.updateCronScheduleAfterEnqueue(
|
|
3771
|
+
schedule.id,
|
|
3772
|
+
/* @__PURE__ */ new Date(),
|
|
3773
|
+
jobId,
|
|
3774
|
+
nextRunAt
|
|
3775
|
+
);
|
|
3776
|
+
count++;
|
|
3777
|
+
}
|
|
3778
|
+
return count;
|
|
3779
|
+
};
|
|
3094
3780
|
return {
|
|
3095
3781
|
// Job queue operations
|
|
3096
3782
|
addJob: withLogContext(
|
|
@@ -3143,8 +3829,10 @@ var initJobQueue = (config) => {
|
|
|
3143
3829
|
(tags, mode = "all", limit, offset) => backend.getJobsByTags(tags, mode, limit, offset),
|
|
3144
3830
|
config.verbose ?? false
|
|
3145
3831
|
),
|
|
3146
|
-
// Job processing
|
|
3147
|
-
createProcessor: (handlers, options) => createProcessor(backend, handlers, options)
|
|
3832
|
+
// Job processing — automatically enqueues due cron jobs before each batch
|
|
3833
|
+
createProcessor: (handlers, options) => createProcessor(backend, handlers, options, async () => {
|
|
3834
|
+
await enqueueDueCronJobsImpl();
|
|
3835
|
+
}),
|
|
3148
3836
|
// Job events
|
|
3149
3837
|
getJobEvents: withLogContext(
|
|
3150
3838
|
(jobId) => backend.getJobEvents(jobId),
|
|
@@ -3167,6 +3855,82 @@ var initJobQueue = (config) => {
|
|
|
3167
3855
|
() => expireTimedOutWaitpoints(requirePool()),
|
|
3168
3856
|
config.verbose ?? false
|
|
3169
3857
|
),
|
|
3858
|
+
// Cron schedule operations
|
|
3859
|
+
addCronJob: withLogContext(
|
|
3860
|
+
(options) => {
|
|
3861
|
+
if (!validateCronExpression(options.cronExpression)) {
|
|
3862
|
+
return Promise.reject(
|
|
3863
|
+
new Error(`Invalid cron expression: "${options.cronExpression}"`)
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
const nextRunAt = getNextCronOccurrence(
|
|
3867
|
+
options.cronExpression,
|
|
3868
|
+
options.timezone ?? "UTC"
|
|
3869
|
+
);
|
|
3870
|
+
const input = {
|
|
3871
|
+
scheduleName: options.scheduleName,
|
|
3872
|
+
cronExpression: options.cronExpression,
|
|
3873
|
+
jobType: options.jobType,
|
|
3874
|
+
payload: options.payload,
|
|
3875
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
3876
|
+
priority: options.priority ?? 0,
|
|
3877
|
+
timeoutMs: options.timeoutMs ?? null,
|
|
3878
|
+
forceKillOnTimeout: options.forceKillOnTimeout ?? false,
|
|
3879
|
+
tags: options.tags,
|
|
3880
|
+
timezone: options.timezone ?? "UTC",
|
|
3881
|
+
allowOverlap: options.allowOverlap ?? false,
|
|
3882
|
+
nextRunAt
|
|
3883
|
+
};
|
|
3884
|
+
return backend.addCronSchedule(input);
|
|
3885
|
+
},
|
|
3886
|
+
config.verbose ?? false
|
|
3887
|
+
),
|
|
3888
|
+
getCronJob: withLogContext(
|
|
3889
|
+
(id) => backend.getCronSchedule(id),
|
|
3890
|
+
config.verbose ?? false
|
|
3891
|
+
),
|
|
3892
|
+
getCronJobByName: withLogContext(
|
|
3893
|
+
(name) => backend.getCronScheduleByName(name),
|
|
3894
|
+
config.verbose ?? false
|
|
3895
|
+
),
|
|
3896
|
+
listCronJobs: withLogContext(
|
|
3897
|
+
(status) => backend.listCronSchedules(status),
|
|
3898
|
+
config.verbose ?? false
|
|
3899
|
+
),
|
|
3900
|
+
removeCronJob: withLogContext(
|
|
3901
|
+
(id) => backend.removeCronSchedule(id),
|
|
3902
|
+
config.verbose ?? false
|
|
3903
|
+
),
|
|
3904
|
+
pauseCronJob: withLogContext(
|
|
3905
|
+
(id) => backend.pauseCronSchedule(id),
|
|
3906
|
+
config.verbose ?? false
|
|
3907
|
+
),
|
|
3908
|
+
resumeCronJob: withLogContext(
|
|
3909
|
+
(id) => backend.resumeCronSchedule(id),
|
|
3910
|
+
config.verbose ?? false
|
|
3911
|
+
),
|
|
3912
|
+
editCronJob: withLogContext(
|
|
3913
|
+
async (id, updates) => {
|
|
3914
|
+
if (updates.cronExpression !== void 0 && !validateCronExpression(updates.cronExpression)) {
|
|
3915
|
+
throw new Error(
|
|
3916
|
+
`Invalid cron expression: "${updates.cronExpression}"`
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
let nextRunAt;
|
|
3920
|
+
if (updates.cronExpression !== void 0 || updates.timezone !== void 0) {
|
|
3921
|
+
const existing = await backend.getCronSchedule(id);
|
|
3922
|
+
const expr = updates.cronExpression ?? existing?.cronExpression ?? "";
|
|
3923
|
+
const tz = updates.timezone ?? existing?.timezone ?? "UTC";
|
|
3924
|
+
nextRunAt = getNextCronOccurrence(expr, tz);
|
|
3925
|
+
}
|
|
3926
|
+
await backend.editCronSchedule(id, updates, nextRunAt);
|
|
3927
|
+
},
|
|
3928
|
+
config.verbose ?? false
|
|
3929
|
+
),
|
|
3930
|
+
enqueueDueCronJobs: withLogContext(
|
|
3931
|
+
() => enqueueDueCronJobsImpl(),
|
|
3932
|
+
config.verbose ?? false
|
|
3933
|
+
),
|
|
3170
3934
|
// Advanced access
|
|
3171
3935
|
getPool: () => {
|
|
3172
3936
|
if (backendType !== "postgres") {
|
|
@@ -3195,8 +3959,10 @@ exports.FailureReason = FailureReason;
|
|
|
3195
3959
|
exports.JobEventType = JobEventType;
|
|
3196
3960
|
exports.PostgresBackend = PostgresBackend;
|
|
3197
3961
|
exports.WaitSignal = WaitSignal;
|
|
3962
|
+
exports.getNextCronOccurrence = getNextCronOccurrence;
|
|
3198
3963
|
exports.initJobQueue = initJobQueue;
|
|
3199
3964
|
exports.testHandlerSerialization = testHandlerSerialization;
|
|
3965
|
+
exports.validateCronExpression = validateCronExpression;
|
|
3200
3966
|
exports.validateHandlerSerializable = validateHandlerSerializable2;
|
|
3201
3967
|
//# sourceMappingURL=index.cjs.map
|
|
3202
3968
|
//# sourceMappingURL=index.cjs.map
|