cronbake
Version:
A powerful and flexible cron job manager built with TypeScript
1,045 lines (1,038 loc) • 31 kB
JavaScript
// lib/parser.ts
var CronParser = class {
/**
* Creates a new instance of the `CronParser` class.
*/
constructor(cron) {
this.cron = cron;
}
/**
* A map of cron expression aliases to their corresponding cron expressions.
* * * * * * * (second minute hour day month day-of-week)
* | | | | | |
* | | | | | +-- day of the week (0 - 6) (Sunday to Saturday)
* | | | | +---- month (1 - 12)
* | | | +------ day of the month (1 - 31)
* | | +-------- hour (0 - 23)
* | +---------- minute (0 - 59)
* +------------ second (0 - 59)
*/
aliases = /* @__PURE__ */ new Map([
["@every_second", "* * * * * *"],
["@every_minute", "0 * * * * *"],
["@yearly", "0 0 0 1 1 *"],
// 00:00:00 at day 1 of month and Jan
["@annually", "0 0 0 1 1 *"],
// 00:00:00 at day 1 of month and Jan
["@monthly", "0 0 0 1 * *"],
// 00:00:00 at day 1 of month
["@weekly", "0 0 0 * * 0"],
// 00:00:00 at sun
["@daily", "0 0 0 * * *"],
// 00:00:00 every day
["@hourly", "0 0 * * * *"]
// 00:00 every hour
]);
/**
* Parses a string in the format "@every_<value>_<unit>" and returns the corresponding cron expression.
*/
parseEveryStr(str) {
const [, value, unit] = str.split("_");
switch (unit) {
case "seconds":
return `*/${value} * * * * *`;
case "minutes":
return `0 */${value} * * * *`;
case "hours":
return `0 0 */${value} * * *`;
case "dayOfMonth":
return `0 0 0 */${value} * *`;
case "months":
return `0 0 0 1 */${value} *`;
case "dayOfWeek":
return `0 0 0 * * */${value}`;
default:
return "* * * * * *";
}
}
/**
* Parses a string in the format "@at_<time>" and returns the corresponding cron expression.
*/
parseAtHourStr(str) {
const [, time] = str.split("_");
const [hour, minute] = time.split(":");
return `0 ${minute} ${hour} * * *`;
}
/**
* Parses a string in the format "@on_<day>" and returns the corresponding cron expression.
*/
parseOnDayStr(str) {
const [, day] = str.split("_");
const days = /* @__PURE__ */ new Map([
["sunday", 0],
["monday", 1],
["tuesday", 2],
["wednesday", 3],
["thursday", 4],
["friday", 5],
["saturday", 6]
]);
return `0 0 0 * * ${days.get(day)}`;
}
/**
* Parses a string in the format "@between_<start>_<end>" and returns the corresponding cron expression.
*/
parseBetweenStr(str) {
const [, start, end] = str.split("_");
return `0 0 ${start}-${end} * * *`;
}
/**
* Parses the input string and returns the corresponding cron expression.
*/
parseStr(str) {
if (this.aliases.has(str)) {
return this.aliases.get(str) || "";
}
if (str.includes("@every_")) {
return this.parseEveryStr(str);
}
if (str.includes("@at_")) {
return this.parseAtHourStr(str);
}
if (str.includes("@on_")) {
return this.parseOnDayStr(str);
}
if (str.includes("@between_")) {
return this.parseBetweenStr(
str
);
}
return str;
}
/**
* Parses the cron expression and returns a `CronTime` object representing the parsed cron expression.
* @returns A `CronTime` object representing the parsed cron expression.
*/
parse() {
this.cron = this.parseStr(this.cron);
const [second, minute, hour, dayOfMonth, month, dayOfWeek] = this.cron.split(" ");
return {
second: this.parseCronTime(second, 0, 59),
minute: this.parseCronTime(minute, 0, 59),
hour: this.parseCronTime(hour, 0, 23),
dayOfMonth: this.parseCronTime(dayOfMonth, 1, 31),
month: this.parseCronTime(month, 1, 12),
dayOfWeek: this.parseCronTime(dayOfWeek, 0, 6)
};
}
/**
* Gets the next execution time based on the current time.
* @returns A `Date` object representing the next execution time.
*/
getNext() {
const cronTime = this.parse();
const now = /* @__PURE__ */ new Date();
let next = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
now.getSeconds()
);
next.setMilliseconds(0);
next.setSeconds(next.getSeconds() + 1);
while (!this.checkCronTime(cronTime, next)) {
next.setSeconds(next.getSeconds() + 1);
}
return next;
}
/**
* Gets the previous execution time based on the current time.
* @returns A `Date` object representing the previous execution time.
*/
getPrevious() {
const cronTime = this.parse();
const now = /* @__PURE__ */ new Date();
let previous = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
now.getSeconds()
);
previous.setMilliseconds(0);
previous.setSeconds(previous.getSeconds() - 1);
while (!this.checkCronTime(cronTime, previous)) {
previous.setSeconds(previous.getSeconds() - 1);
}
return previous;
}
/**
* Parses a cron time string and returns an array of numbers representing the valid values for that field.
*/
parseCronTime(cronTime, min, max) {
const result = [];
switch (true) {
case cronTime === "*":
for (let i = min; i <= max; i++) {
result.push(i);
}
break;
case cronTime.includes("-"): {
const [start, end] = cronTime.split("-");
const step = this.getStep(cronTime);
for (let i = parseInt(start); i <= parseInt(end); i += step) {
result.push(i);
}
break;
}
case cronTime.includes("/"): {
const [value, step] = cronTime.split("/");
const start = value === "*" ? min : parseInt(value);
for (let i = start; i <= max; i += parseInt(step)) {
result.push(i);
}
break;
}
case cronTime.includes(","): {
const times = cronTime.split(",");
for (let i = 0; i < times.length; i++) {
result.push(parseInt(times[i]));
}
break;
}
default:
result.push(parseInt(cronTime));
break;
}
return result;
}
/**
* Gets the step value for a cron time string.
*/
getStep(cronTime) {
const [, step] = cronTime.split("/");
const parsedStep = parseInt(step);
return isNaN(parsedStep) ? 1 : parsedStep;
}
/**
* Checks if the given date matches the cron time.
*/
checkCronTime(cronTime, date) {
if (cronTime.second && !cronTime.second.includes(date.getSeconds())) {
return false;
}
if (cronTime.minute && !cronTime.minute.includes(date.getMinutes())) {
return false;
}
if (cronTime.hour && !cronTime.hour.includes(date.getHours())) {
return false;
}
if (cronTime.dayOfMonth && !cronTime.dayOfMonth.includes(date.getDate())) {
return false;
}
if (cronTime.month && !cronTime.month.includes(date.getMonth() + 1)) {
return false;
}
if (cronTime.dayOfWeek && !cronTime.dayOfWeek.includes(date.getDay())) {
return false;
}
return true;
}
};
var parser_default = CronParser;
// lib/utils.ts
var resolveIfPromise = async (value) => value instanceof Promise ? await value : value;
var CBResolver = async (callback, onError, logger = console) => {
try {
if (callback) {
await resolveIfPromise(callback());
}
} catch (error) {
if (onError) {
onError(error);
} else {
logger.warn("Callback execution failed:", error);
}
}
};
// lib/cron.ts
var Cron = class _Cron {
name;
cron;
callback;
onTick;
onComplete;
onError;
priority;
interval = null;
timeout = null;
next = null;
status = "stopped";
parser;
history = [];
metrics = {
totalExecutions: 0,
successfulExecutions: 0,
failedExecutions: 0,
averageExecutionTime: 0
};
maxHistory;
useCalculatedTimeouts;
pollingInterval;
overrunProtection;
isExecuting = false;
immediate;
initialDelayMs;
lastRunTime = null;
logger;
/**
* Creates a new instance of the `Cron` class.
*/
constructor(options, config) {
if (!options.name || typeof options.name !== "string") {
throw new Error("Cron job name is required and must be a string");
}
this.logger = options.logger ?? console;
this.name = options.name;
this.cron = options.cron;
this.callback = options.callback;
this.onTick = CBResolver.bind(this, options.onTick, void 0, this.logger);
this.onComplete = CBResolver.bind(this, options.onComplete, void 0, this.logger);
this.onError = options.onError;
this.priority = options.priority ?? 0;
this.maxHistory = options.maxHistory ?? 100;
this.useCalculatedTimeouts = config?.useCalculatedTimeouts ?? true;
this.pollingInterval = config?.pollingInterval ?? 1e3;
this.overrunProtection = options.overrunProtection ?? true;
this.immediate = options.immediate ?? false;
this.initialDelayMs = this.parseDelay(options.delay);
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.pause = this.pause.bind(this);
this.resume = this.resume.bind(this);
this.destroy = this.destroy.bind(this);
this.getStatus = this.getStatus.bind(this);
this.isRunning = this.isRunning.bind(this);
this.lastExecution = this.lastExecution.bind(this);
this.nextExecution = this.nextExecution.bind(this);
this.remaining = this.remaining.bind(this);
this.time = this.time.bind(this);
this.getHistory = this.getHistory.bind(this);
this.getMetrics = this.getMetrics.bind(this);
this.resetMetrics = this.resetMetrics.bind(this);
this.parser = new parser_default(this.cron);
this.validateCronExpression();
if (options.start) {
this.start();
}
}
/**
* Validates the cron expression and custom preset bounds
*/
validateCronExpression() {
if (typeof this.cron === "string") {
if (this.cron.includes("@every_")) {
const parts = this.cron.split("_");
if (parts.length >= 3) {
const value = parseInt(parts[1]);
if (isNaN(value) || value <= 0) {
throw new Error(`Invalid value in custom preset: ${this.cron}`);
}
}
}
if (this.cron.includes("@at_")) {
const timeMatch = this.cron.match(/:(\d+)/);
if (timeMatch) {
const hour = parseInt(timeMatch[1]);
const minute = parseInt(timeMatch[2]);
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
throw new Error(`Invalid time in custom preset: ${this.cron}`);
}
}
}
if (this.cron.includes("@between_")) {
const betweenMatch = this.cron.match(/_(\d+)/);
if (betweenMatch) {
const start = parseInt(betweenMatch[1]);
const end = parseInt(betweenMatch[2]);
if (start < 0 || start > 23 || end < 0 || end > 23 || start >= end) {
throw new Error(
`Invalid hour range in custom preset: ${this.cron}`
);
}
}
}
}
try {
this.parser.parse();
} catch (error) {
throw new Error(`Invalid cron expression: ${this.cron}`);
}
}
start() {
if (this.status === "running") {
return;
}
this.status = "running";
if (this.immediate) {
this.scheduleImmediateFirstRun();
return;
}
this.next = this.parser.getNext();
if (this.useCalculatedTimeouts) {
this.scheduleWithTimeout();
} else {
this.scheduleWithInterval();
}
}
/**
* Schedules execution using calculated timeouts for better efficiency
*/
scheduleWithTimeout() {
if (this.status !== "running" || !this.next) return;
const delay = Math.max(0, this.next.getTime() - Date.now());
const MAX_TIMEOUT_VALUE = 2147483647;
if (delay > MAX_TIMEOUT_VALUE) {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.scheduleWithInterval();
return;
}
this.timeout = setTimeout(async () => {
if (this.status === "running") {
await this.executeJob();
this.next = this.parser.getNext();
this.scheduleWithTimeout();
}
}, delay);
}
/**
* Schedule a one-off immediate or delayed first run, then fall back to normal scheduling
*/
scheduleImmediateFirstRun() {
if (this.status !== "running") return;
const targetDelay = Math.max(0, this.initialDelayMs ?? 0);
const MAX_TIMEOUT_VALUE = 2147483647;
if (targetDelay > MAX_TIMEOUT_VALUE) {
const targetTime = Date.now() + targetDelay;
this.interval = setInterval(async () => {
if (this.status !== "running") return;
if (Date.now() >= targetTime) {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
await this.executeJob();
this.next = this.parser.getNext();
if (this.useCalculatedTimeouts) this.scheduleWithTimeout();
else this.scheduleWithInterval();
}
}, Math.min(this.pollingInterval, 1e3));
return;
}
this.timeout = setTimeout(async () => {
if (this.status !== "running") return;
await this.executeJob();
this.next = this.parser.getNext();
if (this.useCalculatedTimeouts) this.scheduleWithTimeout();
else this.scheduleWithInterval();
}, targetDelay);
}
/**
* Schedules execution using traditional polling interval
*/
scheduleWithInterval() {
this.interval = setInterval(async () => {
if (this.overrunProtection && this.isExecuting) {
this.metrics.skippedExecutions = (this.metrics.skippedExecutions ?? 0) + 1;
return;
}
if (this.next && this.next.getTime() <= Date.now() && this.status === "running") {
await this.executeJob();
this.next = this.parser.getNext();
}
}, this.pollingInterval);
}
/**
* Executes the job and handles metrics/history tracking
*/
async executeJob() {
const startTime = Date.now();
let success = true;
let error;
this.isExecuting = true;
try {
if (this.callback) {
await resolveIfPromise(this.callback());
}
await this.onTick();
} catch (err) {
success = false;
error = err instanceof Error ? err.message : String(err);
if (this.onError) {
try {
this.onError(err instanceof Error ? err : new Error(String(err)));
} catch (handlerError) {
this.logger.warn("Error handler failed:", handlerError);
}
} else {
this.logger.warn(`Cron job '${this.name}' failed:`, err);
}
this.metrics.lastError = error;
} finally {
this.isExecuting = false;
}
const duration = Date.now() - startTime;
this.metrics.totalExecutions++;
if (success) {
this.metrics.successfulExecutions++;
} else {
this.metrics.failedExecutions++;
}
this.metrics.lastExecutionTime = duration;
this.metrics.averageExecutionTime = (this.metrics.averageExecutionTime * (this.metrics.totalExecutions - 1) + duration) / this.metrics.totalExecutions;
const historyEntry = {
timestamp: new Date(startTime),
duration,
success,
error
};
this.history.unshift(historyEntry);
if (this.history.length > this.maxHistory) {
this.history = this.history.slice(0, this.maxHistory);
}
this.lastRunTime = historyEntry.timestamp;
}
stop() {
if (this.status === "stopped") {
return;
}
this.status = "stopped";
this.clearSchedulers();
}
pause() {
if (this.status !== "running") {
return;
}
this.status = "paused";
this.clearSchedulers();
}
resume() {
if (this.status !== "paused") {
return;
}
this.status = "running";
this.next = this.parser.getNext();
if (this.useCalculatedTimeouts) {
this.scheduleWithTimeout();
} else {
this.scheduleWithInterval();
}
}
/**
* Clears all active schedulers
*/
clearSchedulers() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
destroy() {
this.clearSchedulers();
this.status = "stopped";
this.onComplete();
}
getStatus() {
return this.status;
}
isRunning() {
return this.status === "running";
}
lastExecution() {
return this.lastRunTime ? new Date(this.lastRunTime) : this.parser.getPrevious();
}
nextExecution() {
return this.next || /* @__PURE__ */ new Date();
}
remaining() {
return this.next ? this.next.getTime() - Date.now() : 0;
}
time() {
return this.remaining();
}
getHistory() {
return [...this.history];
}
getMetrics() {
return { ...this.metrics };
}
resetMetrics() {
this.history = [];
this.metrics = {
totalExecutions: 0,
successfulExecutions: 0,
failedExecutions: 0,
averageExecutionTime: 0
};
}
hydratePersistence(state) {
if (state.history) {
this.history = state.history.slice(0, this.maxHistory);
}
if (state.metrics) {
this.metrics = { ...state.metrics };
}
}
/** Parse a human-friendly delay string or number into milliseconds */
parseDelay(delay) {
if (delay === void 0 || delay === null) return void 0;
if (typeof delay === "number") return delay >= 0 ? delay : 0;
const str = String(delay).trim().toLowerCase();
const match = str.match(/^(\d+)\s*(ms|s|m|h|d)?$/);
if (!match) return void 0;
const value = parseInt(match[1], 10);
const unit = match[2] ?? "ms";
const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
return value * factor;
}
/**
* Creates a new cron job with the specified options.
* @returns A new `ICron` object representing the cron job.
*/
static create(options, config) {
return new _Cron(options, config);
}
/**
* Parses the specified cron expression and returns a `CronTime` object.
* @returns A `CronTime` object representing the parsed cron expression.
*/
static parse(cron) {
return new parser_default(cron).parse();
}
/**
* Gets the next execution time for the specified cron expression.
* @template T The type of the cron expression.
* @returns A `Date` object representing the next execution time.
*/
static getNext(cron) {
return new parser_default(cron).getNext();
}
/**
* Gets the previous execution time for the specified cron expression.
* @returns A `Date` object representing the previous execution time.
*/
static getPrevious(cron) {
return new parser_default(cron).getPrevious();
}
/**
* Checks if the specified string is a valid cron expression.
* @returns `true` if the string is a valid cron expression, `false` otherwise.
*/
static isValid(cron) {
try {
new parser_default(cron).parse();
return true;
} catch (e) {
return false;
}
}
};
var cron_default = Cron;
// lib/persistence/file.ts
import * as fs from "fs";
import * as path from "path";
var FilePersistenceProvider = class {
constructor(filePath) {
this.filePath = filePath;
}
queue = Promise.resolve();
async save(state) {
const filePath = path.resolve(this.filePath);
const dir = path.dirname(filePath);
const tmp = `${filePath}.tmp`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const write = async () => {
const data = JSON.stringify(state, null, 2);
await fs.promises.writeFile(tmp, data, "utf8");
await fs.promises.rename(tmp, filePath);
};
this.queue = this.queue.then(write, write);
return this.queue;
}
async load() {
const filePath = path.resolve(this.filePath);
if (!fs.existsSync(filePath)) return null;
try {
const data = await fs.promises.readFile(filePath, "utf8");
const state = JSON.parse(data);
if (!state || typeof state !== "object") return null;
return state;
} catch {
return null;
}
}
};
// lib/baker.ts
var Baker = class _Baker {
crons = /* @__PURE__ */ new Map();
restoredJobs = /* @__PURE__ */ new Set();
jobPersistence = /* @__PURE__ */ new Map();
isRestoring = false;
initialRestorePromise = Promise.resolve();
config;
persistence;
persistenceProvider;
enableMetrics;
onError;
logger;
constructor(options = {}) {
this.logger = options.logger ?? console;
this.config = {
pollingInterval: options.schedulerConfig?.pollingInterval ?? 1e3,
useCalculatedTimeouts: options.schedulerConfig?.useCalculatedTimeouts ?? true,
maxHistoryEntries: options.schedulerConfig?.maxHistoryEntries ?? 100
};
this.persistence = {
enabled: options.persistence?.enabled ?? false,
filePath: options.persistence?.filePath ?? "./cronbake-state.json",
autoRestore: options.persistence?.autoRestore ?? true,
strategy: options.persistence?.strategy ?? "file",
provider: options.persistence?.provider,
redis: options.persistence?.redis
};
if (this.persistence.enabled) {
if (this.persistence.provider) {
this.persistenceProvider = this.persistence.provider;
} else if (this.persistence.strategy === "file") {
this.persistenceProvider = new FilePersistenceProvider(
this.persistence.filePath
);
} else if (this.persistence.strategy === "redis") {
throw new Error(
"Redis persistence selected but no provider supplied. Pass persistence.provider or use FilePersistenceProvider."
);
}
}
this.enableMetrics = options.enableMetrics ?? true;
this.onError = options.onError;
if (this.persistence.enabled && this.persistence.autoRestore) {
const restorePromise = this.restoreState();
restorePromise.catch((err) => {
this.logger.warn("Failed to restore state:", err);
});
this.initialRestorePromise = restorePromise;
}
if (options.autoStart) {
this.initialRestorePromise.finally(() => this.bakeAll());
}
}
add(options) {
const existingCron = this.crons.get(options.name);
if (existingCron) {
if (this.restoredJobs.has(options.name)) {
existingCron.destroy();
this.crons.delete(options.name);
this.restoredJobs.delete(options.name);
this.jobPersistence.delete(options.name);
} else {
throw new Error(`Cron job with name '${options.name}' already exists`);
}
}
this.jobPersistence.delete(options.name);
const cronConfig = {
useCalculatedTimeouts: this.config.useCalculatedTimeouts,
pollingInterval: this.config.pollingInterval
};
const jobLogger = options.logger ?? this.logger;
const enhancedOptions = {
...options,
maxHistory: options.maxHistory ?? this.config.maxHistoryEntries,
logger: jobLogger,
onError: options.onError ?? ((error) => {
if (this.onError) {
this.onError(error, options.name);
} else {
jobLogger.warn(`Cron job '${options.name}' failed:`, error);
}
})
};
const cron = cron_default.create(enhancedOptions, cronConfig);
const shouldPersist = options.persist ?? true;
this.jobPersistence.set(cron.name, shouldPersist);
this.crons.set(cron.name, cron);
if (this.persistence.enabled && !this.isRestoring) {
this.saveState().catch((err) => {
this.logger.warn("Failed to save state:", err);
});
}
return cron;
}
remove(name) {
const cron = this.crons.get(name);
if (cron) {
cron.destroy();
this.crons.delete(name);
this.restoredJobs.delete(name);
this.jobPersistence.delete(name);
if (this.persistence.enabled) {
this.saveState().catch((err) => {
this.logger.warn("Failed to save state:", err);
});
}
}
}
bake(name) {
const cron = this.crons.get(name);
if (cron) {
cron.start();
}
}
stop(name) {
const cron = this.crons.get(name);
if (cron) {
cron.stop();
}
}
pause(name) {
const cron = this.crons.get(name);
if (cron) {
cron.pause();
}
}
resume(name) {
const cron = this.crons.get(name);
if (cron) {
cron.resume();
}
}
destroy(name) {
const cron = this.crons.get(name);
if (cron) {
cron.destroy();
this.crons.delete(name);
this.restoredJobs.delete(name);
this.jobPersistence.delete(name);
if (this.persistence.enabled) {
this.saveState().catch((err) => {
this.logger.warn("Failed to save state:", err);
});
}
}
}
getStatus(name) {
const cron = this.crons.get(name);
return cron ? cron.getStatus() : "stopped";
}
isRunning(name) {
const cron = this.crons.get(name);
return cron ? cron.isRunning() : false;
}
lastExecution(name) {
const cron = this.crons.get(name);
return cron ? cron.lastExecution() : /* @__PURE__ */ new Date();
}
nextExecution(name) {
const cron = this.crons.get(name);
return cron ? cron.nextExecution() : /* @__PURE__ */ new Date();
}
remaining(name) {
const cron = this.crons.get(name);
return cron ? cron.remaining() : 0;
}
time(name) {
const cron = this.crons.get(name);
return cron ? cron.time() : 0;
}
getHistory(name) {
const cron = this.crons.get(name);
return cron && this.enableMetrics ? cron.getHistory() : [];
}
getMetrics(name) {
const cron = this.crons.get(name);
return cron && this.enableMetrics ? cron.getMetrics() : {
totalExecutions: 0,
successfulExecutions: 0,
failedExecutions: 0,
averageExecutionTime: 0
};
}
getJobNames() {
return Array.from(this.crons.keys());
}
getAllJobs() {
return new Map(this.crons);
}
bakeAll() {
this.crons.forEach((cron) => cron.start());
}
stopAll() {
this.crons.forEach((cron) => cron.stop());
}
pauseAll() {
this.crons.forEach((cron) => cron.pause());
}
resumeAll() {
this.crons.forEach((cron) => cron.resume());
}
destroyAll() {
this.crons.forEach((cron) => cron.destroy());
this.crons.clear();
this.restoredJobs.clear();
this.jobPersistence.clear();
if (this.persistence.enabled) {
this.saveState().catch((err) => {
this.logger.warn("Failed to save state:", err);
});
}
}
resetAllMetrics() {
if (this.enableMetrics) {
this.crons.forEach((cron) => cron.resetMetrics());
}
}
async saveState() {
if (!this.persistence.enabled || !this.persistenceProvider) return;
try {
const jobs = Array.from(this.crons.entries()).filter(([name]) => this.jobPersistence.get(name) !== false).map(([name, cron]) => ({
name,
cron: String(cron.cron),
status: cron.getStatus(),
priority: cron.priority,
persist: this.jobPersistence.get(name) !== false,
metrics: this.enableMetrics ? cron.getMetrics() : void 0,
history: this.enableMetrics ? cron.getHistory().map((entry) => ({
...entry,
timestamp: entry.timestamp.toISOString()
})) : void 0
}));
const state = {
version: 1,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
jobs,
config: this.config
};
await this.persistenceProvider.save(state);
} catch (error) {
throw new Error(`Failed to save state: ${error}`);
}
}
async restoreState() {
if (!this.persistence.enabled || !this.persistenceProvider) return;
if (this.isRestoring) return;
this.isRestoring = true;
try {
const state = await this.persistenceProvider.load();
if (!state) return;
if (!state.jobs || !Array.isArray(state.jobs)) {
throw new Error("Invalid state format");
}
let restoredCount = 0;
for (const jobData of state.jobs) {
if (!jobData.name || !jobData.cron) {
this.logger.warn("Skipping invalid job data:", jobData);
continue;
}
if (jobData.persist === false) {
continue;
}
if (this.crons.has(jobData.name)) {
continue;
}
try {
const options = {
name: jobData.name,
cron: jobData.cron,
callback: () => {
this.logger.warn(
`Restored job '${jobData.name}' executed but no callback was provided`
);
},
priority: jobData.priority,
start: jobData.status === "running",
persist: true
};
this.add(options);
const cron = this.crons.get(jobData.name);
if (cron && typeof cron.hydratePersistence === "function" && this.enableMetrics) {
const history = jobData.history?.map((entry) => ({
...entry,
timestamp: new Date(entry.timestamp)
}));
cron.hydratePersistence({
history,
metrics: jobData.metrics
});
}
this.restoredJobs.add(jobData.name);
restoredCount++;
} catch (error) {
this.logger.warn(`Failed to restore job '${jobData.name}':`, error);
}
}
if (restoredCount > 0) {
this.logger.info(`Restored ${restoredCount} cron jobs from persistence`);
}
} catch (error) {
throw new Error(`Failed to restore state: ${error}`);
} finally {
this.isRestoring = false;
}
}
async ready() {
await this.initialRestorePromise;
}
/**
* Creates a new instance of `Baker`.
*/
static create(options = {}) {
return new _Baker(options);
}
};
var baker_default = Baker;
// lib/persistence/redis.ts
var RedisPersistenceProvider = class {
constructor(options) {
this.options = options;
if (!options?.client) {
throw new Error("RedisPersistenceProvider requires a redis-like client with get/set");
}
this.key = options.key ?? "cronbake:state";
}
key;
async save(state) {
await this.options.client.set(this.key, JSON.stringify(state));
}
async load() {
const raw = await this.options.client.get(this.key);
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return null;
return parsed;
} catch {
return null;
}
}
};
// lib/index.ts
var index_default = baker_default;
export {
baker_default as Baker,
cron_default as Cron,
parser_default as CronParser,
FilePersistenceProvider,
RedisPersistenceProvider,
index_default as default
};