UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

702 lines (701 loc) 26.3 kB
import { $atom, $hook, $inject, $module, $pipeline, $state, Alepha, KIND, Primitive, createPrimitive, z } from "alepha"; import { $lock, AlephaLock, LockAcquireError } from "alepha/lock"; import { DateTimeProvider } from "alepha/datetime"; import { $logger } from "alepha/logger"; //#region ../../../../node_modules/cron-schedule/dist/utils.js function extractDateElements(date) { return { second: date.getSeconds(), minute: date.getMinutes(), hour: date.getHours(), day: date.getDate(), month: date.getMonth(), weekday: date.getDay(), year: date.getFullYear() }; } function getDaysInMonth(year, month) { return new Date(year, month + 1, 0).getDate(); } function getDaysBetweenWeekdays(weekday1, weekday2) { if (weekday1 <= weekday2) return weekday2 - weekday1; return 6 - weekday1 + weekday2 + 1; } //#endregion //#region ../../../../node_modules/cron-schedule/dist/cron.js var Cron = class { constructor({ seconds, minutes, hours, days, months, weekdays }) { if (!seconds || seconds.size === 0) throw new Error("There must be at least one allowed second."); if (!minutes || minutes.size === 0) throw new Error("There must be at least one allowed minute."); if (!hours || hours.size === 0) throw new Error("There must be at least one allowed hour."); if (!months || months.size === 0) throw new Error("There must be at least one allowed month."); if ((!weekdays || weekdays.size === 0) && (!days || days.size === 0)) throw new Error("There must be at least one allowed day or weekday."); this.seconds = Array.from(seconds).sort((a, b) => a - b); this.minutes = Array.from(minutes).sort((a, b) => a - b); this.hours = Array.from(hours).sort((a, b) => a - b); this.days = Array.from(days).sort((a, b) => a - b); this.months = Array.from(months).sort((a, b) => a - b); this.weekdays = Array.from(weekdays).sort((a, b) => a - b); const validateData = (name, data, constraint) => { if (data.some((x) => typeof x !== "number" || x % 1 !== 0 || x < constraint.min || x > constraint.max)) throw new Error(`${name} must only consist of integers which are within the range of ${constraint.min} and ${constraint.max}`); }; validateData("seconds", this.seconds, { min: 0, max: 59 }); validateData("minutes", this.minutes, { min: 0, max: 59 }); validateData("hours", this.hours, { min: 0, max: 23 }); validateData("days", this.days, { min: 1, max: 31 }); validateData("months", this.months, { min: 0, max: 11 }); validateData("weekdays", this.weekdays, { min: 0, max: 6 }); this.reversed = { seconds: this.seconds.map((x) => x).reverse(), minutes: this.minutes.map((x) => x).reverse(), hours: this.hours.map((x) => x).reverse(), days: this.days.map((x) => x).reverse(), months: this.months.map((x) => x).reverse(), weekdays: this.weekdays.map((x) => x).reverse() }; } /** * Find the next or previous hour, starting from the given start hour that matches the hour constraint. * startHour itself might also be allowed. */ findAllowedHour(dir, startHour) { return dir === "next" ? this.hours.find((x) => x >= startHour) : this.reversed.hours.find((x) => x <= startHour); } /** * Find the next or previous minute, starting from the given start minute that matches the minute constraint. * startMinute itself might also be allowed. */ findAllowedMinute(dir, startMinute) { return dir === "next" ? this.minutes.find((x) => x >= startMinute) : this.reversed.minutes.find((x) => x <= startMinute); } /** * Find the next or previous second, starting from the given start second that matches the second constraint. * startSecond itself IS NOT allowed. */ findAllowedSecond(dir, startSecond) { return dir === "next" ? this.seconds.find((x) => x > startSecond) : this.reversed.seconds.find((x) => x < startSecond); } /** * Find the next or previous time, starting from the given start time that matches the hour, minute * and second constraints. startTime itself might also be allowed. */ findAllowedTime(dir, startTime) { let hour = this.findAllowedHour(dir, startTime.hour); if (hour !== void 0) if (hour === startTime.hour) { let minute = this.findAllowedMinute(dir, startTime.minute); if (minute !== void 0) if (minute === startTime.minute) { const second = this.findAllowedSecond(dir, startTime.second); if (second !== void 0) return { hour, minute, second }; minute = this.findAllowedMinute(dir, dir === "next" ? startTime.minute + 1 : startTime.minute - 1); if (minute !== void 0) return { hour, minute, second: dir === "next" ? this.seconds[0] : this.reversed.seconds[0] }; } else return { hour, minute, second: dir === "next" ? this.seconds[0] : this.reversed.seconds[0] }; hour = this.findAllowedHour(dir, dir === "next" ? startTime.hour + 1 : startTime.hour - 1); if (hour !== void 0) return { hour, minute: dir === "next" ? this.minutes[0] : this.reversed.minutes[0], second: dir === "next" ? this.seconds[0] : this.reversed.seconds[0] }; } else return { hour, minute: dir === "next" ? this.minutes[0] : this.reversed.minutes[0], second: dir === "next" ? this.seconds[0] : this.reversed.seconds[0] }; } /** * Find the next or previous day in the given month, starting from the given startDay * that matches either the day or the weekday constraint. startDay itself might also be allowed. */ findAllowedDayInMonth(dir, year, month, startDay) { var _a, _b; if (startDay < 1) throw new Error("startDay must not be smaller than 1."); const daysInMonth = getDaysInMonth(year, month); const daysRestricted = this.days.length !== 31; const weekdaysRestricted = this.weekdays.length !== 7; if (!daysRestricted && !weekdaysRestricted) { if (startDay > daysInMonth) return dir === "next" ? void 0 : daysInMonth; return startDay; } let allowedDayByDays; if (daysRestricted) { allowedDayByDays = dir === "next" ? this.days.find((x) => x >= startDay) : this.reversed.days.find((x) => x <= startDay); if (allowedDayByDays !== void 0 && allowedDayByDays > daysInMonth) allowedDayByDays = void 0; } let allowedDayByWeekdays; if (weekdaysRestricted) { const startWeekday = new Date(year, month, startDay).getDay(); const nearestAllowedWeekday = dir === "next" ? (_a = this.weekdays.find((x) => x >= startWeekday)) !== null && _a !== void 0 ? _a : this.weekdays[0] : (_b = this.reversed.weekdays.find((x) => x <= startWeekday)) !== null && _b !== void 0 ? _b : this.reversed.weekdays[0]; if (nearestAllowedWeekday !== void 0) { const daysBetweenWeekdays = dir === "next" ? getDaysBetweenWeekdays(startWeekday, nearestAllowedWeekday) : getDaysBetweenWeekdays(nearestAllowedWeekday, startWeekday); allowedDayByWeekdays = dir === "next" ? startDay + daysBetweenWeekdays : startDay - daysBetweenWeekdays; if (allowedDayByWeekdays > daysInMonth || allowedDayByWeekdays < 1) allowedDayByWeekdays = void 0; } } if (allowedDayByDays !== void 0 && allowedDayByWeekdays !== void 0) return dir === "next" ? Math.min(allowedDayByDays, allowedDayByWeekdays) : Math.max(allowedDayByDays, allowedDayByWeekdays); if (allowedDayByDays !== void 0) return allowedDayByDays; if (allowedDayByWeekdays !== void 0) return allowedDayByWeekdays; } /** Gets the next date starting from the given start date or now. */ getNextDate(startDate = /* @__PURE__ */ new Date()) { const startDateElements = extractDateElements(startDate); let minYear = startDateElements.year; let startIndexMonth = this.months.findIndex((x) => x >= startDateElements.month); if (startIndexMonth === -1) { startIndexMonth = 0; minYear++; } const maxIterations = this.months.length * 5; for (let i = 0; i < maxIterations; i++) { const year = minYear + Math.floor((startIndexMonth + i) / this.months.length); const month = this.months[(startIndexMonth + i) % this.months.length]; const isStartMonth = year === startDateElements.year && month === startDateElements.month; let day = this.findAllowedDayInMonth("next", year, month, isStartMonth ? startDateElements.day : 1); let isStartDay = isStartMonth && day === startDateElements.day; if (day !== void 0 && isStartDay) { const nextTime = this.findAllowedTime("next", startDateElements); if (nextTime !== void 0) return new Date(year, month, day, nextTime.hour, nextTime.minute, nextTime.second); day = this.findAllowedDayInMonth("next", year, month, day + 1); isStartDay = false; } if (day !== void 0 && !isStartDay) return new Date(year, month, day, this.hours[0], this.minutes[0], this.seconds[0]); } throw new Error("No valid next date was found."); } /** Gets the specified amount of future dates starting from the given start date or now. */ getNextDates(amount, startDate) { const dates = []; let nextDate; for (let i = 0; i < amount; i++) { nextDate = this.getNextDate(nextDate !== null && nextDate !== void 0 ? nextDate : startDate); dates.push(nextDate); } return dates; } /** * Get an ES6 compatible iterator which iterates over the next dates starting from startDate or now. * The iterator runs until the optional endDate is reached or forever. */ *getNextDatesIterator(startDate, endDate) { let nextDate; while (true) { nextDate = this.getNextDate(nextDate !== null && nextDate !== void 0 ? nextDate : startDate); if (endDate && endDate.getTime() < nextDate.getTime()) return; yield nextDate; } } /** Gets the previous date starting from the given start date or now. */ getPrevDate(startDate = /* @__PURE__ */ new Date()) { const startDateElements = extractDateElements(startDate); let maxYear = startDateElements.year; let startIndexMonth = this.reversed.months.findIndex((x) => x <= startDateElements.month); if (startIndexMonth === -1) { startIndexMonth = 0; maxYear--; } const maxIterations = this.reversed.months.length * 5; for (let i = 0; i < maxIterations; i++) { const year = maxYear - Math.floor((startIndexMonth + i) / this.reversed.months.length); const month = this.reversed.months[(startIndexMonth + i) % this.reversed.months.length]; const isStartMonth = year === startDateElements.year && month === startDateElements.month; let day = this.findAllowedDayInMonth("prev", year, month, isStartMonth ? startDateElements.day : getDaysInMonth(year, month)); let isStartDay = isStartMonth && day === startDateElements.day; if (day !== void 0 && isStartDay) { const prevTime = this.findAllowedTime("prev", startDateElements); if (prevTime !== void 0) return new Date(year, month, day, prevTime.hour, prevTime.minute, prevTime.second); if (day > 1) { day = this.findAllowedDayInMonth("prev", year, month, day - 1); isStartDay = false; } } if (day !== void 0 && !isStartDay) return new Date(year, month, day, this.reversed.hours[0], this.reversed.minutes[0], this.reversed.seconds[0]); } throw new Error("No valid previous date was found."); } /** Gets the specified amount of previous dates starting from the given start date or now. */ getPrevDates(amount, startDate) { const dates = []; let prevDate; for (let i = 0; i < amount; i++) { prevDate = this.getPrevDate(prevDate !== null && prevDate !== void 0 ? prevDate : startDate); dates.push(prevDate); } return dates; } /** * Get an ES6 compatible iterator which iterates over the previous dates starting from startDate or now. * The iterator runs until the optional endDate is reached or forever. */ *getPrevDatesIterator(startDate, endDate) { let prevDate; while (true) { prevDate = this.getPrevDate(prevDate !== null && prevDate !== void 0 ? prevDate : startDate); if (endDate && endDate.getTime() > prevDate.getTime()) return; yield prevDate; } } /** Returns true when there is a cron date at the given date. */ matchDate(date) { const { second, minute, hour, day, month, weekday } = extractDateElements(date); if (this.seconds.indexOf(second) === -1 || this.minutes.indexOf(minute) === -1 || this.hours.indexOf(hour) === -1 || this.months.indexOf(month) === -1) return false; if (this.days.length !== 31 && this.weekdays.length !== 7) return this.days.indexOf(day) !== -1 || this.weekdays.indexOf(weekday) !== -1; return this.days.indexOf(day) !== -1 && this.weekdays.indexOf(weekday) !== -1; } }; //#endregion //#region ../../../../node_modules/cron-schedule/dist/cron-parser.js const secondConstraint = { min: 0, max: 59 }; const minuteConstraint = { min: 0, max: 59 }; const hourConstraint = { min: 0, max: 23 }; const dayConstraint = { min: 1, max: 31 }; const monthConstraint = { min: 1, max: 12, aliases: { jan: "1", feb: "2", mar: "3", apr: "4", may: "5", jun: "6", jul: "7", aug: "8", sep: "9", oct: "10", nov: "11", dec: "12" } }; const weekdayConstraint = { min: 0, max: 7, aliases: { mon: "1", tue: "2", wed: "3", thu: "4", fri: "5", sat: "6", sun: "7" } }; const timeNicknames = { "@yearly": "0 0 1 1 *", "@annually": "0 0 1 1 *", "@monthly": "0 0 1 * *", "@weekly": "0 0 * * 0", "@daily": "0 0 * * *", "@hourly": "0 * * * *", "@minutely": "* * * * *" }; function parseElement(element, constraint) { const result = /* @__PURE__ */ new Set(); if (element === "*") { for (let i = constraint.min; i <= constraint.max; i = i + 1) result.add(i); return result; } const listElements = element.split(","); if (listElements.length > 1) { for (const listElement of listElements) { const parsedListElement = parseElement(listElement, constraint); for (const x of parsedListElement) result.add(x); } return result; } const parseSingleElement = (singleElement) => { var _a, _b; singleElement = (_b = (_a = constraint.aliases) === null || _a === void 0 ? void 0 : _a[singleElement.toLowerCase()]) !== null && _b !== void 0 ? _b : singleElement; const parsedElement = Number.parseInt(singleElement, 10); if (Number.isNaN(parsedElement)) throw new Error(`Failed to parse ${element}: ${singleElement} is NaN.`); if (parsedElement < constraint.min || parsedElement > constraint.max) throw new Error(`Failed to parse ${element}: ${singleElement} is outside of constraint range of ${constraint.min} - ${constraint.max}.`); return parsedElement; }; const rangeSegments = /^(([0-9a-zA-Z]+)-([0-9a-zA-Z]+)|\*)(\/([0-9]+))?$/.exec(element); if (rangeSegments === null) { result.add(parseSingleElement(element)); return result; } let parsedStart = rangeSegments[1] === "*" ? constraint.min : parseSingleElement(rangeSegments[2]); const parsedEnd = rangeSegments[1] === "*" ? constraint.max : parseSingleElement(rangeSegments[3]); if (constraint === weekdayConstraint && parsedStart === 7 && parsedEnd !== 7) parsedStart = 0; if (parsedStart > parsedEnd) throw new Error(`Failed to parse ${element}: Invalid range (start: ${parsedStart}, end: ${parsedEnd}).`); const step = rangeSegments[5]; let parsedStep = 1; if (step !== void 0) { parsedStep = Number.parseInt(step, 10); if (Number.isNaN(parsedStep)) throw new Error(`Failed to parse step: ${step} is NaN.`); if (parsedStep < 1) throw new Error(`Failed to parse step: Expected ${step} to be greater than 0.`); } for (let i = parsedStart; i <= parsedEnd; i = i + parsedStep) result.add(i); return result; } /** Parses a cron expression into a Cron instance. */ function parseCronExpression(cronExpression) { var _a; if (typeof cronExpression !== "string") throw new TypeError("Invalid cron expression: must be of type string."); cronExpression = (_a = timeNicknames[cronExpression.toLowerCase()]) !== null && _a !== void 0 ? _a : cronExpression; const elements = cronExpression.split(" ").filter((elem) => elem.length > 0); if (elements.length < 5 || elements.length > 6) throw new Error("Invalid cron expression: expected 5 or 6 elements."); const rawSeconds = elements.length === 6 ? elements[0] : "0"; const rawMinutes = elements.length === 6 ? elements[1] : elements[0]; const rawHours = elements.length === 6 ? elements[2] : elements[1]; const rawDays = elements.length === 6 ? elements[3] : elements[2]; const rawMonths = elements.length === 6 ? elements[4] : elements[3]; const rawWeekdays = elements.length === 6 ? elements[5] : elements[4]; return new Cron({ seconds: parseElement(rawSeconds, secondConstraint), minutes: parseElement(rawMinutes, minuteConstraint), hours: parseElement(rawHours, hourConstraint), days: parseElement(rawDays, dayConstraint), months: new Set(Array.from(parseElement(rawMonths, monthConstraint)).map((x) => x - 1)), weekdays: new Set(Array.from(parseElement(rawWeekdays, weekdayConstraint)).map((x) => x % 7)) }); } //#endregion //#region ../../src/scheduler/providers/CronProvider.ts var CronProvider = class { dt = $inject(DateTimeProvider); alepha = $inject(Alepha); log = $logger(); cronJobs = []; getCronJobs() { return this.cronJobs; } start = $hook({ on: "start", handler: () => { if (this.alepha.isServerless()) { this.log.info("Ignoring cron jobs in serverless environment"); return; } for (const cron of this.cronJobs) if (!cron.running) { cron.running = true; this.log.debug(`Starting cron task '${cron.name}' with '${cron.expression}'`); this.run(cron); } } }); stop = $hook({ on: "stop", handler: () => { for (const cron of this.cronJobs) this.abort(cron); } }); /** * Generic serverless cron trigger. Vercel's platform-emitted entry * point fires `serverless:cron` with the job name; we run the matching * job in-process. On long-running runtimes this listener is harmless * (no one fires the event). */ onServerlessCron = $hook({ on: "serverless:cron", handler: async ({ name }) => { await this.trigger(name); } }); boot(name) { const cron = typeof name === "string" ? this.cronJobs.find((c) => c.name === name) : name; if (!cron) return; cron.running = true; this.log.debug(`Starting cron task '${cron.name}' with '${cron.expression}'`); this.run(cron); } abort(name) { const cron = typeof name === "string" ? this.cronJobs.find((c) => c.name === name) : name; if (!cron?.running) return; cron.running = false; cron.abort?.abort(); this.log.debug(`Cron task '${cron.name}' stopped`); } /** * Registers a cron job. * * It's automatically done when using the `$scheduler` primitive but can also be used manually. */ createCronJob(name, expression, handler, start) { const cron = { name, cron: parseCronExpression(expression), expression, handler, loop: true, abort: new AbortController() }; this.cronJobs.push(cron); if (start && this.alepha.isStarted()) this.boot(cron); } run(task, now = this.dt.now()) { if (!task.running) return; const [next] = task.cron.getNextDates(1, now.toDate()); if (!next) return; const duration = next.getTime() - now.toDate().getTime(); const abort = new AbortController(); task.abort = abort; this.dt.wait(duration, { now: now.valueOf(), signal: abort.signal }).then(() => { if (!task.running) { this.log.trace("Cron task stopped before execution"); return; } this.log.trace("Running cron task"); if (task.executing) { this.log.warn(`Cron task '${task.name}' is still running, skipping this invocation`); if (task.loop) this.run(task, this.dt.of(next)); return; } task.executing = true; task.handler({ now: this.dt.of(next) }).catch((err) => { if (task.onError) task.onError(err); else this.log.error("Error in cron task:", err); }).finally(() => { task.executing = false; }); if (task.loop) this.run(task, this.dt.of(next)); }).catch((err) => { this.log.warn("Issue during cron waiting timer", err); }); } /** * Trigger a specific cron job by name. */ async trigger(name) { const job = this.cronJobs.find((j) => j.name === name); if (!job) { this.log.warn(`Cron job '${name}' not found`); return; } await this.runJobs([job], this.dt.now()); } /** * Trigger all registered cron jobs. */ async triggerAll() { await this.runJobs(this.cronJobs, this.dt.now()); } /** * Run multiple cron jobs in parallel. */ async runJobs(jobs, now) { const failures = (await Promise.allSettled(jobs.map(async (job) => { this.log.debug(`Running cron job '${job.name}'`); try { await job.handler({ now }); this.log.debug(`Cron job '${job.name}' completed`); } catch (error) { this.log.error(`Cron job '${job.name}' failed`, error); throw error; } }))).filter((r) => r.status === "rejected"); if (failures.length > 0) this.log.error(`${failures.length}/${jobs.length} cron jobs failed`); } }; //#endregion //#region ../../src/scheduler/primitives/$scheduler.ts /** * Scheduler primitive. */ const $scheduler = (options) => { return createPrimitive(SchedulerPrimitive, options); }; /** * Scheduler configuration atom. */ const schedulerOptions = $atom({ name: "alepha.scheduler.options", schema: z.object({ prefix: z.text({ description: "Prefix for scheduler lock keys." }).optional() }), default: {} }); var SchedulerPrimitive = class extends Primitive { log = $logger(); settings = $state(schedulerOptions); alepha = $inject(Alepha); dateTimeProvider = $inject(DateTimeProvider); cronProvider = $inject(CronProvider); get name() { return this.options.name ?? `${this.config.service.name}.${this.config.propertyKey}`; } onInit() { if (this.options.interval) this.dateTimeProvider.createInterval(() => this.trigger(), this.options.interval); if (this.options.cron) this.cronProvider.createCronJob(this.name, this.options.cron, () => this.trigger()); } async trigger() { if (!this.alepha.isStarted()) return; const context = this.alepha.context.createContextId(); await this.alepha.context.run(async () => { try { const now = this.dateTimeProvider.now(); await this.alepha.events.emit("scheduler:begin", { name: this.name, now, context }); if (this.options.lock !== false) try { await this.schedulerLock.run({ now }); } catch (error) { if (!(error instanceof LockAcquireError)) throw error; this.log.debug(`Scheduler '${this.name}' tick skipped — lock held by another runner`); } else await this.options.handler({ now }); await this.alepha.events.emit("scheduler:success", { name: this.name, context }, { catch: true }); } catch (error) { await this.alepha.events.emit("scheduler:error", { name: this.name, error, context }, { catch: true }); this.log.error("Error running scheduler:", error); } await this.alepha.events.emit("scheduler:end", { name: this.name, context }, { catch: true }); }, { context }); } schedulerLock = $pipeline({ use: [$lock({ name: () => { return `${this.settings.prefix ? `${this.settings.prefix}:` : ""}scheduler:${this.name}`; } })], handler: async (args) => { await this.options.handler(args); } }); }; $scheduler[KIND] = SchedulerPrimitive; //#endregion //#region ../../src/scheduler/constants/CRON.ts const CRON = { EVERY_MINUTE: "* * * * *", EVERY_5_MINUTES: "*/5 * * * *", EVERY_15_MINUTES: "*/15 * * * *", EVERY_30_MINUTES: "*/30 * * * *", EVERY_HOUR: "0 * * * *", EVERY_DAY_AT_MIDNIGHT: "0 0 * * *" }; //#endregion //#region ../../src/scheduler/providers/WorkerdCronProvider.ts /** * Cloudflare Workers cron provider. * * This provider handles scheduled events from Cloudflare Workers Cron Triggers. * Unlike the Node.js CronProvider, this doesn't use intervals/timeouts - instead, * it reacts to scheduled events triggered by Cloudflare. * * **Usage:** * 1. Define schedulers with `$scheduler({ cron: "0 * * * *", handler: ... })` * 2. Build your app with `alepha build` - cron triggers are automatically added to `wrangler.jsonc` * 3. Deploy to Cloudflare Workers * * **How it works:** * - During build, all registered `$scheduler` cron expressions are collected * - The build generates `wrangler.jsonc` with `triggers.crons` automatically filled * - When Cloudflare fires a cron trigger, the `scheduled` handler emits `cloudflare:scheduled` * - This provider listens to that event and runs matching schedulers * * @see https://developers.cloudflare.com/workers/configuration/cron-triggers/ */ var WorkerdCronProvider = class extends CronProvider { /** * Override to avoid creating AbortController in global scope. * Cloudflare Workers doesn't allow this during initialization. */ createCronJob(name, expression, handler) { this.cronJobs.push({ name, cron: parseCronExpression(expression), expression, handler, loop: false }); } /** * Handle a scheduled event from Cloudflare Workers. */ onScheduledEvent = $hook({ on: "cloudflare:scheduled", handler: async (event) => { const now = this.dt.of(event.scheduledTime); this.log.info("Received scheduled event", { cron: event.cron, scheduledTime: now.format() }); const matchingJobs = this.cronJobs.filter((job) => job.expression === event.cron); if (matchingJobs.length === 0) { const matchingByTime = this.cronJobs.filter((job) => job.cron.matchDate(now.toDate())); if (matchingByTime.length > 0) { this.log.debug(`No exact cron match for '${event.cron}', found ${matchingByTime.length} jobs matching by time`); await this.runJobs(matchingByTime, now); return; } this.log.warn(`No cron jobs found for expression '${event.cron}'`); return; } await this.runJobs(matchingJobs, now); } }); }; //#endregion //#region ../../src/scheduler/index.ts /** * Cron and interval-based task execution. * * **Features:** * - Scheduled tasks with cron expressions (e.g., `0 0 * * *`) * - Interval-based scheduling * - Distributed locking to prevent duplicate execution * - Lifecycle hooks: `begin`, `success`, `error`, `end` * * @module alepha.scheduler */ const AlephaScheduler = $module({ name: "alepha.scheduler", primitives: [$scheduler], services: [AlephaLock, CronProvider] }); //#endregion export { $scheduler, AlephaScheduler, CRON, CronProvider, SchedulerPrimitive, WorkerdCronProvider, schedulerOptions }; //# sourceMappingURL=index.js.map