mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 17:21:30 +00:00
feat: add Cron Triggers support to module framework
- modules may declare crons: [{ schedule, name, handler }]
- handler signature (event, { db, sql, env }) matches init context
- scheduled() export in src/index.js dispatches to matching handlers with fan-out and per-handler error isolation
- registry validates cron entries and collects into registry.crons
- wrangler.toml [triggers] crons must still be populated manually by module author
This commit is contained in:
45
src/modules/cron-dispatcher.js
Normal file
45
src/modules/cron-dispatcher.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @file cron-dispatcher — dispatches a Cloudflare scheduled event to all
|
||||
* matching module cron handlers.
|
||||
*
|
||||
* Design:
|
||||
* - Iterates registry.crons, filters by event.cron === entry.schedule.
|
||||
* - Wraps each handler invocation in try/catch so one failure cannot block
|
||||
* others (equivalent to Promise.allSettled fan-out via ctx.waitUntil).
|
||||
* - ctx.waitUntil is fire-and-forget from Workers' perspective; we wrap in
|
||||
* an async IIFE so errors are caught and logged rather than silently lost.
|
||||
*/
|
||||
|
||||
import { createSqlStore } from "../db/create-sql-store.js";
|
||||
import { createStore } from "../db/create-store.js";
|
||||
|
||||
/**
|
||||
* @param {any} event — Cloudflare ScheduledEvent (has .cron string).
|
||||
* @param {any} env
|
||||
* @param {{ waitUntil: (p: Promise<any>) => void }} ctx
|
||||
* @param {import("./registry.js").Registry} registry
|
||||
*/
|
||||
export function dispatchScheduled(event, env, ctx, registry) {
|
||||
const matching = registry.crons.filter((entry) => entry.schedule === event.cron);
|
||||
|
||||
for (const entry of matching) {
|
||||
const handlerCtx = {
|
||||
db: createStore(entry.module.name, env),
|
||||
sql: createSqlStore(entry.module.name, env),
|
||||
env,
|
||||
};
|
||||
|
||||
ctx.waitUntil(
|
||||
(async () => {
|
||||
try {
|
||||
await entry.handler(event, handlerCtx);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[cron] handler "${entry.name}" (module "${entry.module.name}", schedule "${entry.schedule}") failed:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
})(),
|
||||
);
|
||||
}
|
||||
}
|
||||
66
src/modules/validate-cron.js
Normal file
66
src/modules/validate-cron.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file validate-cron — validates module-registered cron entries.
|
||||
*
|
||||
* Cron entry contract:
|
||||
* - name: ^[a-z0-9_-]{1,32}$ — unique within the module (checked by registry)
|
||||
* - schedule: non-empty string matching a cron-ish pattern
|
||||
* (5 or 6 fields separated by spaces, e.g. "0 1 * * *")
|
||||
* - handler: function
|
||||
*
|
||||
* All errors include module name + cron name for debuggability.
|
||||
*/
|
||||
|
||||
export const CRON_NAME_RE = /^[a-z0-9_-]{1,32}$/;
|
||||
|
||||
/**
|
||||
* Very loose cron expression check: 5 or 6 space-separated tokens.
|
||||
* Cloudflare Workers validates the real expression at deploy time;
|
||||
* we just catch obvious mistakes (empty string, random garbage).
|
||||
*/
|
||||
export const CRON_SCHEDULE_RE = /^\S+(\s+\S+){4,5}$/;
|
||||
|
||||
/**
|
||||
* @typedef {object} ModuleCron
|
||||
* @property {string} name — unique identifier within the module.
|
||||
* @property {string} schedule — cron expression, e.g. "0 1 * * *".
|
||||
* @property {(event: any, ctx: { db: any, sql: any, env: any }) => Promise<void>|void} handler
|
||||
*/
|
||||
|
||||
/**
|
||||
* Throws on any contract violation. Called once per cron entry at registry build.
|
||||
*
|
||||
* @param {any} cron
|
||||
* @param {string} moduleName — for error messages.
|
||||
*/
|
||||
export function validateCron(cron, moduleName) {
|
||||
const prefix = `module "${moduleName}" cron`;
|
||||
|
||||
if (!cron || typeof cron !== "object") {
|
||||
throw new Error(`${prefix}: cron entry is not an object`);
|
||||
}
|
||||
|
||||
// name
|
||||
if (typeof cron.name !== "string") {
|
||||
throw new Error(`${prefix}: name must be a string`);
|
||||
}
|
||||
if (!CRON_NAME_RE.test(cron.name)) {
|
||||
throw new Error(
|
||||
`${prefix} "${cron.name}": name must match ${CRON_NAME_RE} (lowercase letters, digits, underscore, hyphen; 1–32 chars)`,
|
||||
);
|
||||
}
|
||||
|
||||
// schedule
|
||||
if (typeof cron.schedule !== "string" || cron.schedule.trim().length === 0) {
|
||||
throw new Error(`${prefix} "${cron.name}": schedule must be a non-empty string`);
|
||||
}
|
||||
if (!CRON_SCHEDULE_RE.test(cron.schedule.trim())) {
|
||||
throw new Error(
|
||||
`${prefix} "${cron.name}": schedule must be a valid cron expression (5 or 6 space-separated fields), got "${cron.schedule}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// handler
|
||||
if (typeof cron.handler !== "function") {
|
||||
throw new Error(`${prefix} "${cron.name}": handler must be a function`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user